@heyramzi/ui 1.54.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPONENTS.md +3 -2
- package/README.md +34 -9
- package/package.json +198 -6
- package/src/ai/generation-progress/GenerationProgress.svelte +12 -12
- package/src/ambient.d.ts +8 -0
- package/src/brand/brand.test.ts +67 -0
- package/src/brand/index.ts +137 -0
- package/src/canvas/README.md +236 -0
- package/src/canvas/card/card-frame.svelte +61 -0
- package/src/canvas/card/card.css +39 -0
- package/src/canvas/card/resize-handles.svelte +119 -0
- package/src/canvas/card/resize.ts +33 -0
- package/src/canvas/chrome/chrome-zoom.svelte +120 -0
- package/src/canvas/chrome/chrome.css +231 -0
- package/src/canvas/chrome/lock-badge.svelte +18 -0
- package/src/canvas/chrome/side-handles.svelte +30 -0
- package/src/canvas/chrome/size-chip.svelte +45 -0
- package/src/canvas/chrome/snap-guides.svelte +69 -0
- package/src/canvas/components/icons/ToolIcon.svelte +30 -0
- package/src/canvas/components/shared/svg/PreviewSvg.svelte +67 -0
- package/src/canvas/components/shared/svg/ShapeOutline.svelte +76 -0
- package/src/canvas/components/shared/svg/StrokeNode.svelte +69 -0
- package/src/canvas/components/shared/svg/TextRule.svelte +37 -0
- package/src/canvas/edges/bend-handle.svelte +76 -0
- package/src/canvas/edges/edge-gradient.svelte +30 -0
- package/src/canvas/edges/edge-path.test.ts +58 -0
- package/src/canvas/edges/edge-path.ts +293 -0
- package/src/canvas/edges/gradient-edge.svelte +43 -0
- package/src/canvas/editor/context-menu.test.ts +65 -0
- package/src/canvas/editor/context-menu.ts +48 -0
- package/src/canvas/editor/dispatch.svelte.test.ts +243 -0
- package/src/canvas/editor/dispatch.svelte.ts +205 -0
- package/src/canvas/editor/editor-context.ts +62 -0
- package/src/canvas/editor/editor-mode.ts +67 -0
- package/src/canvas/editor/frame.ts +98 -0
- package/src/canvas/editor/hand-context.ts +15 -0
- package/src/canvas/editor/history-core.test.ts +239 -0
- package/src/canvas/editor/history-core.ts +247 -0
- package/src/canvas/editor/keymap.test.ts +161 -0
- package/src/canvas/editor/keymap.ts +151 -0
- package/src/canvas/editor/layout-actions.test.ts +156 -0
- package/src/canvas/editor/layout-actions.ts +219 -0
- package/src/canvas/editor/lock.test.ts +68 -0
- package/src/canvas/editor/lock.ts +87 -0
- package/src/canvas/editor/rebase.test.ts +208 -0
- package/src/canvas/editor/rebase.ts +272 -0
- package/src/canvas/editor/style.ts +184 -0
- package/src/canvas/editor/text-field-context.ts +41 -0
- package/src/canvas/editor/z-order.test.ts +75 -0
- package/src/canvas/editor/z-order.ts +87 -0
- package/src/canvas/headless/alignment.test.ts +59 -0
- package/src/canvas/headless/alignment.ts +23 -0
- package/src/canvas/headless/break-apart-find.ts +418 -0
- package/src/canvas/headless/break-apart-pieces.ts +104 -0
- package/src/canvas/headless/break-apart-plan.ts +439 -0
- package/src/canvas/headless/break-apart-style.ts +106 -0
- package/src/canvas/headless/break-apart.ts +244 -0
- package/src/canvas/headless/canvas-cleanup.test.ts +181 -0
- package/src/canvas/headless/canvas-cleanup.ts +102 -0
- package/src/canvas/headless/edge-anchors.test.ts +70 -0
- package/src/canvas/headless/edge-anchors.ts +143 -0
- package/src/canvas/headless/edge-memo.test.ts +58 -0
- package/src/canvas/headless/edge-memo.ts +32 -0
- package/src/canvas/headless/edge-serialization.test.ts +55 -0
- package/src/canvas/headless/edge-serialization.ts +32 -0
- package/src/canvas/headless/figure-geometry.ts +293 -0
- package/src/canvas/headless/fit-viewport.test.ts +85 -0
- package/src/canvas/headless/fit-viewport.ts +137 -0
- package/src/canvas/headless/grid.test.ts +34 -0
- package/src/canvas/headless/grid.ts +94 -0
- package/src/canvas/headless/icon-node.test.ts +23 -0
- package/src/canvas/headless/icon-node.ts +66 -0
- package/src/canvas/headless/index.ts +9 -0
- package/src/canvas/headless/linkify.test.ts +23 -0
- package/src/canvas/headless/linkify.ts +25 -0
- package/src/canvas/headless/pen-marks.ts +133 -0
- package/src/canvas/headless/sketch.test.ts +99 -0
- package/src/canvas/headless/sketch.ts +302 -0
- package/src/canvas/headless/snap-guides.test.ts +61 -0
- package/src/canvas/headless/snap-guides.ts +146 -0
- package/src/canvas/headless/svg-figure.test.ts +49 -0
- package/src/canvas/headless/svg-figure.ts +176 -0
- package/src/canvas/headless/table-layout.test.ts +158 -0
- package/src/canvas/headless/table-layout.ts +309 -0
- package/src/canvas/index.ts +35 -0
- package/src/canvas/lod/lod-mode.svelte.ts +30 -0
- package/src/canvas/lod/lod-switch.svelte +140 -0
- package/src/canvas/lod/render-all.svelte.ts +52 -0
- package/src/canvas/ops/arrange.ts +108 -0
- package/src/canvas/ops/ops.test.ts +98 -0
- package/src/canvas/ops/ops.ts +347 -0
- package/src/canvas/pen/PenField.svelte +128 -0
- package/src/canvas/pen/PenField.test.ts +87 -0
- package/src/canvas/pen/PenOverlay.svelte +172 -0
- package/src/canvas/pen/ShapeNode.svelte +64 -0
- package/src/canvas/pen/TextNode.svelte +97 -0
- package/src/canvas/pen/colors.ts +28 -0
- package/src/canvas/pen/eraser.ts +46 -0
- package/src/canvas/pen/geometry.ts +148 -0
- package/src/canvas/pen/host-field.test.svelte +13 -0
- package/src/canvas/pen/index.ts +40 -0
- package/src/canvas/pen/ops.ts +16 -0
- package/src/canvas/pen/pen-session.ts +177 -0
- package/src/canvas/pen/pen.css +149 -0
- package/src/canvas/pen/plugin.ts +139 -0
- package/src/canvas/pen/shapes.ts +104 -0
- package/src/canvas/pen/stroke-path.ts +54 -0
- package/src/canvas/pen/types.ts +96 -0
- package/src/canvas/pen/ui.ts +26 -0
- package/src/canvas/plugin.test.ts +45 -0
- package/src/canvas/plugin.ts +159 -0
- package/src/canvas/screen/GlyphIcon.svelte +25 -0
- package/src/canvas/screen/PartView.svelte +176 -0
- package/src/canvas/screen/ScreenBody.svelte +142 -0
- package/src/canvas/screen/ScreenCard.svelte +32 -0
- package/src/canvas/screen/glyphs/button.svg +1 -0
- package/src/canvas/screen/glyphs/close.svg +1 -0
- package/src/canvas/screen/glyphs/down.svg +1 -0
- package/src/canvas/screen/glyphs/header.svg +1 -0
- package/src/canvas/screen/glyphs/hills.svg +1 -0
- package/src/canvas/screen/glyphs/image.svg +1 -0
- package/src/canvas/screen/glyphs/input.svg +1 -0
- package/src/canvas/screen/glyphs/list.svg +1 -0
- package/src/canvas/screen/glyphs/modal.svg +1 -0
- package/src/canvas/screen/glyphs/plus.svg +1 -0
- package/src/canvas/screen/glyphs/up.svg +1 -0
- package/src/canvas/screen/glyphs.ts +40 -0
- package/src/canvas/screen/labels.ts +41 -0
- package/src/canvas/screen/parts.ts +164 -0
- package/src/canvas/screen/plugin.ts +67 -0
- package/src/canvas/screen/screen.css +593 -0
- package/src/canvas/screen/types.ts +47 -0
- package/src/canvas/screen/ui.ts +13 -0
- package/src/canvas/test-utils.ts +18 -0
- package/src/canvas/types.ts +53 -0
- package/src/canvas/ui/CanvasToolbar.svelte +169 -0
- package/src/canvas/ui/FrameLayer.svelte +116 -0
- package/src/canvas/ui/PenLayer.svelte +39 -0
- package/src/canvas/ui/StylePanel.svelte +268 -0
- package/src/canvas/ui/canvas-grid.svelte +156 -0
- package/src/canvas/ui/canvas-ui.css +350 -0
- package/src/canvas/ui/forward-wheel.ts +29 -0
- package/src/canvas/ui/icons.ts +124 -0
- package/src/canvas/ui/place-panel.ts +80 -0
- package/src/canvas/ui/shortcut-tip.test.ts +16 -0
- package/src/canvas/ui/shortcut-tip.ts +17 -0
- package/src/canvas/ui/tools.ts +146 -0
- package/src/charts/README.md +104 -0
- package/src/charts/ambient.d.ts +4 -0
- package/src/charts/animated-value.css +7 -0
- package/src/charts/animated-value.svelte +110 -0
- package/src/charts/area-chart.css +29 -0
- package/src/charts/area-chart.svelte +204 -0
- package/src/charts/bar-chart.css +119 -0
- package/src/charts/bar-chart.svelte +275 -0
- package/src/charts/charts.css +201 -0
- package/src/charts/crosshair.svelte +35 -0
- package/src/charts/dashboard/chart.ts +5 -0
- package/src/charts/dashboard/dashboard-empty.svelte +55 -0
- package/src/charts/dashboard/dashboard-section.svelte +58 -0
- package/src/charts/dashboard/data-panel.svelte +81 -0
- package/src/charts/dashboard/data-sort-head.svelte +102 -0
- package/src/charts/dashboard/data-table.svelte +278 -0
- package/src/charts/dashboard/data-table.ts +26 -0
- package/src/charts/dashboard/index.ts +25 -0
- package/src/charts/dashboard/panel-grid.svelte +43 -0
- package/src/charts/dashboard/period-filter.svelte +53 -0
- package/src/charts/dashboard/rating-chip.svelte +35 -0
- package/src/charts/dashboard/stat-band.svelte +47 -0
- package/src/charts/dashboard/stat-tile.svelte +176 -0
- package/src/charts/dashboard/windows.ts +8 -0
- package/src/charts/donut-chart.css +56 -0
- package/src/charts/donut-chart.svelte +158 -0
- package/src/charts/edge-fade.svelte +43 -0
- package/src/charts/edge-fade.ts +16 -0
- package/src/charts/figure.css +100 -0
- package/src/charts/figure.svelte +122 -0
- package/src/charts/gauge-chart.css +94 -0
- package/src/charts/gauge-chart.svelte +302 -0
- package/src/charts/hover-band.test.ts +30 -0
- package/src/charts/hover-band.ts +21 -0
- package/src/charts/hover-focus.svelte +85 -0
- package/src/charts/index.ts +31 -0
- package/src/charts/legend.css +136 -0
- package/src/charts/legend.svelte +112 -0
- package/src/charts/line-chart.css +26 -0
- package/src/charts/line-chart.svelte +256 -0
- package/src/charts/motion.ts +19 -0
- package/src/charts/number-chart.css +179 -0
- package/src/charts/number-chart.svelte +187 -0
- package/src/charts/primitives/chart-container.svelte +57 -0
- package/src/charts/primitives/chart-style.svelte +36 -0
- package/src/charts/primitives/chart-tooltip.svelte +161 -0
- package/src/charts/primitives/chart-utils.ts +54 -0
- package/src/charts/primitives/index.ts +11 -0
- package/src/charts/reference-line.css +18 -0
- package/src/charts/reference-line.svelte +31 -0
- package/src/charts/reveal.svelte +61 -0
- package/src/charts/ring-chart.css +104 -0
- package/src/charts/ring-chart.svelte +166 -0
- package/src/charts/sparkline.css +45 -0
- package/src/charts/sparkline.svelte +141 -0
- package/src/charts/stat-card.css +105 -0
- package/src/charts/stat-card.svelte +267 -0
- package/src/charts/time-series-chart.css +28 -0
- package/src/charts/time-series-chart.svelte +179 -0
- package/src/charts/trend-pill.css +38 -0
- package/src/charts/trend-pill.svelte +48 -0
- package/src/components/copy/copy-button.svelte +1 -1
- package/src/components/docs/doc-page.svelte +1 -1
- package/src/components/docs/on-this-page.svelte +2 -2
- package/src/components/list-page/empty-state.svelte +1 -1
- package/src/components/list-page/filter-popover.svelte +1 -1
- package/src/components/list-page/section-label.svelte +1 -1
- package/src/components/segmented/segment.svelte +4 -4
- package/src/components/theme/theme-provider.svelte +6 -4
- package/src/components/toolbar/index.ts +5 -0
- package/src/components/toolbar/toolbar.svelte +168 -0
- package/src/components/toolbar/toolbar.test.ts +66 -0
- package/src/editor/README.md +58 -0
- package/src/editor/ambient.d.ts +1 -0
- package/src/editor/block-id.ts +77 -0
- package/src/editor/block-ops.test.ts +237 -0
- package/src/editor/block-ops.ts +157 -0
- package/src/editor/content.css +418 -0
- package/src/editor/editor-toolbar.svelte +303 -0
- package/src/editor/editor.svelte +462 -0
- package/src/editor/extensions.ts +145 -0
- package/src/editor/image-toolbar.svelte +123 -0
- package/src/editor/index.ts +17 -0
- package/src/editor/inline-editor.svelte +261 -0
- package/src/editor/markdown-escaping.test.ts +158 -0
- package/src/editor/markdown-marks.test.ts +89 -0
- package/src/editor/markdown-test-helpers.ts +13 -0
- package/src/editor/markdown-youtube.test.ts +93 -0
- package/src/editor/markdown.test.ts +250 -0
- package/src/editor/markdown.ts +511 -0
- package/src/editor/renderer.svelte +92 -0
- package/src/editor/serializer.test.ts +133 -0
- package/src/editor/serializer.ts +370 -0
- package/src/editor/slash-items.ts +115 -0
- package/src/editor/slash-menu.svelte +159 -0
- package/src/editor/youtube-url.ts +35 -0
- package/src/effects/interactive-hover-button/InteractiveHoverButton.svelte +13 -13
- package/src/lib/result.ts +19 -0
- package/src/onboarding/README.md +120 -0
- package/src/onboarding/define-tours.test.ts +50 -0
- package/src/onboarding/define-tours.ts +31 -0
- package/src/onboarding/demo.svelte +86 -0
- package/src/onboarding/index.ts +12 -0
- package/src/onboarding/onboarding-provider.svelte +120 -0
- package/src/onboarding/onboarding.svelte.ts +24 -0
- package/src/onboarding/pick.test.ts +79 -0
- package/src/onboarding/pick.ts +36 -0
- package/src/onboarding/stores.test.ts +103 -0
- package/src/onboarding/stores.ts +58 -0
- package/src/onboarding/tour-card.svelte +70 -0
- package/src/onboarding/tour-runner.svelte +197 -0
- package/src/onboarding/types.ts +34 -0
- package/src/onboarding/wait.ts +29 -0
- package/src/shadcn/command/command-input.svelte +1 -1
- package/src/slides/Controls.svelte +138 -0
- package/src/slides/Deck.svelte +271 -0
- package/src/slides/Fragment.svelte +71 -0
- package/src/slides/Overview.svelte +113 -0
- package/src/slides/Presenter.svelte +292 -0
- package/src/slides/README.md +89 -0
- package/src/slides/Slide.svelte +179 -0
- package/src/slides/SlideList.svelte +19 -0
- package/src/slides/ambient.d.ts +1 -0
- package/src/slides/bridges.ts +69 -0
- package/src/slides/deck-controller.svelte.ts +292 -0
- package/src/slides/demo-slides.svelte +358 -0
- package/src/slides/demo.svelte +176 -0
- package/src/slides/export/capture.ts +113 -0
- package/src/slides/export/ffmpeg.ts +120 -0
- package/src/slides/export/index.ts +9 -0
- package/src/slides/export/launch.ts +10 -0
- package/src/slides/export/pdf.ts +152 -0
- package/src/slides/export/types.ts +48 -0
- package/src/slides/index.ts +19 -0
- package/src/slides/labels.ts +48 -0
- package/src/slides/layout.ts +51 -0
- package/src/slides/model.ts +47 -0
- package/src/slides/navigation.ts +100 -0
- package/src/slides/theme.css +205 -0
- package/src/slides/types.ts +86 -0
- package/src/tasks/README.md +312 -0
- package/src/tasks/ambient.d.ts +3 -0
- package/src/tasks/board-order.ts +137 -0
- package/src/tasks/dates.ts +31 -0
- package/src/tasks/define-statuses.ts +32 -0
- package/src/tasks/demo.svelte +389 -0
- package/src/tasks/detail/detail-demo.svelte +186 -0
- package/src/tasks/detail/detail-types.ts +54 -0
- package/src/tasks/detail/task-chip.svelte +28 -0
- package/src/tasks/detail/task-detail.svelte +315 -0
- package/src/tasks/detail/task-property.svelte +17 -0
- package/src/tasks/detail/task-thread.svelte +258 -0
- package/src/tasks/gantt/gantt-bar.svelte +243 -0
- package/src/tasks/gantt/gantt-grid.svelte +94 -0
- package/src/tasks/gantt/gantt-group-row.svelte +55 -0
- package/src/tasks/gantt/gantt-header.svelte +71 -0
- package/src/tasks/gantt/gantt-task-row.svelte +186 -0
- package/src/tasks/gantt/gantt.css +22 -0
- package/src/tasks/gantt/gantt.test.ts +77 -0
- package/src/tasks/gantt/logic.test.ts +163 -0
- package/src/tasks/gantt/logic.ts +248 -0
- package/src/tasks/inbox/inbox-card.svelte +283 -0
- package/src/tasks/inbox/inbox-draft.svelte +84 -0
- package/src/tasks/inbox/inbox-logic.ts +55 -0
- package/src/tasks/inbox/inbox-statuses.ts +48 -0
- package/src/tasks/inbox/inbox-trace.svelte +45 -0
- package/src/tasks/inbox/inbox-types.ts +108 -0
- package/src/tasks/inbox/inbox.svelte +203 -0
- package/src/tasks/inbox/index.ts +23 -0
- package/src/tasks/index.ts +85 -0
- package/src/tasks/kanban-board.svelte +561 -0
- package/src/tasks/kanban-column.svelte +117 -0
- package/src/tasks/kanban-drag.ts +38 -0
- package/src/tasks/kanban-types.ts +65 -0
- package/src/tasks/presets/sales.ts +16 -0
- package/src/tasks/record/demo-data.ts +286 -0
- package/src/tasks/record/field-section.svelte +53 -0
- package/src/tasks/record/fields.ts +142 -0
- package/src/tasks/record/format.ts +87 -0
- package/src/tasks/record/gutter.ts +2 -0
- package/src/tasks/record/index.ts +54 -0
- package/src/tasks/record/info-list.svelte +316 -0
- package/src/tasks/record/kinds.ts +42 -0
- package/src/tasks/record/logic.ts +324 -0
- package/src/tasks/record/next-up.svelte +70 -0
- package/src/tasks/record/project-list.svelte +133 -0
- package/src/tasks/record/record-demo.svelte +324 -0
- package/src/tasks/record/record-header.svelte +70 -0
- package/src/tasks/record/record-shell.svelte +138 -0
- package/src/tasks/record/record-stats.svelte +48 -0
- package/src/tasks/record/record-timeline.svelte +231 -0
- package/src/tasks/record/record.test.ts +350 -0
- package/src/tasks/record/steps-rail.svelte +474 -0
- package/src/tasks/record/types.ts +280 -0
- package/src/tasks/record/where-we-are.svelte +121 -0
- package/src/tasks/reorder.ts +19 -0
- package/src/tasks/side-panel-types.ts +16 -0
- package/src/tasks/side-panel.svelte +193 -0
- package/src/tasks/status-badge.svelte +63 -0
- package/src/tasks/status-dot.svelte +228 -0
- package/src/tasks/status-icon.svelte +30 -0
- package/src/tasks/status-types.ts +40 -0
- package/src/tasks/task-gantt.svelte +230 -0
- package/src/tasks/task-list.svelte +470 -0
- package/src/tasks/task-types.ts +33 -0
- package/src/tasks/time/duration.ts +69 -0
- package/src/tasks/time/time-chip.svelte +62 -0
- package/src/tasks/time/time-dot.svelte +58 -0
- package/src/tasks/time/time-ring.svelte +74 -0
- package/src/tasks/time/time-tracker.svelte +565 -0
- package/src/tasks/time/time-types.ts +74 -0
- package/src/tasks/use-kanban-dnd.svelte.ts +173 -0
- package/src/tokens/base.css +5 -5
- package/src/tokens/palette.css +115 -120
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
3
|
+
import Deck from './Deck.svelte';
|
|
4
|
+
import type { DeckController } from './deck-controller.svelte.js';
|
|
5
|
+
import { resolveLabels } from './labels.js';
|
|
6
|
+
import { DEFAULT_CANVAS, type DeckProps } from './types.js';
|
|
7
|
+
|
|
8
|
+
interface Props
|
|
9
|
+
extends Pick<
|
|
10
|
+
DeckProps,
|
|
11
|
+
'id' | 'title' | 'locale' | 'canvas' | 'theme' | 'transition' | 'slides' | 'registry' | 'labels' | 'style' | 'children'
|
|
12
|
+
> {
|
|
13
|
+
/** The channel name the audience deck uses as its `sync`. */
|
|
14
|
+
sync: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let { sync, id, title, locale, canvas = DEFAULT_CANVAS, theme = 'ink', transition, slides, registry, labels: overrides, style, children }: Props =
|
|
18
|
+
$props();
|
|
19
|
+
|
|
20
|
+
const labels = $derived(resolveLabels(overrides));
|
|
21
|
+
|
|
22
|
+
let index = $state(0);
|
|
23
|
+
let step = $state(0);
|
|
24
|
+
let now = $state<DeckController>();
|
|
25
|
+
|
|
26
|
+
const upcoming = $derived(now?.upcoming ?? null);
|
|
27
|
+
const notes = $derived(now?.current?.notes);
|
|
28
|
+
const steps = $derived(now?.current?.steps ?? 0);
|
|
29
|
+
|
|
30
|
+
let elapsed = $state(0);
|
|
31
|
+
let running = $state(true);
|
|
32
|
+
$effect(() => {
|
|
33
|
+
if (!running) return;
|
|
34
|
+
const base = untrack(() => elapsed);
|
|
35
|
+
const start = Date.now();
|
|
36
|
+
const tick = setInterval(() => (elapsed = base + Math.round((Date.now() - start) / 1000)), 250);
|
|
37
|
+
return () => clearInterval(tick);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const clock = $derived.by(() => {
|
|
41
|
+
const [h, m, s] = [Math.floor(elapsed / 3600), Math.floor(elapsed / 60) % 60, elapsed % 60];
|
|
42
|
+
const pad = (n: number) => String(n).padStart(2, '0');
|
|
43
|
+
return h ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function onkeydown(event: KeyboardEvent) {
|
|
47
|
+
if (event.key === 't' && !event.metaKey && !event.ctrlKey && !event.altKey) running = !running;
|
|
48
|
+
}
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<svelte:window {onkeydown} />
|
|
52
|
+
<svelte:head>
|
|
53
|
+
<title>{title ?? labels.deck}</title>
|
|
54
|
+
</svelte:head>
|
|
55
|
+
|
|
56
|
+
<div class="presenter deck-surface deck--{theme}" lang={locale} style:--ratio={canvas.w / canvas.h} {style}>
|
|
57
|
+
<header class="presenter__bar">
|
|
58
|
+
<h1 class="presenter__title">{title ?? labels.deck}</h1>
|
|
59
|
+
{#if now}
|
|
60
|
+
<span class="presenter__where">
|
|
61
|
+
{labels.slide(now.ordinal.n, now.ordinal.of)}{#if steps > 0}<span class="presenter__steps">{now.position.step}/{steps}</span>{/if}
|
|
62
|
+
</span>
|
|
63
|
+
{/if}
|
|
64
|
+
<div class="presenter__timer" role="timer" aria-label={labels.timer}>
|
|
65
|
+
<span class="presenter__clock">{clock}</span>
|
|
66
|
+
<button type="button" class="presenter__button" onclick={() => (running = !running)}>
|
|
67
|
+
{running ? labels.pauseTimer : labels.startTimer}
|
|
68
|
+
</button>
|
|
69
|
+
<button type="button" class="presenter__button" onclick={() => (elapsed = 0)}>{labels.resetTimer}</button>
|
|
70
|
+
</div>
|
|
71
|
+
</header>
|
|
72
|
+
|
|
73
|
+
<main class="presenter__main">
|
|
74
|
+
<section class="presenter__now" aria-label={labels.current}>
|
|
75
|
+
<div class="presenter__frame" style:aspect-ratio="{canvas.w} / {canvas.h}">
|
|
76
|
+
<Deck
|
|
77
|
+
bind:index
|
|
78
|
+
bind:step
|
|
79
|
+
bind:controller={now}
|
|
80
|
+
{sync}
|
|
81
|
+
{id}
|
|
82
|
+
{title}
|
|
83
|
+
{locale}
|
|
84
|
+
{canvas}
|
|
85
|
+
{theme}
|
|
86
|
+
{transition}
|
|
87
|
+
{slides}
|
|
88
|
+
{registry}
|
|
89
|
+
{labels}
|
|
90
|
+
{style}
|
|
91
|
+
controls={false}
|
|
92
|
+
>
|
|
93
|
+
{@render children?.()}
|
|
94
|
+
</Deck>
|
|
95
|
+
</div>
|
|
96
|
+
</section>
|
|
97
|
+
|
|
98
|
+
<aside class="presenter__side">
|
|
99
|
+
<section aria-label={labels.upcoming}>
|
|
100
|
+
<h2 class="presenter__heading">{labels.upcoming}</h2>
|
|
101
|
+
{#if upcoming !== null}
|
|
102
|
+
<div class="presenter__frame presenter__frame--next" style:aspect-ratio="{canvas.w} / {canvas.h}" inert>
|
|
103
|
+
<Deck
|
|
104
|
+
index={upcoming}
|
|
105
|
+
step={Number.MAX_SAFE_INTEGER}
|
|
106
|
+
{id}
|
|
107
|
+
{canvas}
|
|
108
|
+
{theme}
|
|
109
|
+
{slides}
|
|
110
|
+
{registry}
|
|
111
|
+
{labels}
|
|
112
|
+
{style}
|
|
113
|
+
transition="none"
|
|
114
|
+
controls={false}
|
|
115
|
+
progress={false}
|
|
116
|
+
keyboard={false}
|
|
117
|
+
>
|
|
118
|
+
{@render children?.()}
|
|
119
|
+
</Deck>
|
|
120
|
+
</div>
|
|
121
|
+
{:else}
|
|
122
|
+
<p class="presenter__empty">{labels.endOfDeck}</p>
|
|
123
|
+
{/if}
|
|
124
|
+
</section>
|
|
125
|
+
|
|
126
|
+
<section class="presenter__notes" aria-label={labels.notes}>
|
|
127
|
+
<h2 class="presenter__heading">{labels.notes}</h2>
|
|
128
|
+
<div class="presenter__notes-body">
|
|
129
|
+
{#if typeof notes === 'string'}
|
|
130
|
+
<p>{notes}</p>
|
|
131
|
+
{:else if notes}
|
|
132
|
+
{@render notes()}
|
|
133
|
+
{:else}
|
|
134
|
+
<p class="presenter__empty">{labels.noNotes}</p>
|
|
135
|
+
{/if}
|
|
136
|
+
</div>
|
|
137
|
+
</section>
|
|
138
|
+
</aside>
|
|
139
|
+
</main>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<style>
|
|
143
|
+
.presenter {
|
|
144
|
+
display: grid;
|
|
145
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
146
|
+
height: 100dvh;
|
|
147
|
+
color: var(--deck-fg);
|
|
148
|
+
background: var(--deck-bg);
|
|
149
|
+
font-family: var(--deck-font);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.presenter__bar {
|
|
153
|
+
display: flex;
|
|
154
|
+
flex-wrap: wrap;
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 8px 20px;
|
|
157
|
+
padding: 12px 20px;
|
|
158
|
+
border-bottom: 1px solid var(--deck-line);
|
|
159
|
+
}
|
|
160
|
+
.presenter__title {
|
|
161
|
+
flex: 1;
|
|
162
|
+
min-width: 0;
|
|
163
|
+
margin: 0;
|
|
164
|
+
overflow: hidden;
|
|
165
|
+
font: 600 15px/1.2 var(--deck-font);
|
|
166
|
+
text-overflow: ellipsis;
|
|
167
|
+
white-space: nowrap;
|
|
168
|
+
}
|
|
169
|
+
.presenter__where {
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: 10px;
|
|
173
|
+
font: 500 14px/1 var(--deck-font);
|
|
174
|
+
color: var(--deck-muted);
|
|
175
|
+
}
|
|
176
|
+
.presenter__steps {
|
|
177
|
+
padding: 3px 7px;
|
|
178
|
+
font-size: 12px;
|
|
179
|
+
font-variant-numeric: tabular-nums;
|
|
180
|
+
color: var(--deck-on-accent);
|
|
181
|
+
background: var(--deck-accent);
|
|
182
|
+
border-radius: 6px;
|
|
183
|
+
}
|
|
184
|
+
.presenter__timer {
|
|
185
|
+
display: flex;
|
|
186
|
+
align-items: center;
|
|
187
|
+
gap: 8px;
|
|
188
|
+
}
|
|
189
|
+
.presenter__clock {
|
|
190
|
+
min-width: 5ch;
|
|
191
|
+
margin-right: 4px;
|
|
192
|
+
font: 700 28px/1 var(--deck-font-display);
|
|
193
|
+
font-variant-numeric: tabular-nums;
|
|
194
|
+
letter-spacing: -0.02em;
|
|
195
|
+
}
|
|
196
|
+
.presenter__button {
|
|
197
|
+
min-height: 32px;
|
|
198
|
+
padding: 0 12px;
|
|
199
|
+
font: 500 13px/1 var(--deck-font);
|
|
200
|
+
color: var(--deck-fg);
|
|
201
|
+
background: var(--deck-surface);
|
|
202
|
+
border: 1px solid var(--deck-line);
|
|
203
|
+
border-radius: 8px;
|
|
204
|
+
cursor: pointer;
|
|
205
|
+
transition: background-color var(--deck-t-quick) var(--deck-ease);
|
|
206
|
+
}
|
|
207
|
+
@media (hover: hover) and (pointer: fine) {
|
|
208
|
+
.presenter__button:hover {
|
|
209
|
+
background: var(--deck-line);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
.presenter__button:focus-visible {
|
|
213
|
+
outline: 2px solid var(--deck-accent);
|
|
214
|
+
outline-offset: 2px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.presenter__main {
|
|
218
|
+
display: grid;
|
|
219
|
+
grid-template-columns: minmax(0, 1.7fr) minmax(0, 1fr);
|
|
220
|
+
gap: 20px;
|
|
221
|
+
min-height: 0;
|
|
222
|
+
padding: 20px;
|
|
223
|
+
}
|
|
224
|
+
.presenter__now {
|
|
225
|
+
display: grid;
|
|
226
|
+
place-items: center;
|
|
227
|
+
min-height: 0;
|
|
228
|
+
container-type: size;
|
|
229
|
+
}
|
|
230
|
+
.presenter__frame {
|
|
231
|
+
overflow: hidden;
|
|
232
|
+
border-radius: 12px;
|
|
233
|
+
box-shadow: 0 0 0 1px var(--deck-line);
|
|
234
|
+
}
|
|
235
|
+
.presenter__now .presenter__frame {
|
|
236
|
+
width: min(100cqw, calc(100cqh * var(--ratio)));
|
|
237
|
+
}
|
|
238
|
+
.presenter__frame--next {
|
|
239
|
+
width: 100%;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.presenter__side {
|
|
243
|
+
display: grid;
|
|
244
|
+
grid-template-rows: auto minmax(0, 1fr);
|
|
245
|
+
gap: 20px;
|
|
246
|
+
min-height: 0;
|
|
247
|
+
}
|
|
248
|
+
.presenter__heading {
|
|
249
|
+
margin: 0 0 8px;
|
|
250
|
+
font: 600 12px/1 var(--deck-font);
|
|
251
|
+
letter-spacing: 0.02em;
|
|
252
|
+
color: var(--deck-muted);
|
|
253
|
+
}
|
|
254
|
+
.presenter__notes {
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
min-height: 0;
|
|
258
|
+
}
|
|
259
|
+
.presenter__notes-body {
|
|
260
|
+
flex: 1;
|
|
261
|
+
min-height: 0;
|
|
262
|
+
padding-right: 4px;
|
|
263
|
+
overflow-y: auto;
|
|
264
|
+
font-size: clamp(18px, 1.5vw, 26px);
|
|
265
|
+
line-height: 1.5;
|
|
266
|
+
white-space: pre-line;
|
|
267
|
+
text-wrap: pretty;
|
|
268
|
+
}
|
|
269
|
+
.presenter__notes-body p {
|
|
270
|
+
margin: 0;
|
|
271
|
+
}
|
|
272
|
+
.presenter__empty {
|
|
273
|
+
margin: 0;
|
|
274
|
+
color: var(--deck-muted);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
@media (max-width: 860px) {
|
|
278
|
+
.presenter {
|
|
279
|
+
height: auto;
|
|
280
|
+
min-height: 100dvh;
|
|
281
|
+
}
|
|
282
|
+
.presenter__main {
|
|
283
|
+
grid-template-columns: minmax(0, 1fr);
|
|
284
|
+
}
|
|
285
|
+
.presenter__now {
|
|
286
|
+
container-type: inline-size;
|
|
287
|
+
}
|
|
288
|
+
.presenter__now .presenter__frame {
|
|
289
|
+
width: 100%;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
</style>
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# @heyramzi/ui/slides
|
|
2
|
+
|
|
3
|
+
Slide decks for Svelte 5. Slides lay out on a fixed canvas (960 by 540 px by default) and the whole canvas scales to fit its container. Reveals, an overview grid, a presenter view, print and data-driven slide types are in the box. Theming is CSS variables only, so the core has no brand.
|
|
4
|
+
|
|
5
|
+
```svelte
|
|
6
|
+
<script>
|
|
7
|
+
import { Deck, Slide, Fragment } from '@heyramzi/ui/slides';
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<Deck title="Q3 review" urlSync presenter="/present">
|
|
11
|
+
<Slide notes="Open on the number.">
|
|
12
|
+
<h1 class="deck-display">Revenue is up</h1>
|
|
13
|
+
</Slide>
|
|
14
|
+
<Slide>
|
|
15
|
+
<Fragment step={1}>First point</Fragment>
|
|
16
|
+
<Fragment step={2}>Second point</Fragment>
|
|
17
|
+
</Slide>
|
|
18
|
+
</Deck>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Keys: arrows, Space (Shift+Space back), PageUp/PageDown, Home/End, `O` overview, `F` fullscreen, `P` presenter. Swipe works on touch. With several decks on a page, only the sole, focused or hovered deck answers keys.
|
|
22
|
+
|
|
23
|
+
## Deck props
|
|
24
|
+
|
|
25
|
+
| Prop | Default | |
|
|
26
|
+
| --- | --- | --- |
|
|
27
|
+
| `index`, `step` | 0 | Bindable. `index` counts hidden slides. |
|
|
28
|
+
| `canvas` | `{ w: 960, h: 540 }` | Design size in px. |
|
|
29
|
+
| `theme` | `'ink'` | `ink`, `paper` or `auto`. |
|
|
30
|
+
| `transition` | `'fade'` | `fade`, `slide`, `zoom`, `morph` (View Transitions, elements with a shared `data-morph` name) or `none`. |
|
|
31
|
+
| `keyboard` | `true` | Turn off when a slide holds a form. |
|
|
32
|
+
| `fragments` | `true` | Off shows every step at once. |
|
|
33
|
+
| `loop`, `progress`, `controls` | `false`, `true`, `true` | |
|
|
34
|
+
| `urlSync` | `false` | Keeps `?s=` (1 based). Pass `{ param, write }` to rename it or write the URL yourself, for example with SvelteKit's `replaceState`. |
|
|
35
|
+
| `bridge` | `false` | postMessage for iframes. In: `{type:'deck:nav', delta}`, `{type:'deck:go', index, step?}`. Out: `{type:'deck:change', index, step, total}`. Same origin unless `{ origin }`. |
|
|
36
|
+
| `sync` | | Name that mirrors the deck across windows over BroadcastChannel. |
|
|
37
|
+
| `presenter` | | URL of the presenter view. Adds a button and the `p` key. |
|
|
38
|
+
| `labels` | | Partial `DeckLabels` to translate every string. |
|
|
39
|
+
| `controller` | | Bindable `DeckController`: `next()`, `prev()`, `go(i, step)`, `toggleOverview()`, `position`, `ordinal`. |
|
|
40
|
+
| `id`, `title`, `locale`, `onchange`, `class`, `style` | | |
|
|
41
|
+
|
|
42
|
+
`Slide`: `notes` (string or snippet), `hidden`, `label`, `align` (`center`, `start`, `bare`). `Fragment`: `step`, `reveal` (`rise`, `fade`, `scale`), `as`. Fragments that share a step stagger. Authored type classes: `deck-display`, `deck-title`, `deck-heading`, `deck-lead`, `deck-body`, `deck-caption`, `deck-label`, `deck-accent`.
|
|
43
|
+
|
|
44
|
+
## Theming
|
|
45
|
+
|
|
46
|
+
Set variables on `.deck`: `--deck-bg`, `--deck-fg`, `--deck-accent`, `--deck-on-accent`, `--deck-font`, `--deck-font-display`, `--deck-pad`, `--deck-text-*` (design px) and the timing tokens `--deck-t-slide`, `--deck-t-morph`, `--deck-stagger`, `--deck-ease`. They default to the `@heyramzi/ui` motion tokens when present. Motion is transform and opacity only and stops under `prefers-reduced-motion`.
|
|
47
|
+
|
|
48
|
+
## Data-driven decks
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { defineRegistry, defineSlideType } from '@heyramzi/ui/slides/model';
|
|
52
|
+
|
|
53
|
+
const cover = defineSlideType({ type: 'cover', label: 'Cover', layout: CoverLayout });
|
|
54
|
+
const registry = defineRegistry([cover]);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```svelte
|
|
58
|
+
<Deck {...deck} {registry} />
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`deck` is a `DeckData` (`id`, `title`, `locale?`, `canvas`, `theme?`, `slides: SlideInstance[]`). A slide is `{ id, type, content, notes?, hidden? }`. Validate content where it comes in, with the schema you already use.
|
|
62
|
+
|
|
63
|
+
## Presenter view
|
|
64
|
+
|
|
65
|
+
Mount it on its own route and give the audience deck the same `sync` name:
|
|
66
|
+
|
|
67
|
+
```svelte
|
|
68
|
+
<Presenter sync="q3" title="Q3 review"><Slides /></Presenter>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
It shows the current slide, the next one, notes and a timer, in step with the audience window. Pass the same children or `slides` and `registry` as the audience deck.
|
|
72
|
+
|
|
73
|
+
## Print
|
|
74
|
+
|
|
75
|
+
Print gives one canvas per page, sized by an injected `@page` rule (960 by 540 px is 10 by 5.625 in), with every fragment shown and the chrome hidden. Hidden slides are skipped.
|
|
76
|
+
|
|
77
|
+
## Export
|
|
78
|
+
|
|
79
|
+
`@heyramzi/ui/slides/export` is Node only. It drives the Playwright browser and pages you pass in, and the `ffmpeg` on your path, so the package depends on neither.
|
|
80
|
+
|
|
81
|
+
- `launchChromium(chromium)`: headless Chromium, falling back to installed Chrome.
|
|
82
|
+
- `capturePage` and `captureEach`: PNGs of a page or each slide at a canvas size and device scale.
|
|
83
|
+
- `printUrl`, `printHtml` and `printPage`: a PDF at the page size, after fonts, images and an optional `ready` selector settle.
|
|
84
|
+
- `imagesToPdf`: one page per PNG, for colour that has to match the PNGs exactly.
|
|
85
|
+
- `renderLoop` and `encodeFrames`: an mp4 from captured frames.
|
|
86
|
+
|
|
87
|
+
## Accessibility
|
|
88
|
+
|
|
89
|
+
The deck is a labelled region, inactive slides are `inert`, the progress bar has `role="progressbar"`, and a live region announces "Slide 3 of 12".
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { getContext, setContext, type Snippet } from 'svelte';
|
|
3
|
+
import type { DeckController, SlideEntry } from './deck-controller.svelte.js';
|
|
4
|
+
import { DECK_KEY, SLIDE_KEY, type SlideAlign, type SlideContext, type SlideNotes } from './types.js';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
/** Accessible name, read after "Slide 3 of 12". */
|
|
8
|
+
label?: string;
|
|
9
|
+
/** Speaker notes for the presenter view. */
|
|
10
|
+
notes?: SlideNotes;
|
|
11
|
+
/** Skipped by paging and left out of the overview and print. */
|
|
12
|
+
hidden?: boolean;
|
|
13
|
+
/** `center` (default), `start`, or `bare` to lay the canvas out yourself. */
|
|
14
|
+
align?: SlideAlign;
|
|
15
|
+
class?: string;
|
|
16
|
+
style?: string;
|
|
17
|
+
children?: Snippet;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let { label, notes, hidden = false, align = 'center', class: className, style, children }: Props = $props();
|
|
21
|
+
|
|
22
|
+
const deck = getContext<DeckController | undefined>(DECK_KEY);
|
|
23
|
+
if (!deck) throw new Error('<Slide> must be rendered inside <Deck>.');
|
|
24
|
+
|
|
25
|
+
let el = $state<HTMLElement | null>(null);
|
|
26
|
+
let steps = $state(0);
|
|
27
|
+
/* Fragments per step, so one leaving doesn't drop a step another still asks for. */
|
|
28
|
+
const counts = new Map<number, number>();
|
|
29
|
+
|
|
30
|
+
const entry: SlideEntry = {
|
|
31
|
+
id: Symbol('slide'),
|
|
32
|
+
get steps() {
|
|
33
|
+
return steps;
|
|
34
|
+
},
|
|
35
|
+
get hidden() {
|
|
36
|
+
return hidden;
|
|
37
|
+
},
|
|
38
|
+
get label() {
|
|
39
|
+
return label;
|
|
40
|
+
},
|
|
41
|
+
get notes() {
|
|
42
|
+
return notes;
|
|
43
|
+
},
|
|
44
|
+
get el() {
|
|
45
|
+
return el;
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
$effect(() => deck.register(entry));
|
|
50
|
+
|
|
51
|
+
const index = $derived(deck.entries.indexOf(entry));
|
|
52
|
+
const active = $derived(index >= 0 && deck.position.index === index);
|
|
53
|
+
const box = $derived(deck.boxOf(index));
|
|
54
|
+
const order = $derived(Math.max(0, deck.visible.indexOf(index)));
|
|
55
|
+
|
|
56
|
+
/* A slide that is leaving keeps the step it left at, so its fragments don't vanish mid-fade. */
|
|
57
|
+
let held = $state(0);
|
|
58
|
+
$effect.pre(() => {
|
|
59
|
+
if (active) held = deck.position.step;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const recount = () => (steps = counts.size ? Math.max(...counts.keys()) : 0);
|
|
63
|
+
|
|
64
|
+
setContext<SlideContext>(SLIDE_KEY, {
|
|
65
|
+
shown: (step) => deck.revealAll || step <= (active ? deck.position.step : held),
|
|
66
|
+
join(step) {
|
|
67
|
+
const order = counts.get(step) ?? 0;
|
|
68
|
+
counts.set(step, order + 1);
|
|
69
|
+
recount();
|
|
70
|
+
return {
|
|
71
|
+
order,
|
|
72
|
+
leave() {
|
|
73
|
+
const left = (counts.get(step) ?? 1) - 1;
|
|
74
|
+
if (left <= 0) counts.delete(step);
|
|
75
|
+
else counts.set(step, left);
|
|
76
|
+
recount();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
/* A View Transition needs one element per name, so only the active slide holds its morph names. */
|
|
83
|
+
$effect(() => {
|
|
84
|
+
const names = el?.querySelectorAll<HTMLElement>('[data-morph]') ?? [];
|
|
85
|
+
for (const node of names) node.style.viewTransitionName = active ? (node.dataset.morph ?? '') : '';
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const where = $derived(deck.labels.slide(order + 1, deck.visible.length));
|
|
89
|
+
const name = $derived(label ? `${where}: ${label}` : where);
|
|
90
|
+
</script>
|
|
91
|
+
|
|
92
|
+
<section
|
|
93
|
+
bind:this={el}
|
|
94
|
+
class={['slide', className]}
|
|
95
|
+
style:--slide-w="{deck.canvas.w}px"
|
|
96
|
+
style:--slide-h="{deck.canvas.h}px"
|
|
97
|
+
style:--slide-scale={box?.scale}
|
|
98
|
+
style:--i={order}
|
|
99
|
+
style:transform={box ? `translate3d(${box.x}px, ${box.y}px, 0) scale(${box.scale})` : undefined}
|
|
100
|
+
{style}
|
|
101
|
+
{hidden}
|
|
102
|
+
data-active={active}
|
|
103
|
+
role="group"
|
|
104
|
+
aria-roledescription={deck.labels.slideRole}
|
|
105
|
+
aria-label={name}
|
|
106
|
+
aria-hidden={!active}
|
|
107
|
+
inert={!active || deck.overview}
|
|
108
|
+
>
|
|
109
|
+
<div class="slide__surface" data-align={align}>
|
|
110
|
+
{@render children?.()}
|
|
111
|
+
</div>
|
|
112
|
+
</section>
|
|
113
|
+
|
|
114
|
+
<style>
|
|
115
|
+
/* The box is a full canvas, moved and scaled from its top-left. */
|
|
116
|
+
.slide {
|
|
117
|
+
position: absolute;
|
|
118
|
+
top: 0;
|
|
119
|
+
left: 0;
|
|
120
|
+
z-index: 1;
|
|
121
|
+
width: var(--slide-w);
|
|
122
|
+
height: var(--slide-h);
|
|
123
|
+
transform-origin: 0 0;
|
|
124
|
+
visibility: hidden;
|
|
125
|
+
/* The old slide stays drawn for the length of the enter animation. */
|
|
126
|
+
transition: visibility 0s linear var(--deck-t-slide);
|
|
127
|
+
}
|
|
128
|
+
.slide[hidden] {
|
|
129
|
+
display: none;
|
|
130
|
+
}
|
|
131
|
+
.slide[data-active='true'] {
|
|
132
|
+
z-index: 2;
|
|
133
|
+
visibility: visible;
|
|
134
|
+
transition-delay: 0s;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* The overview shows every slide. They all leave the stack at once and settle in turn, later ones slower. */
|
|
138
|
+
:global(.deck[data-view='overview']) .slide {
|
|
139
|
+
visibility: visible;
|
|
140
|
+
}
|
|
141
|
+
:global(.deck[data-morphing]) .slide {
|
|
142
|
+
transition:
|
|
143
|
+
visibility 0s linear calc(var(--deck-t-morph) + 10 * var(--deck-stagger)),
|
|
144
|
+
transform calc(var(--deck-t-morph) + min(var(--i), 10) * var(--deck-stagger)) var(--deck-ease);
|
|
145
|
+
}
|
|
146
|
+
/* Opening shows the slides at once, so only closing waits before hiding them. */
|
|
147
|
+
:global(.deck[data-morphing][data-view='overview']) .slide {
|
|
148
|
+
transition-delay: 0s;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.slide__surface {
|
|
152
|
+
position: relative;
|
|
153
|
+
box-sizing: border-box;
|
|
154
|
+
width: 100%;
|
|
155
|
+
height: 100%;
|
|
156
|
+
overflow: hidden;
|
|
157
|
+
color: var(--deck-fg);
|
|
158
|
+
background: var(--deck-bg);
|
|
159
|
+
font-family: var(--deck-font);
|
|
160
|
+
animation: var(--deck-t-slide) var(--deck-ease) backwards;
|
|
161
|
+
}
|
|
162
|
+
:global(.deck[data-view='overview']) .slide__surface {
|
|
163
|
+
/* Design px, so divide by the scale to get the same corner on every thumbnail. */
|
|
164
|
+
border-radius: calc(10px / var(--slide-scale, 1));
|
|
165
|
+
box-shadow: 0 0 0 calc(1px / var(--slide-scale, 1)) var(--deck-line);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.slide__surface[data-align='center'],
|
|
169
|
+
.slide__surface[data-align='start'] {
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
justify-content: center;
|
|
173
|
+
padding: var(--deck-pad);
|
|
174
|
+
}
|
|
175
|
+
.slide__surface[data-align='center'] {
|
|
176
|
+
align-items: center;
|
|
177
|
+
text-align: center;
|
|
178
|
+
}
|
|
179
|
+
</style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Slide from './Slide.svelte';
|
|
3
|
+
import type { SlideInstance, SlideRegistry } from './model.js';
|
|
4
|
+
|
|
5
|
+
interface Props { slides: SlideInstance[]; registry: SlideRegistry; unknownLabel: (type: string) => string };
|
|
6
|
+
|
|
7
|
+
let { slides, registry, unknownLabel }: Props = $props();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
{#each slides as slide (slide.id)}
|
|
11
|
+
{@const definition = registry[slide.type]}
|
|
12
|
+
<Slide label={definition?.label} notes={slide.notes} hidden={slide.hidden} align="bare">
|
|
13
|
+
{#if definition}
|
|
14
|
+
<definition.layout content={slide.content} {slide} />
|
|
15
|
+
{:else}
|
|
16
|
+
<p class="deck-body" style:padding="var(--deck-pad)">{unknownLabel(slide.type)}</p>
|
|
17
|
+
{/if}
|
|
18
|
+
</Slide>
|
|
19
|
+
{/each}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
declare module '*.css';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* The ways a deck talks to the outside: its URL, a parent frame, another window. Each returns a cleanup. */
|
|
2
|
+
|
|
3
|
+
import type { DeckController } from './deck-controller.svelte.js';
|
|
4
|
+
import { parseSlideParam } from './navigation.js';
|
|
5
|
+
import type { BridgeOptions, UrlSync } from './types.js';
|
|
6
|
+
|
|
7
|
+
/** Reads `?s=` once, then keeps it at the current slide. The write is a replace, so Back skips it. */
|
|
8
|
+
export function attachUrlSync(deck: DeckController, { param = 's', write }: UrlSync = {}): () => void {
|
|
9
|
+
const initial = parseSlideParam(new URL(location.href).searchParams.get(param), deck.entries.length);
|
|
10
|
+
if (initial !== null) deck.go(initial, 0, 'remote');
|
|
11
|
+
return deck.listen(({ index }) => {
|
|
12
|
+
const url = new URL(location.href);
|
|
13
|
+
url.searchParams.set(param, String(index + 1));
|
|
14
|
+
if (write) write(url);
|
|
15
|
+
else history.replaceState(history.state, '', url);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Lets a host page drive a deck in an iframe. In: `{type:'deck:nav', delta}` and
|
|
21
|
+
* `{type:'deck:go', index, step?}`. Out to the parent: `{type:'deck:change', index, step, total}`.
|
|
22
|
+
*/
|
|
23
|
+
export function attachBridge(deck: DeckController, { origin = location.origin }: BridgeOptions = {}): () => void {
|
|
24
|
+
const onMessage = (event: MessageEvent) => {
|
|
25
|
+
if (origin !== '*' && event.origin !== origin) return;
|
|
26
|
+
const data = event.data;
|
|
27
|
+
if (data?.type === 'deck:nav') {
|
|
28
|
+
if (data.delta === -1) deck.prev();
|
|
29
|
+
else if (data.delta === 1) deck.next();
|
|
30
|
+
} else if (data?.type === 'deck:go' && Number.isInteger(data.index)) {
|
|
31
|
+
deck.go(data.index, Number.isInteger(data.step) ? data.step : 0, 'remote');
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
window.addEventListener('message', onMessage);
|
|
35
|
+
const stop = deck.listen(({ index, step }, source) => {
|
|
36
|
+
if (source === 'local' && window.parent !== window) {
|
|
37
|
+
window.parent.postMessage({ type: 'deck:change', index, step, total: deck.visible.length }, origin);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
return () => {
|
|
41
|
+
window.removeEventListener('message', onMessage);
|
|
42
|
+
stop();
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type ChannelMessage = { type: 'state'; from: string; index: number; step: number } | { type: 'hello'; from: string };
|
|
47
|
+
|
|
48
|
+
/** Mirrors the deck across windows of one origin: the audience screen and a presenter view. */
|
|
49
|
+
export function attachChannel(deck: DeckController, name: string): () => void {
|
|
50
|
+
if (typeof BroadcastChannel === 'undefined') return () => {};
|
|
51
|
+
const channel = new BroadcastChannel(`heyramzi-slides:${name}`);
|
|
52
|
+
const self = crypto.randomUUID();
|
|
53
|
+
const send = (message: ChannelMessage) => channel.postMessage(message);
|
|
54
|
+
const state = ({ index, step } = deck.position): ChannelMessage => ({ type: 'state', from: self, index, step });
|
|
55
|
+
|
|
56
|
+
channel.onmessage = ({ data }: MessageEvent<ChannelMessage>) => {
|
|
57
|
+
if (data.from === self) return;
|
|
58
|
+
if (data.type === 'hello') send(state());
|
|
59
|
+
else deck.go(data.index, data.step, 'remote');
|
|
60
|
+
};
|
|
61
|
+
const stop = deck.listen((position, source) => {
|
|
62
|
+
if (source === 'local') send(state(position));
|
|
63
|
+
});
|
|
64
|
+
send({ type: 'hello', from: self });
|
|
65
|
+
return () => {
|
|
66
|
+
stop();
|
|
67
|
+
channel.close();
|
|
68
|
+
};
|
|
69
|
+
}
|