this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(runtime): add toolbar component

Adds a basic state machine, hands it down via context, and subsequently
gets consumed in the toolbar compononent to provide action buttons.

+685 -79
+5 -1
deno.json
··· 15 15 "imports": { 16 16 "@cliffy/command": "jsr:@cliffy/command@1.0.0-rc.8", 17 17 "@deno/esbuild-plugin": "jsr:@deno/esbuild-plugin@^1.1.5", 18 + "@es-toolkit/es-toolkit": "jsr:@es-toolkit/es-toolkit@^1.39.9", 18 19 "@eta-dev/eta": "jsr:@eta-dev/eta@^3.5.0", 20 + "@lit/context": "npm:@lit/context@^1.1.6", 19 21 "@markdoc/markdoc": "https://esm.sh/@markdoc/markdoc@0.5.2", 20 22 "@std/assert": "jsr:@std/assert@1", 21 23 "@std/async": "jsr:@std/async@^1.0.14", ··· 25 27 "hast": "npm:@types/hast@^3.0.4", 26 28 "hast-util-is-element": "npm:hast-util-is-element@^3.0.0", 27 29 "lit": "npm:lit@^3.3.1", 28 - "shiki": "npm:shiki@^3.8.1" 30 + "motion": "npm:motion@^12.23.12", 31 + "shiki": "npm:shiki@^3.8.1", 32 + "xstate": "npm:xstate@^5.20.2" 29 33 } 30 34 }
+52 -3
deno.lock
··· 7 7 "jsr:@cliffy/table@1.0.0-rc.8": "1.0.0-rc.8", 8 8 "jsr:@deno/esbuild-plugin@^1.1.5": "1.1.5", 9 9 "jsr:@deno/loader@~0.3.3": "0.3.4", 10 + "jsr:@es-toolkit/es-toolkit@^1.39.9": "1.39.9", 10 11 "jsr:@eta-dev/eta@^3.5.0": "3.5.0", 11 12 "jsr:@std/assert@1": "1.0.13", 12 13 "jsr:@std/async@^1.0.14": "1.0.14", ··· 24 25 "jsr:@std/path@^1.1.1": "1.1.1", 25 26 "jsr:@std/streams@^1.0.10": "1.0.10", 26 27 "jsr:@std/text@~1.0.7": "1.0.15", 28 + "npm:@lit/context@^1.1.6": "1.1.6", 27 29 "npm:@markdoc/markdoc@*": "0.5.2", 28 30 "npm:@types/hast@^3.0.4": "3.0.4", 29 31 "npm:@types/node@*": "22.15.15", 32 + "npm:esbuild@~0.25.5": "0.25.8", 30 33 "npm:esbuild@~0.25.8": "0.25.8", 31 34 "npm:hast-util-is-element@3": "3.0.0", 32 35 "npm:lit@^3.3.1": "3.3.1", 33 - "npm:shiki@^3.8.1": "3.8.1" 36 + "npm:motion@^12.23.12": "12.23.12", 37 + "npm:shiki@^3.8.1": "3.8.1", 38 + "npm:xstate@^5.20.2": "5.20.2" 34 39 }, 35 40 "jsr": { 36 41 "@cliffy/command@1.0.0-rc.8": { ··· 62 67 "integrity": "c9cde95990b97802a0da6c73c26ab4a48f30d286818845e365dddcd8297abd7d", 63 68 "dependencies": [ 64 69 "jsr:@deno/loader", 65 - "jsr:@std/path" 70 + "jsr:@std/path", 71 + "npm:esbuild@~0.25.5" 66 72 ] 67 73 }, 68 74 "@deno/loader@0.3.4": { 69 75 "integrity": "c56003bc7027606301c3fe62704723b207a9e508c9fb154cf5131abb9d4d2673" 76 + }, 77 + "@es-toolkit/es-toolkit@1.39.9": { 78 + "integrity": "bce5ff73fe5e6e481c3ec6fd0da05e7a90cc7895cbee788441b47ebc280f680e" 70 79 }, 71 80 "@eta-dev/eta@3.5.0": { 72 81 "integrity": "6b70827efc14c7cbf08498ac7a922ecab003641caf3852a6cb5b1b12ee58fb37" ··· 265 274 "@lit-labs/ssr-dom-shim@1.4.0": { 266 275 "integrity": "sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==" 267 276 }, 277 + "@lit/context@1.1.6": { 278 + "integrity": "sha512-M26qDE6UkQbZA2mQ3RjJ3Gzd8TxP+/0obMgE5HfkfLhEEyYE3Bui4A5XHiGPjy0MUGAyxB3QgVuw2ciS0kHn6A==", 279 + "dependencies": [ 280 + "@lit/reactive-element" 281 + ] 282 + }, 268 283 "@lit/reactive-element@2.1.1": { 269 284 "integrity": "sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==", 270 285 "dependencies": [ ··· 418 433 "scripts": true, 419 434 "bin": true 420 435 }, 436 + "framer-motion@12.23.12": { 437 + "integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==", 438 + "dependencies": [ 439 + "motion-dom", 440 + "motion-utils", 441 + "tslib" 442 + ] 443 + }, 421 444 "hast-util-is-element@3.0.0": { 422 445 "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", 423 446 "dependencies": [ ··· 509 532 "micromark-util-types@2.0.2": { 510 533 "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==" 511 534 }, 535 + "motion-dom@12.23.12": { 536 + "integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==", 537 + "dependencies": [ 538 + "motion-utils" 539 + ] 540 + }, 541 + "motion-utils@12.23.6": { 542 + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==" 543 + }, 544 + "motion@12.23.12": { 545 + "integrity": "sha512-8jCD8uW5GD1csOoqh1WhH1A6j5APHVE15nuBkFeRiMzYBdRwyAHmSP/oXSuW0WJPZRXTFdBoG4hY9TFWNhhwng==", 546 + "dependencies": [ 547 + "framer-motion", 548 + "tslib" 549 + ] 550 + }, 512 551 "oniguruma-parser@0.12.1": { 513 552 "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==" 514 553 }, ··· 563 602 }, 564 603 "trim-lines@3.0.1": { 565 604 "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==" 605 + }, 606 + "tslib@2.8.1": { 607 + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" 566 608 }, 567 609 "undici-types@6.21.0": { 568 610 "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" ··· 613 655 "@types/unist", 614 656 "vfile-message" 615 657 ] 658 + }, 659 + "xstate@5.20.2": { 660 + "integrity": "sha512-GZmLmc+WPKfFRxuTDAxCg0cUhS/ZnWaRD86DO8MKizeK4a050jd5k7UNnIQ2jJDWRig2/r0tmVXeezUNIhoz5Q==" 616 661 }, 617 662 "zwitch@2.0.4": { 618 663 "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==" ··· 630 675 "dependencies": [ 631 676 "jsr:@cliffy/command@1.0.0-rc.8", 632 677 "jsr:@deno/esbuild-plugin@^1.1.5", 678 + "jsr:@es-toolkit/es-toolkit@^1.39.9", 633 679 "jsr:@eta-dev/eta@^3.5.0", 634 680 "jsr:@std/assert@1", 635 681 "jsr:@std/async@^1.0.14", 636 682 "jsr:@std/http@^1.0.20", 637 683 "jsr:@std/path@^1.1.1", 684 + "npm:@lit/context@^1.1.6", 638 685 "npm:@types/hast@^3.0.4", 639 686 "npm:esbuild@~0.25.8", 640 687 "npm:hast-util-is-element@3", 641 688 "npm:lit@^3.3.1", 642 - "npm:shiki@^3.8.1" 689 + "npm:motion@^12.23.12", 690 + "npm:shiki@^3.8.1", 691 + "npm:xstate@^5.20.2" 643 692 ] 644 693 } 645 694 }
+40
runtime/actor/machine.ts
··· 1 + import { machineBase } from "./setup.ts"; 2 + 3 + export const presentation = machineBase.createMachine({ 4 + id: "presentation", 5 + initial: "initializing", 6 + context: { 7 + presentationId: "", 8 + currentIndex: 0, 9 + slides: [], 10 + sessionId: crypto.randomUUID(), 11 + channel: new BroadcastChannel(""), 12 + }, 13 + states: { 14 + initializing: { 15 + on: { 16 + "presentation.start": { 17 + actions: ["registerSlides"], 18 + target: "independent", 19 + }, 20 + }, 21 + }, 22 + independent: { 23 + entry: ["scrollToSlide"], 24 + on: { 25 + "navigate.next": { 26 + actions: ["nextSlide", "updateUrl"], 27 + }, 28 + "navigate.previous": { 29 + actions: ["previousSlide", "updateUrl"], 30 + }, 31 + "navigate.scroll": { 32 + actions: ["updateCurrentSlide", "updateUrl"], 33 + }, 34 + }, 35 + }, 36 + // TODO: add presenter mode 37 + presenting: {}, 38 + following: {}, 39 + }, 40 + });
+74
runtime/actor/setup.ts
··· 1 + import { assign, setup } from "xstate"; 2 + import { Context, Events } from "./types.ts"; 3 + 4 + export const machineBase = setup({ 5 + types: { 6 + context: {} as Context, 7 + events: {} as Events, 8 + }, 9 + actions: { 10 + registerSlides: assign(({ context, event }) => { 11 + if (event.type === "presentation.start") { 12 + const currentIndex = event.slides.findIndex((s) => 13 + s.id === event.currentSlide 14 + ); 15 + 16 + return { 17 + ...context, 18 + presentationId: event.presentationId, 19 + slides: event.slides, 20 + currentIndex: Math.max(currentIndex, 0), 21 + channel: new BroadcastChannel(event.presentationId), 22 + }; 23 + } else { 24 + return context; 25 + } 26 + }), 27 + scrollToSlide({ context }) { 28 + if (context.currentIndex > 0) { 29 + const slide = context.slides[context.currentIndex]; 30 + 31 + document.querySelector(`#${slide.id}`)?.scrollIntoView({ 32 + behavior: "instant", 33 + }); 34 + } 35 + }, 36 + updateUrl({ context }) { 37 + const url = new URL(document.URL); 38 + const hash = `#${context.slides[context.currentIndex].id}`; 39 + 40 + url.hash = hash; 41 + 42 + location.replace(url); 43 + }, 44 + updateCurrentSlide: assign({ 45 + currentIndex: ({ context, event }) => { 46 + if (event.type === "navigate.scroll") { 47 + const index = context.slides.findIndex((s) => s.id === event.slideId); 48 + 49 + return Math.max(index, 0); 50 + } 51 + 52 + return context.currentIndex; 53 + }, 54 + }), 55 + nextSlide: assign({ 56 + currentIndex: ({ context }) => { 57 + if (context.currentIndex + 1 > context.slides.length - 1) { 58 + return context.currentIndex; 59 + } 60 + 61 + return context.currentIndex + 1; 62 + }, 63 + }), 64 + previousSlide: assign({ 65 + currentIndex: ({ context }) => { 66 + if (context.currentIndex - 1 < 0) { 67 + return context.currentIndex; 68 + } 69 + 70 + return context.currentIndex - 1; 71 + }, 72 + }), 73 + }, 74 + });
+33
runtime/actor/types.ts
··· 1 + import type { ActorRefFrom } from "xstate"; 2 + import type { Slide } from "../components/slide.ts"; 3 + import { presentation } from "./machine.ts"; 4 + 5 + export interface Context { 6 + presentationId: string; 7 + sessionId: string; 8 + currentIndex: number; 9 + slides: Slide[]; 10 + channel: BroadcastChannel; 11 + // slideObserver: IntersectionObserver; 12 + } 13 + 14 + type NavigationEvent = { 15 + type: "navigate.next"; 16 + } | { 17 + type: "navigate.previous"; 18 + } | { 19 + type: "navigate.scroll"; 20 + slideId: string; 21 + }; 22 + 23 + type PresentationEvent = { 24 + type: "presentation.start"; 25 + presentationId: string; 26 + currentSlide?: string; 27 + slides: Slide[]; 28 + // observer: IntersectionObserver; 29 + }; 30 + 31 + export type Events = NavigationEvent | PresentationEvent; 32 + 33 + export type Presentation = ActorRefFrom<typeof presentation>;
-69
runtime/components/presentation.ts
··· 1 - import { css, html, LitElement } from "lit"; 2 - 3 - export class Presentation extends LitElement { 4 - observer: IntersectionObserver; 5 - 6 - static override properties = { 7 - uuid: { type: String }, 8 - }; 9 - 10 - static override styles = css` 11 - :host { 12 - display: block; 13 - width: 100vw; 14 - height: 100vh; 15 - overflow-y: scroll; 16 - scroll-behavior: smooth; 17 - scroll-snap-type: y mandatory; 18 - } 19 - `; 20 - 21 - constructor() { 22 - super(); 23 - 24 - this.observer = new IntersectionObserver(this.updateUrl, { 25 - root: this, 26 - threshold: 1, 27 - }); 28 - } 29 - 30 - override connectedCallback() { 31 - super.connectedCallback(); 32 - document.addEventListener("readystatechange", () => { 33 - if (document.readyState === "complete") { 34 - const url = new URL(document.URL); 35 - 36 - if (url.hash) { 37 - document.querySelector(url.hash)?.scrollIntoView({ 38 - behavior: "instant", 39 - }); 40 - } 41 - 42 - document.querySelectorAll("morkdeck-slide").forEach((e) => 43 - this.observer.observe(e) 44 - ); 45 - } 46 - }); 47 - } 48 - 49 - updateUrl(entries: IntersectionObserverEntry[]) { 50 - for (const entry of entries) { 51 - if (entry.isIntersecting) { 52 - const url = new URL(document.URL); 53 - const id = entry.target.getAttribute("id"); 54 - 55 - url.hash = `#${id}`; 56 - 57 - location.replace(url.toString()); 58 - 59 - break; 60 - } 61 - } 62 - } 63 - 64 - override render() { 65 - return html` 66 - <slot></slot> 67 - `; 68 - } 69 - }
+96
runtime/components/presentation/wc.ts
··· 1 + import { css, html, LitElement, unsafeCSS } from "lit"; 2 + import { 3 + customElement, 4 + property, 5 + queryAssignedElements, 6 + state, 7 + } from "lit/decorators.js"; 8 + import { provide } from "@lit/context"; 9 + import { createActor } from "xstate"; 10 + import { spring } from "motion"; 11 + import { presentation } from "../../actor/machine.ts"; 12 + import type { Presentation } from "../../actor/types.ts"; 13 + import { Slide } from "../slide.ts"; 14 + import { context, observerContext } from "../../context.ts"; 15 + 16 + @customElement("morkdeck-presentation") 17 + export class PresentationWC extends LitElement { 18 + @provide({ context }) 19 + accessor presentation: Presentation = createActor(presentation).start(); 20 + 21 + @provide({ context: observerContext }) 22 + accessor observer: IntersectionObserver = new IntersectionObserver( 23 + this.fireUpdates.bind(this), 24 + { 25 + root: this, 26 + threshold: 0.7, 27 + }, 28 + ); 29 + 30 + @state() 31 + accessor currentIndex = 0; 32 + 33 + @property({ type: String }) 34 + accessor uuid: string = ""; 35 + 36 + @queryAssignedElements({ selector: "morkdeck-slide" }) 37 + accessor slides: Array<Slide> = []; 38 + 39 + static override styles = css` 40 + :host { 41 + display: block; 42 + width: 100vw; 43 + height: 100vh; 44 + overflow-y: scroll; 45 + scroll-behavior: smooth; 46 + scroll-snap-type: y mandatory; 47 + 48 + --presentation-spring: ${unsafeCSS(spring(0.2, 0.1))}; 49 + } 50 + `; 51 + 52 + override connectedCallback() { 53 + super.connectedCallback(); 54 + 55 + document.addEventListener("readystatechange", () => { 56 + // Delay this until page load so that children have been parsed 57 + if (document.readyState === "complete") { 58 + const url = new URL(document.URL); 59 + const currentSlide = url.hash.substring(1); 60 + 61 + this.presentation.send({ 62 + presentationId: this.uuid, 63 + type: "presentation.start", 64 + slides: this.slides, 65 + currentSlide: currentSlide || undefined, 66 + }); 67 + } 68 + }); 69 + } 70 + 71 + // startPresentation() { 72 + // const actor = createActor(presentation); 73 + 74 + // return actor.start(); 75 + // } 76 + 77 + fireUpdates(entries: IntersectionObserverEntry[]) { 78 + for (const entry of entries) { 79 + if (entry.isIntersecting) { 80 + this.presentation.send({ 81 + type: "navigate.scroll", 82 + slideId: entry.target.id, 83 + }); 84 + 85 + break; 86 + } 87 + } 88 + } 89 + 90 + override render() { 91 + return html` 92 + <slot></slot> 93 + <morkdeck-toolbar></morkdeck-toolbar> 94 + `; 95 + } 96 + }
+16 -2
runtime/components/slide.ts
··· 1 - import { css, html, LitElement } from "lit"; 1 + import { css, html } from "lit"; 2 + import { customElement } from "lit/decorators.js"; 3 + import { consume } from "@lit/context"; 4 + import { observerContext } from "../context.ts"; 5 + import { MorkdeckElement } from "../element.ts"; 2 6 3 - export class Slide extends LitElement { 7 + @customElement("morkdeck-slide") 8 + export class Slide extends MorkdeckElement { 4 9 static override styles = css` 5 10 :host { 6 11 display: block; ··· 29 34 background: #1f1d2e; 30 35 } 31 36 `; 37 + 38 + @consume({ context: observerContext }) 39 + accessor slideObserver = new IntersectionObserver(() => {}); 40 + 41 + override connectedCallback(): void { 42 + super.connectedCallback(); 43 + 44 + this.slideObserver.observe(this); 45 + } 32 46 33 47 override render() { 34 48 return html`
+260
runtime/components/toolbar.ts
··· 1 + import { customElement } from "lit/decorators.js"; 2 + import { MorkdeckElement, subscribe } from "../element.ts"; 3 + import { css, html } from "lit"; 4 + 5 + @customElement("morkdeck-toolbar") 6 + export class Toolbar extends MorkdeckElement { 7 + static override styles = css` 8 + :host * { 9 + box-sizing: border-box; 10 + } 11 + 12 + dialog { 13 + all: unset; 14 + } 15 + 16 + button { 17 + all: unset; 18 + } 19 + 20 + .toolbar-container { 21 + position: fixed; 22 + inset: auto 0 0 0; 23 + display: grid; 24 + grid-template-rows: 1fr 2fr; 25 + } 26 + 27 + .toolbar-container > .toolbar { 28 + transform: translateY(100%); 29 + } 30 + 31 + .toolbar-container:hover > .toolbar { 32 + transform: translateY(0); 33 + } 34 + 35 + .toolbar { 36 + display: flex; 37 + grid-row-start: 2; 38 + align-items: center; 39 + justify-content: space-between; 40 + background-color: #26233a; 41 + border-top: 1px solid #403d52; 42 + transition: transform var(--presentation-spring); 43 + } 44 + 45 + .toolset { 46 + border-left: 1px solid; 47 + border-right: 1px solid; 48 + border-color: #403d52; 49 + display: grid; 50 + grid-auto-columns: auto; 51 + grid-auto-flow: column; 52 + align-items: center; 53 + justify-content: center; 54 + } 55 + 56 + .toolset > * + * { 57 + border-left: 1px solid; 58 + border-color: #403d52; 59 + } 60 + 61 + .tool { 62 + display: flex; 63 + align-items: center; 64 + justify-content: center; 65 + color: inherit; 66 + padding: 0.5em; 67 + } 68 + 69 + .info { 70 + flex: 1; 71 + padding: 0 1em; 72 + display: flex; 73 + flex-direction: column; 74 + align-items: center; 75 + justify-content: center; 76 + height: 100%; 77 + gap: 0.25em; 78 + } 79 + `; 80 + 81 + @subscribe("currentIndex") 82 + accessor currentIndex = 0; 83 + 84 + @subscribe("slides") 85 + accessor slides = []; 86 + 87 + toggleFullscreen() { 88 + if (!document.fullscreenElement) { 89 + document.body.requestFullscreen(); 90 + } else { 91 + document.exitFullscreen(); 92 + } 93 + } 94 + 95 + toggleOverview() { 96 + // TODO: Display all slides in overview 97 + } 98 + 99 + goPrev() { 100 + this.deck.send({ type: "navigate.previous" }); 101 + } 102 + 103 + goNext() { 104 + this.deck.send({ type: "navigate.next" }); 105 + } 106 + 107 + toggleShare() { 108 + // TODO: display share menu 109 + } 110 + 111 + togglePresenterMode() { 112 + // TODO: toggle presenter mode 113 + } 114 + 115 + override render() { 116 + return html` 117 + <footer class="toolbar-container"> 118 + <dialog class="toolbar"> 119 + <div class="toolset"> 120 + <button @click="${this.toggleFullscreen}" class="tool" rel="button"> 121 + <svg 122 + xmlns="http://www.w3.org/2000/svg" 123 + width="24" 124 + height="24" 125 + viewBox="0 0 24 24" 126 + fill="none" 127 + stroke="currentColor" 128 + stroke-width="2" 129 + stroke-linecap="round" 130 + stroke-linejoin="round" 131 + class="icon icon-tabler icons-tabler-outline icon-tabler-maximize" 132 + > 133 + <path stroke="none" d="M0 0h24v24H0z" fill="none" /> 134 + <path d="M4 8v-2a2 2 0 0 1 2 -2h2" /> 135 + <path d="M4 16v2a2 2 0 0 0 2 2h2" /> 136 + <path d="M16 4h2a2 2 0 0 1 2 2v2" /> 137 + <path d="M16 20h2a2 2 0 0 0 2 -2v-2" /> 138 + </svg> 139 + </button> 140 + <button @click="${this.toggleOverview}" class="tool" rel="button"> 141 + <svg 142 + xmlns="http://www.w3.org/2000/svg" 143 + width="24" 144 + height="24" 145 + viewBox="0 0 24 24" 146 + fill="currentColor" 147 + class="icon icon-tabler icons-tabler-filled icon-tabler-layout-grid" 148 + > 149 + <path stroke="none" d="M0 0h24v24H0z" fill="none" /> 150 + <path 151 + d="M9 3a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-4a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2z" 152 + /> 153 + <path 154 + d="M19 3a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-4a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2z" 155 + /> 156 + <path 157 + d="M9 13a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-4a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2z" 158 + /> 159 + <path 160 + d="M19 13a2 2 0 0 1 2 2v4a2 2 0 0 1 -2 2h-4a2 2 0 0 1 -2 -2v-4a2 2 0 0 1 2 -2z" 161 + /> 162 + </svg> 163 + </button> 164 + </div> 165 + <div class="toolset"> 166 + <button @click="${this.goPrev}" class="tool" rel="button"> 167 + <svg 168 + xmlns="http://www.w3.org/2000/svg" 169 + width="24" 170 + height="24" 171 + viewBox="0 0 24 24" 172 + fill="none" 173 + stroke="currentColor" 174 + stroke-width="2" 175 + stroke-linecap="round" 176 + stroke-linejoin="round" 177 + class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-left" 178 + > 179 + <path stroke="none" d="M0 0h24v24H0z" fill="none" /> 180 + <path d="M5 12l14 0" /> 181 + <path d="M5 12l6 6" /> 182 + <path d="M5 12l6 -6" /> 183 + </svg> 184 + </button> 185 + <div class="info"> 186 + <span> 187 + ${this.currentIndex + 1} of ${this.slides.length} 188 + </span> 189 + </div> 190 + <button @click="${this.goNext}" class="tool" rel="button"> 191 + <svg 192 + xmlns="http://www.w3.org/2000/svg" 193 + width="24" 194 + height="24" 195 + viewBox="0 0 24 24" 196 + fill="none" 197 + stroke="currentColor" 198 + stroke-width="2" 199 + stroke-linecap="round" 200 + stroke-linejoin="round" 201 + class="icon icon-tabler icons-tabler-outline icon-tabler-arrow-right" 202 + > 203 + <path stroke="none" d="M0 0h24v24H0z" fill="none" /> 204 + <path d="M5 12l14 0" /> 205 + <path d="M13 18l6 -6" /> 206 + <path d="M13 6l6 6" /> 207 + </svg> 208 + </button> 209 + </div> 210 + <div class="toolset"> 211 + <button @click="${this.toggleShare}" class="tool" rel="button"> 212 + <svg 213 + xmlns="http://www.w3.org/2000/svg" 214 + width="24" 215 + height="24" 216 + viewBox="0 0 24 24" 217 + fill="none" 218 + stroke="currentColor" 219 + stroke-width="2" 220 + stroke-linecap="round" 221 + stroke-linejoin="round" 222 + class="icon icon-tabler icons-tabler-outline icon-tabler-link" 223 + > 224 + <path stroke="none" d="M0 0h24v24H0z" fill="none" /> 225 + <path d="M9 15l6 -6" /> 226 + <path d="M11 6l.463 -.536a5 5 0 0 1 7.071 7.072l-.534 .464" /> 227 + <path 228 + d="M13 18l-.397 .534a5.068 5.068 0 0 1 -7.127 0a4.972 4.972 0 0 1 0 -7.071l.524 -.463" 229 + /> 230 + </svg> 231 + </button> 232 + <button @click="${this 233 + .togglePresenterMode}" class="tool" rel="button"> 234 + <svg 235 + xmlns="http://www.w3.org/2000/svg" 236 + width="24" 237 + height="24" 238 + viewBox="0 0 24 24" 239 + fill="none" 240 + stroke="currentColor" 241 + stroke-width="2" 242 + stroke-linecap="round" 243 + stroke-linejoin="round" 244 + class="icon icon-tabler icons-tabler-outline icon-tabler-chalkboard" 245 + > 246 + <path stroke="none" d="M0 0h24v24H0z" fill="none" /> 247 + <path 248 + d="M8 19h-3a2 2 0 0 1 -2 -2v-10a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v11a1 1 0 0 1 -1 1" 249 + /> 250 + <path 251 + d="M11 16m0 1a1 1 0 0 1 1 -1h4a1 1 0 0 1 1 1v1a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1z" 252 + /> 253 + </svg> 254 + </button> 255 + </div> 256 + </dialog> 257 + </footer> 258 + `; 259 + } 260 + }
+7
runtime/context.ts
··· 1 + import { createContext } from "@lit/context"; 2 + 3 + export const context = createContext(Symbol("morkdeck-presentation")); 4 + 5 + export const observerContext = createContext( 6 + Symbol("morkdeck-presentation-observer"), 7 + );
+88
runtime/element.ts
··· 1 + import { LitElement } from "lit"; 2 + import { consume } from "@lit/context"; 3 + import { context } from "./context.ts"; 4 + import { isEqual } from "@es-toolkit/es-toolkit"; 5 + import { Context, Presentation } from "./actor/types.ts"; 6 + import { Subscription } from "xstate"; 7 + 8 + interface ContextSetter<C = Context, K extends keyof C = keyof C, V = C[K]> { 9 + (value: V): void; 10 + } 11 + type SubscribedKeys< 12 + C = Context, 13 + K extends keyof C = keyof C, 14 + V extends C[K] = C[K], 15 + > = { 16 + [K in keyof C]?: ContextSetter<C, K, V>; 17 + }; 18 + 19 + export class MorkdeckElement extends LitElement { 20 + @consume({ context }) 21 + accessor deck: Presentation; 22 + 23 + #subscription?: Subscription; 24 + #subscribedKeys: SubscribedKeys = {}; 25 + 26 + constructor() { 27 + super(); 28 + 29 + this.deck = {} as unknown as Presentation; 30 + } 31 + 32 + subscribeTo( 33 + key: keyof Context, 34 + setter: ContextSetter, 35 + ) { 36 + if (key in this.#subscribedKeys) return; 37 + 38 + this.#subscribedKeys[key] = setter; 39 + this.subscribe(); 40 + } 41 + 42 + subscribe() { 43 + if (!this.#subscription) { 44 + this.#subscription = this.deck.subscribe(({ context }) => { 45 + for (const [key, setter] of Object.entries(this.#subscribedKeys)) { 46 + setter(context[key as keyof Context]); 47 + } 48 + }); 49 + } 50 + } 51 + 52 + override disconnectedCallback(): void { 53 + super.disconnectedCallback(); 54 + 55 + this.#subscription?.unsubscribe(); 56 + } 57 + } 58 + 59 + export function subscribe< 60 + C extends Context = Context, 61 + K extends keyof C = keyof C, 62 + V extends C[K] = C[K], 63 + >( 64 + key: K, 65 + ): <E extends MorkdeckElement>( 66 + target: ClassAccessorDecoratorTarget<E, V>, 67 + context: ClassAccessorDecoratorContext<E, V>, 68 + ) => ClassAccessorDecoratorResult<E, V> { 69 + return (target, context) => { 70 + const { name } = context; 71 + 72 + return { 73 + get(this) { 74 + this.subscribeTo(key as keyof Context, (value) => { 75 + const oldValue = target.get.call(this); 76 + target.set.call(this, value as V); 77 + 78 + this.requestUpdate(name, oldValue, { 79 + attribute: false, 80 + hasChanged: isEqual, 81 + }); 82 + }); 83 + 84 + return target.get.call(this); 85 + }, 86 + }; 87 + }; 88 + }
+14 -4
runtime/morkdeck.ts
··· 1 - import { Presentation } from "./components/presentation.ts"; 2 - import { Slide } from "./components/slide.ts"; 1 + import type { PresentationWC } from "./components/presentation/wc.ts"; 2 + import type { Toolbar } from "./components/toolbar.ts"; 3 + import type { Slide } from "./components/slide.ts"; 3 4 4 - customElements.define("morkdeck-presentation", Presentation); 5 - customElements.define("morkdeck-slide", Slide); 5 + import "./components/presentation/wc.ts"; 6 + import "./components/toolbar.ts"; 7 + import "./components/slide.ts"; 8 + 9 + declare global { 10 + interface HTMLElementTagNameMap { 11 + "morkdeck-presentation": PresentationWC; 12 + "morkdeck-slide": Slide; 13 + "morkdeck-toolbar": Toolbar; 14 + } 15 + }