/** * — reusable web component for HTML decks. * * Handles: * (a) speaker notes — reads * * The :not(:defined) rule prevents a flash of the first slide at its * authored styles before this script runs and attaches the shadow root. * * Slides are the direct element children of . Each slide is * automatically tagged with: * - data-screen-label="NN Label" (1-indexed, for comment flow) * - data-om-validate="no_overflowing_text,no_overlapping_text,slide_sized_text" */ (() => { const DESIGN_W_DEFAULT = 1920; const DESIGN_H_DEFAULT = 1080; const OVERLAY_HIDE_MS = 1800; const VALIDATE_ATTR = 'no_overflowing_text,no_overlapping_text,slide_sized_text'; const pad2 = (n) => String(n).padStart(2, '0'); const stylesheet = ` :host { position: fixed; inset: 0; display: block; background: #000; color: #fff; font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", Helvetica, Arial, sans-serif; overflow: hidden; } /* connectedCallback holds this until document.fonts.ready (capped 2s) so * the first visible paint has the deck's real typography + final rail * layout. opacity (not visibility) so the active slide can't un-hide * itself via the ::slotted([data-deck-active]) visibility:visible rule. * Only the stage/rail hide — the black :host background stays, so the * iframe doesn't flash the page's default white. */ :host([data-fonts-pending]) .stage, :host([data-fonts-pending]) .rail { opacity: 0; pointer-events: none; } .stage { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; } .canvas { position: relative; transform-origin: center center; flex-shrink: 0; background: #fff; will-change: transform; } /* Slides live in light DOM (via ) so authored CSS still applies. We absolutely position each slotted child to stack them. */ ::slotted(*) { position: absolute !important; inset: 0 !important; width: 100% !important; height: 100% !important; box-sizing: border-box !important; overflow: hidden; opacity: 0; pointer-events: none; visibility: hidden; } ::slotted([data-deck-active]) { opacity: 1; pointer-events: auto; visibility: visible; } /* Tap zones for mobile — back/forward thirds like Stories. Transparent, no visible UI, don't block the overlay. */ .tapzones { position: fixed; inset: 0; display: flex; z-index: 2147482000; pointer-events: none; } .tapzone { flex: 1; pointer-events: auto; -webkit-tap-highlight-color: transparent; } /* Only activate tap zones on coarse pointers (touch devices). */ @media (hover: hover) and (pointer: fine) { .tapzones { display: none; } } .overlay { position: fixed; left: 50%; bottom: 22px; transform: translate(-50%, 6px) scale(0.92); filter: blur(6px); display: flex; align-items: center; gap: 4px; padding: 4px; background: #000; color: #fff; border-radius: 999px; font-size: 12px; font-feature-settings: "tnum" 1; letter-spacing: 0.01em; opacity: 0; pointer-events: none; transition: opacity 260ms ease, transform 260ms cubic-bezier(.2,.8,.2,1), filter 260ms ease; transform-origin: center bottom; z-index: 2147483000; user-select: none; } .overlay[data-visible] { opacity: 1; pointer-events: auto; transform: translate(-50%, 0) scale(1); filter: blur(0); } .btn { appearance: none; -webkit-appearance: none; background: transparent; border: 0; margin: 0; padding: 0; color: inherit; font: inherit; cursor: default; display: inline-flex; align-items: center; justify-content: center; height: 28px; min-width: 28px; border-radius: 999px; color: rgba(255,255,255,0.72); transition: background 140ms ease, color 140ms ease; -webkit-tap-highlight-color: transparent; } .btn:hover { background: rgba(255,255,255,0.12); color: #fff; } .btn:active { background: rgba(255,255,255,0.18); } .btn:focus { outline: none; } .btn:focus-visible { outline: none; } .btn::-moz-focus-inner { border: 0; } .btn svg { width: 14px; height: 14px; display: block; } .btn.reset { font-size: 11px; font-weight: 500; letter-spacing: 0.02em; padding: 0 10px 0 12px; gap: 6px; color: rgba(255,255,255,0.72); } .btn.reset .kbd { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 4px; font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 10px; line-height: 1; color: rgba(255,255,255,0.88); background: rgba(255,255,255,0.12); border-radius: 4px; } .count { font-variant-numeric: tabular-nums; color: #fff; font-weight: 500; padding: 0 8px; min-width: 42px; text-align: center; font-size: 12px; } .count .sep { color: rgba(255,255,255,0.45); margin: 0 3px; font-weight: 400; } .count .total { color: rgba(255,255,255,0.55); } .divider { width: 1px; height: 14px; background: rgba(255,255,255,0.18); margin: 0 2px; } /* ── Thumbnail rail ────────────────────────────────────────────────── Fixed column on the left; each thumbnail is a static deep-clone of the light-DOM slide scaled into a 16:9 (or design-aspect) frame. The stage re-fits around it (see _fit); hidden during present / noscale / print so capture geometry and fullscreen output are unchanged. */ .rail { position: fixed; left: 0; top: 0; bottom: 0; width: var(--deck-rail-w, 188px); background: #141414; border-right: 1px solid rgba(255,255,255,0.08); overflow-y: auto; overflow-x: hidden; padding: 12px 10px; box-sizing: border-box; display: flex; flex-direction: column; gap: 12px; z-index: 2147482500; scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.18) transparent; } .rail::-webkit-scrollbar { width: 8px; } .rail::-webkit-scrollbar-track { background: transparent; margin: 2px; } .rail::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.18); border-radius: 4px; border: 2px solid transparent; background-clip: content-box; } .rail::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.28); border: 2px solid transparent; background-clip: content-box; } :host([no-rail]) .rail, :host([noscale]) .rail { display: none; } .rail[data-presenting] { display: none; } /* User-driven show/hide (the TweaksPanel toggle) slides instead of popping. Transitions are gated on :host([data-rail-anim]) — set only for the 200ms around the toggle — so window-resize and rail-width drag (which also call _fit) don't lag behind the cursor. */ .rail[data-user-hidden] { transform: translateX(-100%); } :host([data-rail-anim]) .rail { transition: transform 200ms cubic-bezier(.3,.7,.4,1); } :host([data-rail-anim]) .stage { transition: left 200ms cubic-bezier(.3,.7,.4,1); } :host([data-rail-anim]) .canvas { transition: transform 200ms cubic-bezier(.3,.7,.4,1); } /* transition shorthand replaces rather than merges — repeat the base .overlay opacity/transform/filter transitions so visibility changes during the 200ms toggle window still fade instead of popping. */ :host([data-rail-anim]) .overlay { transition: margin-left 200ms cubic-bezier(.3,.7,.4,1), opacity 260ms ease, transform 260ms cubic-bezier(.2,.8,.2,1), filter 260ms ease; } :host([data-rail-anim]) .tapzones { transition: left 200ms cubic-bezier(.3,.7,.4,1); } .thumb { position: relative; display: flex; align-items: flex-start; gap: 8px; cursor: pointer; user-select: none; } .thumb .num { width: 16px; flex-shrink: 0; font-size: 11px; font-weight: 500; text-align: right; color: rgba(255,255,255,0.55); padding-top: 2px; font-variant-numeric: tabular-nums; } .thumb .frame { position: relative; flex: 1; min-width: 0; aspect-ratio: var(--deck-aspect); background: #fff; border-radius: 4px; outline: 2px solid transparent; outline-offset: 0; overflow: hidden; transition: outline-color 120ms ease; } .thumb:hover .frame { outline-color: rgba(255,255,255,0.25); } .thumb { outline: none; } .thumb:focus-visible .frame { outline-color: rgba(255,255,255,0.5); } .thumb[data-current] .num { color: #fff; } .thumb[data-current] .frame { outline-color: #D97757; } .thumb[data-dragging] { opacity: 0.35; } .thumb::before { content: ''; position: absolute; left: 24px; right: 0; height: 3px; border-radius: 2px; background: #D97757; opacity: 0; pointer-events: none; } .thumb[data-drop="before"]::before { top: -8px; opacity: 1; } .thumb[data-drop="after"]::before { bottom: -8px; opacity: 1; } .thumb[data-skip] .frame { opacity: 0.35; } .thumb[data-skip] .frame::after { content: 'Skipped'; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.45); color: #fff; font-size: 10px; font-weight: 500; letter-spacing: 0.04em; } .ctxmenu { position: fixed; min-width: 150px; padding: 4px; background: #242424; border: 1px solid rgba(255,255,255,0.12); border-radius: 7px; box-shadow: 0 8px 24px rgba(0,0,0,0.45); z-index: 2147483100; display: none; font-size: 12px; } .ctxmenu[data-open] { display: block; } .ctxmenu button { display: block; width: 100%; appearance: none; border: 0; background: transparent; color: #e8e8e8; font: inherit; text-align: left; padding: 6px 10px; border-radius: 4px; cursor: pointer; } .ctxmenu button:hover:not(:disabled) { background: rgba(255,255,255,0.08); } .ctxmenu button:disabled { opacity: 0.35; cursor: default; } .ctxmenu hr { border: 0; border-top: 1px solid rgba(255,255,255,0.1); margin: 4px 2px; } .rail-resize { position: fixed; left: calc(var(--deck-rail-w, 188px) - 3px); top: 0; bottom: 0; width: 6px; cursor: col-resize; z-index: 2147482600; touch-action: none; } .rail-resize:hover, .rail-resize[data-dragging] { background: rgba(255,255,255,0.12); } :host([no-rail]) .rail-resize, :host([noscale]) .rail-resize, .rail[data-presenting] + .rail-resize, .rail[data-user-hidden] + .rail-resize { display: none; } /* Delete-confirm popup — matches the SPA's ConfirmDialog layout (title + message body, depressed footer with Cancel / Delete). */ .confirm-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 2147483200; display: none; align-items: center; justify-content: center; } .confirm-backdrop[data-open] { display: flex; } .confirm { width: 320px; max-width: calc(100vw - 32px); background: #2a2a2a; color: #e8e8e8; border: 1px solid rgba(255,255,255,0.12); border-radius: 12px; box-shadow: 0 12px 32px rgba(0,0,0,0.5); overflow: hidden; font-family: inherit; animation: deck-confirm-in 0.18s ease; } @keyframes deck-confirm-in { from { opacity: 0; transform: scale(0.96); } to { opacity: 1; transform: scale(1); } } .confirm .body { padding: 20px 20px 16px; } .confirm .title { font-size: 14px; font-weight: 600; margin-bottom: 4px; } .confirm .msg { font-size: 13px; line-height: 1.5; color: rgba(255,255,255,0.65); } .confirm .footer { padding: 14px 20px; background: #1f1f1f; border-top: 1px solid rgba(255,255,255,0.08); display: flex; justify-content: flex-end; gap: 8px; } .confirm button { appearance: none; font: inherit; font-size: 13px; font-weight: 500; padding: 8px 16px; border-radius: 8px; cursor: pointer; } .confirm .cancel { background: transparent; border: 0; color: rgba(255,255,255,0.8); } .confirm .cancel:hover { background: rgba(255,255,255,0.08); } .confirm .danger { background: #c96442; border: 1px solid rgba(0,0,0,0.15); color: #fff; box-shadow: 0 1px 3px rgba(166,50,68,0.3), 0 2px 6px rgba(166,50,68,0.18); } .confirm .danger:hover { background: #b5563a; } /* ── Print: one page per slide, no chrome ──────────────────────────── The screen layout stacks every slide at inset:0 inside a scaled canvas; for print we want them in document flow at the authored design size so the browser paginates one slide per sheet. The @page size is set from the width/height attributes via the inline