paraframe 0.1.0 → 0.2.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/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  ![paraframe — Click through your Svelte prototypes.](https://raw.githubusercontent.com/Convex-Works/paraframe/main/docs/assets/paraframe-banner.png)
2
2
 
3
3
  <p align="center">
4
+ <a href="https://paraframe.convex.works">Showcase</a> ·
4
5
  <a href="docs/SETUP.md">Setup</a> ·
5
6
  <a href="docs/COMMENTS.md">Comments</a> ·
6
7
  <a href="docs/DEVELOPMENT.md">Development</a>
@@ -11,6 +12,8 @@ prototype preview. You write each frame as a Svelte component, and paraframe
11
12
  lays them out on a canvas and plays them one at a time. Frames are real
12
13
  components, so anything inside them stays interactive.
13
14
 
15
+ Try the showcase at <https://paraframe.convex.works>.
16
+
14
17
  - **Canvas.** Every frame in sections on a board you can pan and zoom.
15
18
  - **Player.** One frame at a time, scaled to the window; named flows give you starting points.
16
19
  - **Real components in iframes.** Each frame renders at its own size, with its own styles, media queries and fixed elements, isolated from paraframe's chrome.
@@ -19,11 +22,20 @@ components, so anything inside them stays interactive.
19
22
  - **Comments.** Optional pinned threads on elements inside frames, through [visual-comments](https://github.com/Convex-Works/visual-comments).
20
23
  - **Theming.** `--pf-*` custom properties for the chrome.
21
24
 
22
- Requires SvelteKit 3, Svelte 5 and Tailwind 4 (the chrome is built with Tailwind).
25
+ Requires SvelteKit 3 and Svelte 5. The chrome ships as plain CSS, so your app
26
+ needs no Tailwind.
23
27
 
24
28
  ## Quickstart
25
29
 
26
- Add `paraframe` to your SvelteKit app, then define the prototype once:
30
+ In a SvelteKit app (a new one from `npx sv create --template minimal --types ts`
31
+ works), install paraframe and delete the template's root
32
+ `src/routes/+page.svelte`: the canvas takes its place at `/`.
33
+
34
+ ```sh
35
+ npm i -D paraframe
36
+ ```
37
+
38
+ Define the prototype once:
27
39
 
28
40
  ```ts
29
41
  // src/lib/prototype.ts
@@ -49,20 +61,105 @@ export const prototype = definePrototype({
49
61
  });
50
62
  ```
51
63
 
52
- Then add three routes. The canvas and player share a layout that loads
64
+ Each frame is an ordinary Svelte component:
65
+
66
+ ```svelte
67
+ <!-- src/lib/frames/Welcome.svelte -->
68
+ <main>
69
+ <h1>Welcome</h1>
70
+ <p>Your first frame.</p>
71
+ </main>
72
+ ```
73
+
74
+ Then add the routes. The canvas and player share a layout that loads
53
75
  paraframe's styles; the frame route loads your product's styles instead.
54
76
 
55
77
  ```
56
78
  src/routes/
57
- +layout.ts export const prerender = true;
58
- (viewer)/+layout.svelte import 'paraframe/styles.css'
59
- (viewer)/+page.svelte <Canvas {prototype} />
60
- (viewer)/play/[frame]/+page.svelte <Player {prototype} frameId={page.params.frame ?? ''} />
61
- frame/[frame]/+page.svelte import your app's CSS; <FrameHost {prototype} resolved={data.resolved} />
79
+ +layout.ts
80
+ (viewer)/+layout.svelte
81
+ (viewer)/+page.svelte
82
+ (viewer)/play/[frame]/+page.ts
83
+ (viewer)/play/[frame]/+page.svelte
84
+ frame/[frame]/+page.ts
85
+ frame/[frame]/+page.svelte
86
+ ```
87
+
88
+ ```ts
89
+ // src/routes/+layout.ts
90
+ export const prerender = true;
91
+ ```
92
+
93
+ ```svelte
94
+ <!-- src/routes/(viewer)/+layout.svelte -->
95
+ <script lang="ts">
96
+ import 'paraframe/styles.css';
97
+ import type { LayoutProps } from './$types';
98
+
99
+ let { children }: LayoutProps = $props();
100
+ </script>
101
+
102
+ {@render children()}
103
+ ```
104
+
105
+ ```svelte
106
+ <!-- src/routes/(viewer)/+page.svelte -->
107
+ <script lang="ts">
108
+ import { Canvas } from 'paraframe';
109
+ import { prototype } from '#lib/prototype.js';
110
+ </script>
111
+
112
+ <Canvas {prototype} />
113
+ ```
114
+
115
+ ```ts
116
+ // src/routes/(viewer)/play/[frame]/+page.ts
117
+ import { frameEntries } from 'paraframe';
118
+ import { prototype } from '#lib/prototype.js';
119
+
120
+ export const entries = () => frameEntries(prototype);
121
+ ```
122
+
123
+ ```svelte
124
+ <!-- src/routes/(viewer)/play/[frame]/+page.svelte -->
125
+ <script lang="ts">
126
+ import { page } from '$app/state';
127
+ import { Player } from 'paraframe';
128
+ import { prototype } from '#lib/prototype.js';
129
+ </script>
130
+
131
+ <Player {prototype} frameId={page.params.frame ?? ''} />
132
+ ```
133
+
134
+ ```ts
135
+ // src/routes/frame/[frame]/+page.ts
136
+ import { frameEntries, resolveFrame } from 'paraframe';
137
+ import { prototype } from '#lib/prototype.js';
138
+ import type { PageLoad } from './$types';
139
+
140
+ export const entries = () => frameEntries(prototype);
141
+
142
+ export const load: PageLoad = async ({ params }) => ({
143
+ resolved: await resolveFrame(prototype, params.frame)
144
+ });
145
+ ```
146
+
147
+ ```svelte
148
+ <!-- src/routes/frame/[frame]/+page.svelte -->
149
+ <script lang="ts">
150
+ // Import your product's own CSS here, never paraframe/styles.css.
151
+ import { FrameHost } from 'paraframe';
152
+ import { prototype } from '#lib/prototype.js';
153
+ import type { PageProps } from './$types';
154
+
155
+ let { data }: PageProps = $props();
156
+ </script>
157
+
158
+ <FrameHost {prototype} resolved={data.resolved} />
62
159
  ```
63
160
 
64
- The play and frame routes also need `+page.ts` files; [Setup](https://github.com/Convex-Works/paraframe/blob/main/docs/SETUP.md)
65
- has the full routes, frame sizes and props, static previews and theming.
161
+ [Setup](https://github.com/Convex-Works/paraframe/blob/main/docs/SETUP.md) covers
162
+ frame sizes and props, static previews and theming.
66
163
 
67
164
  To see a complete working prototype, clone this repository and run
68
165
  `pnpm install && pnpm showcase`.
@@ -95,7 +192,7 @@ clicks inside the frame (a selectable table row, say), add `data-pf-interactive`
95
192
  | View | Keys |
96
193
  | --- | --- |
97
194
  | Player | ← → previous and next frame, R restart the flow (while one is playing), C comment mode, Esc close the open comment, then leave comment mode |
98
- | Canvas | Scroll or drag to pan, ⌘/Ctrl + scroll to zoom, ⇧1 fit all, ⇧0 100%, F or ⇧2 center and fit the selection, ← → select previous and next frame, Enter play, C comment mode, Esc close the open comment, then leave comment mode |
195
+ | Canvas | Scroll or drag to pan, ⌘/Ctrl + scroll to zoom, ⇧1 fit all, ⇧0 100%, F or ⇧2 center and fit the selection, ← → select previous and next frame, Enter play (when no button or link has focus), C comment mode, Esc close the open comment, then leave comment mode |
99
196
 
100
197
  On the canvas, hover live frames to inspect tooltips and hover states, and
101
198
  double-click a frame to play it.
@@ -15,7 +15,7 @@
15
15
  import Menu from './internal/Menu.svelte';
16
16
  import MenuItem from './internal/MenuItem.svelte';
17
17
  import { readStored, storageKey, writeStored } from './internal/storage.js';
18
- import { isEditable } from './messages.js';
18
+ import { activatesOnEnter, isEditable } from './messages.js';
19
19
  import { describeFrame, useCommentsHolder, type CommentThreadPin } from './internal/comments.js';
20
20
  import type { Flow, Frame, Prototype, Section } from './types.js';
21
21
 
@@ -39,6 +39,8 @@
39
39
  const LOAD_IDLE_MS = 50;
40
40
  // Below this on-screen width, frame titles are hidden.
41
41
  const MIN_LABEL_WIDTH = 72;
42
+ // Below this on-screen width, a frame's path is hidden and its title keeps the room.
43
+ const PATH_LABEL_WIDTH = 360;
42
44
  // The comments panel floats over the right edge; fitting keeps frames clear of it.
43
45
  const PANEL_INSET = 320 + 12 * 2;
44
46
  const TOOLBAR_HEIGHT = 56;
@@ -369,6 +371,8 @@
369
371
  function onkeydown(event: KeyboardEvent) {
370
372
  if (isEditable(event.target) || document.querySelector(':popover-open')) return;
371
373
  if (event.metaKey || event.ctrlKey || event.altKey) return;
374
+ // A focused button or link handles Enter itself; Enter elsewhere plays the selection.
375
+ if (event.code === 'Enter' && activatesOnEnter(event.target)) return;
372
376
  const actions: Record<string, () => void> = {
373
377
  'shift+Digit1': () => fit(layout.bounds),
374
378
  'shift+Digit0': () => zoomTo(1),
@@ -486,6 +490,8 @@
486
490
  {entry.label}
487
491
  </MenuItem>
488
492
  {/each}
493
+ {/snippet}
494
+ {#snippet footer()}
489
495
  <p class="max-w-60 border-t border-pf-line px-2 pt-2 pb-1 text-xs leading-relaxed text-pf-muted">
490
496
  Scroll or drag to pan. Hold ⌘ or Ctrl and scroll to zoom. Double-click a frame to play it.
491
497
  </p>
@@ -633,6 +639,8 @@
633
639
  ondblclick={() => play(frame.id)}
634
640
  >
635
641
  {frame.title}
642
+ <!-- The path only once the title has room beside it. -->
643
+ {#if frame.path && s.width >= PATH_LABEL_WIDTH}<span class="pl-1 text-pf-canvas-muted">{frame.path}</span>{/if}
636
644
  </button>
637
645
  {#if frame.id === selected || frame.id === hovered}
638
646
  <button
@@ -181,6 +181,7 @@
181
181
  <span class="hidden text-pf-island-muted sm:inline" aria-hidden="true">/</span>
182
182
  {/if}
183
183
  <span class="truncate font-medium">{frame?.title ?? frameId}</span>
184
+ {#if frame?.path}<span class="hidden truncate text-pf-island-muted md:inline">{frame.path}</span>{/if}
184
185
  </p>
185
186
  </div>
186
187
 
@@ -268,18 +269,20 @@
268
269
  {/snippet}
269
270
  {#snippet children(close)}
270
271
  {#each prototype.sections as section (section.id)}
271
- <p class="px-2 pt-2 pb-1 text-xs text-pf-muted">{section.title}</p>
272
- {#each section.frames as item (item.id)}
273
- <MenuItem
274
- checked={item.id === frameId}
275
- onselect={() => {
276
- close();
277
- show(item.id);
278
- }}
279
- >
280
- {item.title}
281
- </MenuItem>
282
- {/each}
272
+ <div role="group" aria-label={section.title}>
273
+ <p class="px-2 pt-2 pb-1 text-xs text-pf-muted" aria-hidden="true">{section.title}</p>
274
+ {#each section.frames as item (item.id)}
275
+ <MenuItem
276
+ checked={item.id === frameId}
277
+ onselect={() => {
278
+ close();
279
+ show(item.id);
280
+ }}
281
+ >
282
+ {item.title}
283
+ </MenuItem>
284
+ {/each}
285
+ </div>
283
286
  {/each}
284
287
  {/snippet}
285
288
  </Menu>
@@ -1,4 +1,9 @@
1
- <!-- A button that opens a popover menu below it. Items close the menu with `close()`. -->
1
+ <!--
2
+ A button that opens a popover menu below it. Items close the menu with `close()`.
3
+ Follows the ARIA menu button pattern: opening moves focus to the checked item
4
+ (or the first), Up/Down/Home/End move between items, and Escape, Tab or
5
+ choosing an item closes the menu and returns focus to the trigger.
6
+ -->
2
7
  <script lang="ts">
3
8
  import type { Snippet } from 'svelte';
4
9
 
@@ -6,14 +11,17 @@
6
11
  label,
7
12
  trigger,
8
13
  children,
14
+ footer,
9
15
  align = 'start',
10
16
  tone = 'light',
11
17
  class: className = ''
12
18
  }: {
13
- /** Accessible name for the trigger. */
19
+ /** Accessible name for the trigger and the menu. */
14
20
  label: string;
15
21
  trigger: Snippet;
16
22
  children: Snippet<[close: () => void]>;
23
+ /** Non-interactive content shown below the items, outside the menu role. */
24
+ footer?: Snippet;
17
25
  align?: 'start' | 'end';
18
26
  tone?: 'light' | 'dark';
19
27
  class?: string;
@@ -22,16 +30,67 @@
22
30
  const id = $props.id();
23
31
  let button = $state<HTMLButtonElement>();
24
32
  let popover = $state<HTMLDivElement>();
33
+ let menu = $state<HTMLDivElement>();
25
34
  let open = $state(false);
35
+ // Which item gets focus when the menu opens: the checked one (or first) unless a key asked otherwise.
36
+ let initial: 'first' | 'last' | undefined;
26
37
 
27
- function place(event: ToggleEvent) {
38
+ const items = () => [...(menu?.querySelectorAll<HTMLElement>('[role^=menuitem]') ?? [])];
39
+
40
+ function beforetoggle(event: ToggleEvent) {
41
+ // Closing with focus inside the menu (Escape, Tab, an item was chosen): hand focus back to the trigger.
42
+ if (event.newState === 'closed' && popover?.contains(document.activeElement)) button?.focus();
43
+ }
44
+
45
+ function ontoggle(event: ToggleEvent) {
28
46
  open = event.newState === 'open';
29
- if (event.newState !== 'open' || !button || !popover) return;
47
+ if (!open || !button || !popover) return;
30
48
  const rect = button.getBoundingClientRect();
31
49
  const gutter = 12;
32
50
  const left = align === 'start' ? rect.left : rect.right - popover.offsetWidth;
33
51
  popover.style.left = `${Math.max(gutter, Math.min(left, window.innerWidth - popover.offsetWidth - gutter))}px`;
34
52
  popover.style.top = `${Math.max(gutter, Math.min(rect.bottom + 6, window.innerHeight - popover.offsetHeight - gutter))}px`;
53
+
54
+ focusItem(initial);
55
+ initial = undefined;
56
+ }
57
+
58
+ function focusItem(which?: 'first' | 'last') {
59
+ const all = items();
60
+ const checked = which ? undefined : all.find((item) => item.getAttribute('aria-checked') === 'true');
61
+ (checked ?? (which === 'last' ? all.at(-1) : all[0]))?.focus();
62
+ }
63
+
64
+ function ontriggerkeydown(event: KeyboardEvent) {
65
+ if (event.key !== 'ArrowDown' && event.key !== 'ArrowUp') return;
66
+ event.preventDefault();
67
+ const which = event.key === 'ArrowUp' ? 'last' : 'first';
68
+ if (open) return focusItem(which);
69
+ initial = which;
70
+ popover?.showPopover();
71
+ }
72
+
73
+ function onmenukeydown(event: KeyboardEvent) {
74
+ const all = items();
75
+ const index = all.indexOf(document.activeElement as HTMLElement);
76
+ let next: HTMLElement | undefined;
77
+ if (event.key === 'ArrowDown') next = all[(index + 1) % all.length];
78
+ else if (event.key === 'ArrowUp') next = all.at(index - 1);
79
+ else if (event.key === 'Home') next = all[0];
80
+ else if (event.key === 'End') next = all.at(-1);
81
+ else if (event.key === 'Escape') {
82
+ // Only the menu closes; the canvas and comments don't see this Escape.
83
+ event.preventDefault();
84
+ event.stopPropagation();
85
+ close();
86
+ return;
87
+ } else if (event.key === 'Tab') {
88
+ // Close and let Tab move on from the trigger.
89
+ close();
90
+ return;
91
+ } else return;
92
+ event.preventDefault();
93
+ next?.focus();
35
94
  }
36
95
 
37
96
  const close = () => popover?.hidePopover();
@@ -40,12 +99,15 @@
40
99
  <button
41
100
  bind:this={button}
42
101
  type="button"
102
+ id="pf-menu-button-{id}"
43
103
  popovertarget="pf-menu-{id}"
44
104
  aria-label={label}
105
+ aria-haspopup="menu"
45
106
  aria-expanded={open}
46
- aria-controls="pf-menu-{id}"
107
+ aria-controls="pf-menu-items-{id}"
47
108
  title={label}
48
109
  class={className}
110
+ onkeydown={ontriggerkeydown}
49
111
  >
50
112
  {@render trigger()}
51
113
  </button>
@@ -53,7 +115,8 @@
53
115
  bind:this={popover}
54
116
  id="pf-menu-{id}"
55
117
  popover="auto"
56
- ontoggle={place}
118
+ onbeforetoggle={beforetoggle}
119
+ {ontoggle}
57
120
  class={[
58
121
  'pf-menu fixed inset-auto m-0 max-h-[min(70vh,560px)] w-max min-w-52 max-w-[calc(100vw-24px)] overflow-y-auto rounded border p-1 text-[13px] shadow-lg',
59
122
  tone === 'dark'
@@ -61,5 +124,15 @@
61
124
  : 'border-pf-line bg-pf-surface text-pf-ink'
62
125
  ]}
63
126
  >
64
- {@render children(close)}
127
+ <div
128
+ bind:this={menu}
129
+ id="pf-menu-items-{id}"
130
+ role="menu"
131
+ tabindex="-1"
132
+ aria-labelledby="pf-menu-button-{id}"
133
+ onkeydown={onmenukeydown}
134
+ >
135
+ {@render children(close)}
136
+ </div>
137
+ {@render footer?.()}
65
138
  </div>
@@ -1,9 +1,11 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
- /** Accessible name for the trigger. */
3
+ /** Accessible name for the trigger and the menu. */
4
4
  label: string;
5
5
  trigger: Snippet;
6
6
  children: Snippet<[close: () => void]>;
7
+ /** Non-interactive content shown below the items, outside the menu role. */
8
+ footer?: Snippet;
7
9
  align?: 'start' | 'end';
8
10
  tone?: 'light' | 'dark';
9
11
  class?: string;
@@ -10,7 +10,7 @@
10
10
  tone = 'light'
11
11
  }: {
12
12
  onselect: () => void;
13
- /** Shows a check mark column when not undefined. */
13
+ /** Makes this a radio item (`menuitemradio` with `aria-checked`) with a check mark column when not undefined. */
14
14
  checked?: boolean;
15
15
  children: Snippet;
16
16
  hint?: string;
@@ -20,7 +20,9 @@
20
20
 
21
21
  <button
22
22
  type="button"
23
- role="menuitem"
23
+ role={checked === undefined ? 'menuitem' : 'menuitemradio'}
24
+ aria-checked={checked}
25
+ tabindex="-1"
24
26
  onclick={onselect}
25
27
  class={[
26
28
  'flex w-full items-center gap-2 rounded-[2px] px-2 py-1.5 text-left',
@@ -29,7 +31,7 @@
29
31
  ]}
30
32
  >
31
33
  {#if checked !== undefined}
32
- <span class="w-4 shrink-0 text-pf-accent">
34
+ <span class="w-4 shrink-0 text-pf-accent" aria-hidden="true">
33
35
  {#if checked}<Icon name="check" size={14} />{/if}
34
36
  </span>
35
37
  {/if}
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  type $$ComponentProps = {
3
3
  onselect: () => void;
4
- /** Shows a check mark column when not undefined. */
4
+ /** Makes this a radio item (`menuitemradio` with `aria-checked`) with a check mark column when not undefined. */
5
5
  checked?: boolean;
6
6
  children: Snippet;
7
7
  hint?: string;
@@ -22,3 +22,5 @@ export declare function postToHost(message: FrameMessage): void;
22
22
  */
23
23
  export declare const PLAYER_KEYS: Set<string>;
24
24
  export declare function isEditable(target: EventTarget | null): boolean;
25
+ /** Whether Enter on this target belongs to a focused control rather than to a global shortcut. */
26
+ export declare function activatesOnEnter(target: EventTarget | null): boolean;
package/dist/messages.js CHANGED
@@ -18,3 +18,10 @@ export function isEditable(target) {
18
18
  return (target instanceof HTMLElement &&
19
19
  (target.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName)));
20
20
  }
21
+ // Controls that act on Enter themselves.
22
+ const ACTIVATES_ON_ENTER = 'a[href],button,summary,[role=button],[role=link],[role=tab],[role=checkbox],[role=switch],' +
23
+ '[role=radio],[role=option],[role=menuitem],[role=menuitemradio],[role=menuitemcheckbox]';
24
+ /** Whether Enter on this target belongs to a focused control rather than to a global shortcut. */
25
+ export function activatesOnEnter(target) {
26
+ return target instanceof Element && target.closest(ACTIVATES_ON_ENTER) !== null;
27
+ }
package/dist/prototype.js CHANGED
@@ -25,6 +25,7 @@ export function definePrototype(definition) {
25
25
  const frame = {
26
26
  id: f.id,
27
27
  title: f.title,
28
+ path: f.path,
28
29
  load: f.load,
29
30
  props: f.props ?? {},
30
31
  size: { ...size, ...f.size },
package/dist/styles.css CHANGED
@@ -1,133 +1,2 @@
1
- /*
2
- paraframe's chrome. Import it once in the layout of the canvas and player
3
- routes, not in the frame route: frames carry the host app's own styles.
4
-
5
- Override the --pf-* custom properties to theme the chrome.
6
- */
7
- @import 'tailwindcss' source(none);
8
- @source './';
9
-
10
- :root {
11
- --pf-font: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif;
12
- --pf-ink: #292524;
13
- --pf-muted: #78716c;
14
- --pf-line: #d6d3d1;
15
- --pf-wash: #e7e5e4;
16
- --pf-surface: #f5f5f4;
17
- --pf-canvas: #1c1917;
18
- --pf-canvas-ink: #e7e5e4;
19
- --pf-canvas-muted: #a8a29e;
20
- --pf-selection: #f5f5f4;
21
- --pf-section: #292524;
22
- --pf-grid: rgb(168 162 158 / 0.2);
23
- --pf-stage: #1c1917;
24
- --pf-accent: #57534e;
25
- --pf-avatar: #009aaa;
26
- --pf-comment: #0d99ff;
27
- --pf-comment-soft: #e5f4ff;
28
- /* Comments (visual-comments) take paraframe's colours and font. */
29
- --vc-comment: var(--pf-comment);
30
- --vc-comment-soft: var(--pf-comment-soft);
31
- --vc-avatar: var(--pf-avatar);
32
- --vc-font: var(--pf-font);
33
- --pf-island: rgb(50 50 50 / 0.57);
34
- --pf-island-ink: #fff;
35
- --pf-island-muted: rgb(255 255 255 / 0.6);
36
- --pf-island-hover: rgb(255 255 255 / 0.12);
37
- }
38
-
39
- @theme inline {
40
- --font-pf: var(--pf-font);
41
- --color-pf-ink: var(--pf-ink);
42
- --color-pf-muted: var(--pf-muted);
43
- --color-pf-line: var(--pf-line);
44
- --color-pf-wash: var(--pf-wash);
45
- --color-pf-surface: var(--pf-surface);
46
- --color-pf-canvas: var(--pf-canvas);
47
- --color-pf-canvas-ink: var(--pf-canvas-ink);
48
- --color-pf-canvas-muted: var(--pf-canvas-muted);
49
- --color-pf-selection: var(--pf-selection);
50
- --color-pf-section: var(--pf-section);
51
- --color-pf-stage: var(--pf-stage);
52
- --color-pf-accent: var(--pf-accent);
53
- --color-pf-avatar: var(--pf-avatar);
54
- --color-pf-comment: var(--pf-comment);
55
- --color-pf-comment-soft: var(--pf-comment-soft);
56
- --color-pf-island: var(--pf-island);
57
- --color-pf-island-ink: var(--pf-island-ink);
58
- --color-pf-island-muted: var(--pf-island-muted);
59
- }
60
-
61
- @layer components {
62
- .pf-toolbar {
63
- display: flex;
64
- flex-wrap: wrap;
65
- align-items: flex-start;
66
- justify-content: space-between;
67
- gap: 0.5rem;
68
- pointer-events: none;
69
- }
70
- /* Floating chrome: a frosted pill of pill-shaped tools. */
71
- .pf-island {
72
- display: flex;
73
- min-width: 0;
74
- max-width: 100%;
75
- align-items: center;
76
- gap: 0.125rem;
77
- padding: 0.25rem;
78
- border-radius: 9999px;
79
- background: var(--pf-island);
80
- backdrop-filter: blur(5px);
81
- -webkit-backdrop-filter: blur(5px);
82
- color: var(--pf-island-ink);
83
- pointer-events: auto;
84
- }
85
- .pf-tool {
86
- display: inline-flex;
87
- height: 2rem;
88
- min-width: 2rem;
89
- align-items: center;
90
- justify-content: center;
91
- border-radius: 9999px;
92
- color: var(--pf-island-ink);
93
- outline: none;
94
- }
95
- .pf-tool:hover,
96
- .pf-tool[aria-expanded='true'] {
97
- background: var(--pf-island-hover);
98
- }
99
- .pf-tool:disabled {
100
- opacity: 0.35;
101
- pointer-events: none;
102
- }
103
- .pf-tool[aria-pressed='true'],
104
- .pf-tool-primary {
105
- background: var(--pf-island-ink);
106
- color: var(--pf-ink);
107
- }
108
- .pf-tool-primary {
109
- gap: 0.375rem;
110
- padding-inline: 0.875rem 1rem;
111
- font-weight: 500;
112
- }
113
- .pf-tool-primary:hover { background: rgb(255 255 255 / 0.85); }
114
- .pf-tool:focus-visible {
115
- box-shadow: 0 0 0 2px var(--pf-island-muted);
116
- }
117
-
118
- .pf-tool-light {
119
- display: inline-flex;
120
- height: 1.75rem;
121
- min-width: 1.75rem;
122
- align-items: center;
123
- justify-content: center;
124
- border-radius: 0.125rem;
125
- outline: none;
126
- }
127
- .pf-tool-light:hover {
128
- background: var(--pf-wash);
129
- }
130
- .pf-tool-light:focus-visible {
131
- box-shadow: 0 0 0 2px var(--pf-accent);
132
- }
133
- }
1
+ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}@layer theme{:root,:host{--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--font-weight-medium:500;--font-weight-semibold:600;--leading-relaxed:1.625;--radius-md:.375rem;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components{.pf-toolbar{pointer-events:none;flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:.5rem;display:flex}.pf-island{background:var(--pf-island);-webkit-backdrop-filter:blur(5px);backdrop-filter:blur(5px);min-width:0;max-width:100%;color:var(--pf-island-ink);pointer-events:auto;border-radius:9999px;align-items:center;gap:.125rem;padding:.25rem;display:flex}.pf-tool{min-width:2rem;height:2rem;color:var(--pf-island-ink);border-radius:9999px;outline:none;justify-content:center;align-items:center;display:inline-flex}.pf-tool:hover,.pf-tool[aria-expanded=true]{background:var(--pf-island-hover)}.pf-tool:disabled{opacity:.35;pointer-events:none}.pf-tool[aria-pressed=true],.pf-tool-primary{background:var(--pf-island-ink);color:var(--pf-ink)}.pf-tool-primary{gap:.375rem;padding-inline:.875rem 1rem;font-weight:500}.pf-tool-primary:hover{background:#ffffffd9}.pf-tool:focus-visible{box-shadow:0 0 0 2px var(--pf-island-muted)}.pf-tool-light{border-radius:.125rem;outline:none;justify-content:center;align-items:center;min-width:1.75rem;height:1.75rem;display:inline-flex}.pf-tool-light:hover{background:var(--pf-wash)}.pf-tool-light:focus-visible{box-shadow:0 0 0 2px var(--pf-accent)}}@layer utilities{.pointer-events-none{pointer-events:none}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.inset-0{inset:0}.inset-auto{inset:auto}.inset-x-3{inset-inline:calc(var(--spacing) * 3)}.top-0{top:0}.top-1{top:var(--spacing)}.top-3{top:calc(var(--spacing) * 3)}.top-\[60px\]{top:60px}.right-1{right:var(--spacing)}.right-3{right:calc(var(--spacing) * 3)}.bottom-3{bottom:calc(var(--spacing) * 3)}.left-0{left:0}.z-10{z-index:10}.z-20{z-index:20}.m-0{margin:0}.m-3{margin:calc(var(--spacing) * 3)}.m-auto{margin:auto}.my-3{margin-block:calc(var(--spacing) * 3)}.mr-3{margin-right:calc(var(--spacing) * 3)}.mb-0{margin-bottom:0}.ml-auto{margin-left:auto}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline-flex{display:inline-flex}.table{display:table}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-full{width:100%;height:100%}.h-5{height:calc(var(--spacing) * 5)}.h-dvh{height:100dvh}.max-h-\[min\(70vh\,560px\)\]{max-height:min(70vh,560px)}.min-h-0{min-height:0}.min-h-10{min-height:calc(var(--spacing) * 10)}.w-4{width:calc(var(--spacing) * 4)}.w-20{width:calc(var(--spacing) * 20)}.w-80{width:calc(var(--spacing) * 80)}.w-full{width:100%}.w-max{width:max-content}.max-w-48{max-width:calc(var(--spacing) * 48)}.max-w-60{max-width:calc(var(--spacing) * 60)}.max-w-\[calc\(100vw-24px\)\]{max-width:calc(100vw - 24px)}.max-w-full{max-width:100%}.min-w-0{min-width:0}.min-w-16{min-width:calc(var(--spacing) * 16)}.min-w-52{min-width:calc(var(--spacing) * 52)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.origin-top-left{transform-origin:0 0}.cursor-grabbing{cursor:grabbing}.touch-none{touch-action:none}.resize{resize:both}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-end{justify-content:flex-end}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-\[2px\]{border-radius:2px}.rounded-\[16px\]{border-radius:16px}.rounded-full{border-radius:3.40282e38px}.rounded-md{border-radius:var(--radius-md)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-pf-line{border-color:var(--pf-line)}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.bg-pf-accent{background-color:var(--pf-accent)}.bg-pf-canvas{background-color:var(--pf-canvas)}.bg-pf-comment{background-color:var(--pf-comment)}.bg-pf-ink{background-color:var(--pf-ink)}.bg-pf-section{background-color:var(--pf-section)}.bg-pf-stage{background-color:var(--pf-stage)}.bg-pf-surface{background-color:var(--pf-surface)}.bg-white{background-color:var(--color-white)}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:var(--spacing)}.px-1{padding-inline:var(--spacing)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-5{padding-right:calc(var(--spacing) * 5)}.pb-1{padding-bottom:var(--spacing)}.pl-1{padding-left:var(--spacing)}.pl-1\.5{padding-left:calc(var(--spacing) * 1.5)}.pl-3\.5{padding-left:calc(var(--spacing) * 3.5)}.text-center{text-align:center}.text-left{text-align:left}.font-pf{font-family:var(--pf-font)}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.text-\[13px\]{font-size:13px}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.text-pf-accent{color:var(--pf-accent)}.text-pf-canvas-ink{color:var(--pf-canvas-ink)}.text-pf-canvas-muted{color:var(--pf-canvas-muted)}.text-pf-ink{color:var(--pf-ink)}.text-pf-island-muted{color:var(--pf-island-muted)}.text-pf-muted{color:var(--pf-muted)}.text-pf-selection{color:var(--pf-selection)}.text-pf-surface{color:var(--pf-surface)}.text-white\/90{color:#ffffffe6}@supports (color:color-mix(in lab, red, red)){.text-white\/90{color:color-mix(in oklab, var(--color-white) 90%, transparent)}}.lowercase{text-transform:lowercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.underline{text-decoration-line:underline}.opacity-50{opacity:.5}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-pf-selection,.ring-pf-selection\/60{--tw-ring-color:var(--pf-selection)}@supports (color:color-mix(in lab, red, red)){.ring-pf-selection\/60{--tw-ring-color:color-mix(in oklab, var(--pf-selection) 60%, transparent)}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.hover\:bg-pf-selection\/10:hover{background-color:var(--pf-selection)}@supports (color:color-mix(in lab, red, red)){.hover\:bg-pf-selection\/10:hover{background-color:color-mix(in oklab, var(--pf-selection) 10%, transparent)}}.hover\:bg-pf-wash:hover{background-color:var(--pf-wash)}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.hover\:text-pf-canvas-ink:hover{color:var(--pf-canvas-ink)}.hover\:brightness-110:hover{--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}}.focus-visible\:bg-pf-wash:focus-visible{background-color:var(--pf-wash)}.focus-visible\:bg-white\/10:focus-visible{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.focus-visible\:bg-white\/10:focus-visible{background-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.data-\[pf-hover\=true\]\:pointer-events-auto[data-pf-hover=true]{pointer-events:auto}@media (min-width:40rem){.sm\:inline{display:inline}}@media (min-width:48rem){.md\:inline{display:inline}}}:root{--pf-font:ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;--pf-ink:#292524;--pf-muted:#78716c;--pf-line:#d6d3d1;--pf-wash:#e7e5e4;--pf-surface:#f5f5f4;--pf-canvas:#1c1917;--pf-canvas-ink:#e7e5e4;--pf-canvas-muted:#a8a29e;--pf-selection:#f5f5f4;--pf-section:#292524;--pf-grid:#a8a29e33;--pf-stage:#1c1917;--pf-accent:#57534e;--pf-avatar:#009aaa;--pf-comment:#0d99ff;--pf-comment-soft:#e5f4ff;--vc-comment:var(--pf-comment);--vc-comment-soft:var(--pf-comment-soft);--vc-avatar:var(--pf-avatar);--vc-font:var(--pf-font);--pf-island:#32323291;--pf-island-ink:#fff;--pf-island-muted:#fff9;--pf-island-hover:#ffffff1f}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}
package/dist/types.d.ts CHANGED
@@ -9,6 +9,8 @@ export interface FrameDefinition {
9
9
  /** URL-safe identifier: lowercase letters, digits and single dashes. */
10
10
  id: string;
11
11
  title: string;
12
+ /** Where the screen lives in the product, e.g. `/{account}/projects`. Shown beside the title. */
13
+ path?: string;
12
14
  /** Imports the frame's component, e.g. `() => import('./Overview.svelte')`. */
13
15
  load: () => Promise<{
14
16
  default: FrameComponent;
@@ -57,6 +59,7 @@ export interface PrototypeDefinition {
57
59
  export interface Frame {
58
60
  id: string;
59
61
  title: string;
62
+ path: string | undefined;
60
63
  load: FrameDefinition['load'];
61
64
  props: Record<string, unknown>;
62
65
  size: FrameSize;
package/package.json CHANGED
@@ -1,93 +1,98 @@
1
1
  {
2
- "name": "paraframe",
3
- "version": "0.1.0",
4
- "description": "Click through Svelte prototypes like a Figma file: real Svelte components as frames, on a zoomable canvas and in a flow player.",
5
- "keywords": [
6
- "svelte",
7
- "sveltekit",
8
- "svelte5",
9
- "prototype",
10
- "prototyping",
11
- "prototype-viewer",
12
- "canvas",
13
- "infinite-canvas",
14
- "design-tools",
15
- "figma",
16
- "frames"
17
- ],
18
- "homepage": "https://paraframe.convex.works",
19
- "bugs": "https://github.com/Convex-Works/paraframe/issues",
20
- "repository": {
21
- "type": "git",
22
- "url": "git+https://github.com/Convex-Works/paraframe.git"
23
- },
24
- "license": "MIT",
25
- "author": "Convex Works",
26
- "type": "module",
27
- "engines": {
28
- "node": ">=22.17"
29
- },
30
- "files": [
31
- "dist",
32
- "!dist/**/*.test.*",
33
- "!dist/**/*.spec.*"
34
- ],
35
- "sideEffects": [
36
- "**/*.css"
37
- ],
38
- "svelte": "./dist/index.js",
39
- "types": "./dist/index.d.ts",
40
- "exports": {
41
- ".": {
42
- "types": "./dist/index.d.ts",
43
- "svelte": "./dist/index.js"
44
- },
45
- "./comments": {
46
- "types": "./dist/comments.d.ts",
47
- "svelte": "./dist/comments.js"
48
- },
49
- "./styles.css": "./dist/styles.css"
50
- },
51
- "peerDependencies": {
52
- "@sveltejs/kit": "^3.0.0",
53
- "svelte": "^5.57.1",
54
- "tailwindcss": "^4.3.0",
55
- "visual-comments": "^0.1.0"
56
- },
57
- "peerDependenciesMeta": {
58
- "visual-comments": {
59
- "optional": true
60
- }
61
- },
62
- "devDependencies": {
63
- "@convex-works/sign-in": "link:../sign-in",
64
- "@sveltejs/adapter-static": "^4.0.0",
65
- "@sveltejs/kit": "^3.0.0",
66
- "@sveltejs/package": "^3.0.0",
67
- "@sveltejs/vite-plugin-svelte": "^7.2.0",
68
- "@tailwindcss/vite": "^4.3.0",
69
- "@types/node": "^26.6.5",
70
- "@typescript/native": "npm:typescript@^7.0.2",
71
- "publint": "^0.3.21",
72
- "svelte": "^5.57.1",
73
- "svelte-check": "^4.7.6",
74
- "tailwindcss": "^4.3.0",
75
- "typescript": "~6.0.3",
76
- "visual-comments": "^0.1.0",
77
- "vite": "^8.3.0",
78
- "vitest": "^5.0.5"
79
- },
80
- "imports": {
81
- "#lib": "./src/lib/index.js",
82
- "#lib/*": "./src/lib/*"
83
- },
84
- "scripts": {
85
- "dev": "svelte-kit sync && svelte-package --watch",
86
- "package": "svelte-kit sync && svelte-package && publint",
87
- "showcase": "vite dev",
88
- "build": "vite build && pnpm package",
89
- "preview": "vite preview",
90
- "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --tsgo",
91
- "test": "vitest run"
92
- }
93
- }
2
+ "name": "paraframe",
3
+ "version": "0.2.0",
4
+ "description": "Click through Svelte prototypes like a Figma file: real Svelte components as frames, on a zoomable canvas and in a flow player.",
5
+ "keywords": [
6
+ "svelte",
7
+ "sveltekit",
8
+ "svelte5",
9
+ "prototype",
10
+ "prototyping",
11
+ "prototype-viewer",
12
+ "canvas",
13
+ "infinite-canvas",
14
+ "design-tools",
15
+ "figma",
16
+ "frames"
17
+ ],
18
+ "homepage": "https://paraframe.convex.works",
19
+ "bugs": "https://github.com/Convex-Works/paraframe/issues",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/Convex-Works/paraframe.git"
23
+ },
24
+ "license": "MIT",
25
+ "author": "Convex Works",
26
+ "type": "module",
27
+ "packageManager": "pnpm@12.4.1",
28
+ "engines": {
29
+ "node": ">=22.17"
30
+ },
31
+ "scripts": {
32
+ "dev": "svelte-kit sync && svelte-package && pnpm css && (svelte-package --watch --preserve-output & pnpm css --watch)",
33
+ "package": "svelte-kit sync && svelte-package && pnpm css && publint",
34
+ "css": "tailwindcss -i src/styles.css -o dist/styles.css --minify",
35
+ "showcase": "vite dev",
36
+ "build": "vite build && pnpm package",
37
+ "preview": "vite preview",
38
+ "prepare": "svelte-kit sync || echo ''",
39
+ "prepack": "pnpm package",
40
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --tsgo",
41
+ "test": "vitest run"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "!dist/**/*.test.*",
46
+ "!dist/**/*.spec.*"
47
+ ],
48
+ "sideEffects": [
49
+ "**/*.css"
50
+ ],
51
+ "svelte": "./dist/index.js",
52
+ "types": "./dist/index.d.ts",
53
+ "exports": {
54
+ ".": {
55
+ "types": "./dist/index.d.ts",
56
+ "svelte": "./dist/index.js"
57
+ },
58
+ "./comments": {
59
+ "types": "./dist/comments.d.ts",
60
+ "svelte": "./dist/comments.js"
61
+ },
62
+ "./styles.css": "./dist/styles.css"
63
+ },
64
+ "peerDependencies": {
65
+ "@sveltejs/kit": "^3.0.0",
66
+ "svelte": "^5.57.1",
67
+ "visual-comments": "^0.1.0"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "visual-comments": {
71
+ "optional": true
72
+ }
73
+ },
74
+ "devDependencies": {
75
+ "@convex-works/sign-in": "link:../sign-in",
76
+ "@sveltejs/adapter-static": "^4.0.0",
77
+ "@sveltejs/kit": "^3.0.0",
78
+ "@sveltejs/package": "^3.0.0",
79
+ "@sveltejs/vite-plugin-svelte": "^7.2.0",
80
+ "@tailwindcss/cli": "^4.3.3",
81
+ "@tailwindcss/vite": "^4.3.0",
82
+ "@types/node": "^26.6.5",
83
+ "@typescript/native": "npm:typescript@^7.0.2",
84
+ "happy-dom": "^21.0.3",
85
+ "publint": "^0.3.21",
86
+ "svelte": "^5.57.1",
87
+ "svelte-check": "^4.7.6",
88
+ "tailwindcss": "^4.3.0",
89
+ "typescript": "~6.0.3",
90
+ "visual-comments": "^0.1.0",
91
+ "vite": "^8.3.0",
92
+ "vitest": "^5.0.5"
93
+ },
94
+ "imports": {
95
+ "#lib": "./src/lib/index.js",
96
+ "#lib/*": "./src/lib/*"
97
+ }
98
+ }