@classic-homes/theme-svelte 1.18.0 → 1.19.1

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.
Files changed (35) hide show
  1. package/dist/__tests__/studio.test.js +240 -0
  2. package/dist/lib/components/Sheet.svelte +155 -0
  3. package/dist/lib/components/Sheet.svelte.d.ts +28 -0
  4. package/dist/lib/components/SortableList.svelte +251 -0
  5. package/dist/lib/components/SortableList.svelte.d.ts +64 -0
  6. package/dist/lib/components/studio/InsertPalette.svelte +192 -0
  7. package/dist/lib/components/studio/InsertPalette.svelte.d.ts +39 -0
  8. package/dist/lib/components/studio/IssuesButton.svelte +141 -0
  9. package/dist/lib/components/studio/IssuesButton.svelte.d.ts +33 -0
  10. package/dist/lib/components/studio/SaveStatus.svelte +110 -0
  11. package/dist/lib/components/studio/SaveStatus.svelte.d.ts +22 -0
  12. package/dist/lib/components/studio/Studio.svelte +189 -0
  13. package/dist/lib/components/studio/Studio.svelte.d.ts +49 -0
  14. package/dist/lib/components/studio/StudioBar.svelte +168 -0
  15. package/dist/lib/components/studio/StudioBar.svelte.d.ts +46 -0
  16. package/dist/lib/components/studio/StudioCanvas.svelte +87 -0
  17. package/dist/lib/components/studio/StudioCanvas.svelte.d.ts +30 -0
  18. package/dist/lib/components/studio/StudioCanvasItem.svelte +129 -0
  19. package/dist/lib/components/studio/StudioCanvasItem.svelte.d.ts +29 -0
  20. package/dist/lib/components/studio/StudioInspector.svelte +133 -0
  21. package/dist/lib/components/studio/StudioInspector.svelte.d.ts +37 -0
  22. package/dist/lib/components/studio/StudioInspectorSection.svelte +79 -0
  23. package/dist/lib/components/studio/StudioInspectorSection.svelte.d.ts +24 -0
  24. package/dist/lib/components/studio/StudioOutline.svelte +303 -0
  25. package/dist/lib/components/studio/StudioOutline.svelte.d.ts +55 -0
  26. package/dist/lib/components/studio/StudioPage.svelte +36 -0
  27. package/dist/lib/components/studio/StudioPage.svelte.d.ts +18 -0
  28. package/dist/lib/components/studio/context.d.ts +25 -0
  29. package/dist/lib/components/studio/context.js +9 -0
  30. package/dist/lib/components/studio/index.d.ts +12 -0
  31. package/dist/lib/components/studio/index.js +12 -0
  32. package/dist/lib/index.d.ts +3 -0
  33. package/dist/lib/index.js +6 -0
  34. package/dist/lib/schemas/common.d.ts +2 -2
  35. package/package.json +1 -1
@@ -0,0 +1,129 @@
1
+ <script lang="ts">
2
+ import { tick, type Snippet } from 'svelte';
3
+ import { cn } from '../../utils.js';
4
+ import Icon from '../Icon.svelte';
5
+ import { getStudioContext } from './context.js';
6
+
7
+ /**
8
+ * StudioCanvasItem - one selectable part of the canvas (a question, a block).
9
+ *
10
+ * Clicking anywhere in it, or tabbing into it, selects it; the content stays
11
+ * live, so a previewed question can still be answered. The selected item
12
+ * gets a ring and its `toolbar` (duplicate, delete...). With `onInsert`, a
13
+ * "+" between this item and the next adds a new one right there. On phones
14
+ * picking an item opens the inspector sheet.
15
+ */
16
+ interface Props {
17
+ children: Snippet;
18
+ selected?: boolean;
19
+ onSelect?: () => void;
20
+ /** Names the item for screen readers and its Edit button, e.g. "Question 2" */
21
+ label: string;
22
+ /** Quick actions shown on the selected item */
23
+ toolbar?: Snippet;
24
+ /** Shows a "+" below the item that inserts after it */
25
+ onInsert?: () => void;
26
+ /** Accessible name of the insert button (default: "Add below {label}") */
27
+ insertLabel?: string;
28
+ /** Marks the item as having problems */
29
+ invalid?: boolean;
30
+ class?: string;
31
+ }
32
+
33
+ let {
34
+ children,
35
+ selected = false,
36
+ onSelect,
37
+ label,
38
+ toolbar,
39
+ onInsert,
40
+ insertLabel,
41
+ invalid = false,
42
+ class: className,
43
+ }: Props = $props();
44
+
45
+ const studio = getStudioContext();
46
+ let element: HTMLDivElement | undefined = $state();
47
+
48
+ // Quick actions (toolbar, "+") act on the item without selecting it. A
49
+ // dialog they open (the insert palette) returns focus to them on close;
50
+ // treating that focus as "select me" would undo the dialog's own selection.
51
+ const inQuickAction = (target: EventTarget | null) =>
52
+ !!(target as Element | null)?.closest?.('[data-studio-quick-action]');
53
+
54
+ function select(reveal: boolean) {
55
+ if (!selected) onSelect?.();
56
+ if (reveal) studio?.revealInspector();
57
+ }
58
+
59
+ $effect(() => {
60
+ if (!selected || !element) return;
61
+ const el = element;
62
+ void tick().then(() => {
63
+ // Don't yank the canvas while the user is working inside the item
64
+ if (el.contains(document.activeElement)) return;
65
+ const reduce = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
66
+ el.scrollIntoView?.({ block: 'nearest', behavior: reduce ? 'auto' : 'smooth' });
67
+ });
68
+ });
69
+ </script>
70
+
71
+ <div
72
+ bind:this={element}
73
+ role="group"
74
+ aria-label={label}
75
+ aria-current={selected ? 'true' : undefined}
76
+ data-selected={selected || undefined}
77
+ class={cn(
78
+ 'group/item relative scroll-my-6 rounded-lg transition-shadow',
79
+ // Problems use a dashed amber outline: in brands whose primary is red, a
80
+ // red ring would read as "selected"
81
+ selected ? 'ring-2 ring-primary' : 'hover:ring-1 hover:ring-border',
82
+ invalid && 'outline-dashed outline-2 -outline-offset-2 outline-warning/70',
83
+ className
84
+ )}
85
+ onclickcapture={(e) => select(!inQuickAction(e.target))}
86
+ onfocusin={(e) => {
87
+ if (!inQuickAction(e.target)) select(false);
88
+ }}
89
+ >
90
+ {#if !selected}
91
+ <!-- Keyboard way in for items with no focusable content of their own -->
92
+ <button
93
+ type="button"
94
+ class="sr-only left-2 top-2 z-10 rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground focus:not-sr-only focus:absolute"
95
+ onclick={() => select(true)}
96
+ >
97
+ Edit {label}
98
+ </button>
99
+ {/if}
100
+
101
+ {#if selected && toolbar}
102
+ <div
103
+ data-studio-quick-action
104
+ class="absolute -top-4 right-3 z-10 flex items-center gap-0.5 rounded-md border border-border bg-background p-0.5 shadow-md"
105
+ >
106
+ {@render toolbar()}
107
+ </div>
108
+ {/if}
109
+
110
+ {@render children()}
111
+
112
+ {#if onInsert}
113
+ <div class="relative h-0" data-studio-quick-action>
114
+ <button
115
+ type="button"
116
+ aria-label={insertLabel ?? `Add below ${label}`}
117
+ title={insertLabel ?? 'Add below'}
118
+ class="absolute left-1/2 top-0 z-10 flex h-7 w-7 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border border-border bg-background text-muted-foreground opacity-0 shadow-sm hover:border-primary hover:text-primary focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/item:opacity-100 [@media(hover:none)]:opacity-100"
119
+ class:opacity-100={selected}
120
+ onclick={(e) => {
121
+ e.stopPropagation();
122
+ onInsert?.();
123
+ }}
124
+ >
125
+ <Icon name="plus" size={14} aria-hidden="true" />
126
+ </button>
127
+ </div>
128
+ {/if}
129
+ </div>
@@ -0,0 +1,29 @@
1
+ import { type Snippet } from 'svelte';
2
+ /**
3
+ * StudioCanvasItem - one selectable part of the canvas (a question, a block).
4
+ *
5
+ * Clicking anywhere in it, or tabbing into it, selects it; the content stays
6
+ * live, so a previewed question can still be answered. The selected item
7
+ * gets a ring and its `toolbar` (duplicate, delete...). With `onInsert`, a
8
+ * "+" between this item and the next adds a new one right there. On phones
9
+ * picking an item opens the inspector sheet.
10
+ */
11
+ interface Props {
12
+ children: Snippet;
13
+ selected?: boolean;
14
+ onSelect?: () => void;
15
+ /** Names the item for screen readers and its Edit button, e.g. "Question 2" */
16
+ label: string;
17
+ /** Quick actions shown on the selected item */
18
+ toolbar?: Snippet;
19
+ /** Shows a "+" below the item that inserts after it */
20
+ onInsert?: () => void;
21
+ /** Accessible name of the insert button (default: "Add below {label}") */
22
+ insertLabel?: string;
23
+ /** Marks the item as having problems */
24
+ invalid?: boolean;
25
+ class?: string;
26
+ }
27
+ declare const StudioCanvasItem: import("svelte").Component<Props, {}, "">;
28
+ type StudioCanvasItem = ReturnType<typeof StudioCanvasItem>;
29
+ export default StudioCanvasItem;
@@ -0,0 +1,133 @@
1
+ <script lang="ts" module>
2
+ export interface StudioInspectorTab {
3
+ id: string;
4
+ label: string;
5
+ /** Shown after the label, e.g. the number of comments */
6
+ count?: number;
7
+ }
8
+ </script>
9
+
10
+ <script lang="ts">
11
+ import type { Snippet } from 'svelte';
12
+ import { cn } from '../../utils.js';
13
+
14
+ /**
15
+ * StudioInspector - the Studio's right column: settings for whatever is
16
+ * selected, and nothing else.
17
+ *
18
+ * A header names the selection ("Question 3", "Single choice") with room
19
+ * for an action such as Change type; `tabs` put related views side by side
20
+ * with editing (e.g. Slide · Comments) instead of replacing it. Group the
21
+ * fields with `StudioInspectorSection`, content first and rarely-used
22
+ * settings in a collapsed "Advanced" section.
23
+ */
24
+ interface Props {
25
+ /** What is selected, e.g. "Question 3" */
26
+ title?: string;
27
+ /** Its kind, e.g. "Single choice" */
28
+ description?: string;
29
+ /** Header action, e.g. a Change type menu */
30
+ headerActions?: Snippet;
31
+ tabs?: StudioInspectorTab[];
32
+ /** Active tab (bindable) */
33
+ tab?: string;
34
+ onTabChange?: (tab: string) => void;
35
+ /** The fields; with tabs, render the active tab's content */
36
+ children: Snippet;
37
+ /** Pinned under the fields */
38
+ footer?: Snippet;
39
+ class?: string;
40
+ }
41
+
42
+ let {
43
+ title,
44
+ description,
45
+ headerActions,
46
+ tabs,
47
+ tab = $bindable(tabs?.[0]?.id ?? ''),
48
+ onTabChange,
49
+ children,
50
+ footer,
51
+ class: className,
52
+ }: Props = $props();
53
+
54
+ const uid = $props.id();
55
+
56
+ function choose(id: string) {
57
+ if (id === tab) return;
58
+ tab = id;
59
+ onTabChange?.(id);
60
+ }
61
+
62
+ function tabKeydown(event: KeyboardEvent, index: number) {
63
+ if (!tabs) return;
64
+ let next = -1;
65
+ if (event.key === 'ArrowRight') next = (index + 1) % tabs.length;
66
+ else if (event.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length;
67
+ if (next < 0) return;
68
+ event.preventDefault();
69
+ choose(tabs[next].id);
70
+ document.getElementById(`${uid}-tab-${tabs[next].id}`)?.focus();
71
+ }
72
+ </script>
73
+
74
+ <div class={cn('flex h-full min-h-0 flex-col', className)}>
75
+ {#if title || headerActions}
76
+ <div class="flex shrink-0 items-start gap-2 border-b border-border px-4 py-3">
77
+ <div class="min-w-0 flex-1">
78
+ {#if title}<h2 class="truncate text-sm font-semibold">{title}</h2>{/if}
79
+ {#if description}
80
+ <p class="truncate text-xs text-muted-foreground">{description}</p>
81
+ {/if}
82
+ </div>
83
+ {#if headerActions}
84
+ <div class="flex shrink-0 items-center gap-1">{@render headerActions()}</div>
85
+ {/if}
86
+ </div>
87
+ {/if}
88
+
89
+ {#if tabs && tabs.length > 1}
90
+ <div
91
+ role="tablist"
92
+ aria-label={title ?? 'Inspector'}
93
+ class="flex shrink-0 gap-4 border-b border-border px-4"
94
+ >
95
+ {#each tabs as t, index (t.id)}
96
+ <button
97
+ type="button"
98
+ role="tab"
99
+ id="{uid}-tab-{t.id}"
100
+ aria-selected={t.id === tab}
101
+ aria-controls="{uid}-panel"
102
+ tabindex={t.id === tab ? 0 : -1}
103
+ class={cn(
104
+ '-mb-px border-b-2 py-2 text-sm font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
105
+ t.id === tab
106
+ ? 'border-primary text-foreground'
107
+ : 'border-transparent text-muted-foreground hover:text-foreground'
108
+ )}
109
+ onclick={() => choose(t.id)}
110
+ onkeydown={(e) => tabKeydown(e, index)}
111
+ >
112
+ {t.label}
113
+ {#if t.count}
114
+ <span class="ml-1 rounded-full bg-muted px-1.5 text-xs tabular-nums">{t.count}</span>
115
+ {/if}
116
+ </button>
117
+ {/each}
118
+ </div>
119
+ {/if}
120
+
121
+ <div
122
+ class="min-h-0 flex-1 overflow-y-auto"
123
+ id={tabs && tabs.length > 1 ? `${uid}-panel` : undefined}
124
+ role={tabs && tabs.length > 1 ? 'tabpanel' : undefined}
125
+ aria-labelledby={tabs && tabs.length > 1 ? `${uid}-tab-${tab}` : undefined}
126
+ >
127
+ {@render children()}
128
+ </div>
129
+
130
+ {#if footer}
131
+ <div class="shrink-0 border-t border-border px-4 py-3">{@render footer()}</div>
132
+ {/if}
133
+ </div>
@@ -0,0 +1,37 @@
1
+ export interface StudioInspectorTab {
2
+ id: string;
3
+ label: string;
4
+ /** Shown after the label, e.g. the number of comments */
5
+ count?: number;
6
+ }
7
+ import type { Snippet } from 'svelte';
8
+ /**
9
+ * StudioInspector - the Studio's right column: settings for whatever is
10
+ * selected, and nothing else.
11
+ *
12
+ * A header names the selection ("Question 3", "Single choice") with room
13
+ * for an action such as Change type; `tabs` put related views side by side
14
+ * with editing (e.g. Slide · Comments) instead of replacing it. Group the
15
+ * fields with `StudioInspectorSection`, content first and rarely-used
16
+ * settings in a collapsed "Advanced" section.
17
+ */
18
+ interface Props {
19
+ /** What is selected, e.g. "Question 3" */
20
+ title?: string;
21
+ /** Its kind, e.g. "Single choice" */
22
+ description?: string;
23
+ /** Header action, e.g. a Change type menu */
24
+ headerActions?: Snippet;
25
+ tabs?: StudioInspectorTab[];
26
+ /** Active tab (bindable) */
27
+ tab?: string;
28
+ onTabChange?: (tab: string) => void;
29
+ /** The fields; with tabs, render the active tab's content */
30
+ children: Snippet;
31
+ /** Pinned under the fields */
32
+ footer?: Snippet;
33
+ class?: string;
34
+ }
35
+ declare const StudioInspector: import("svelte").Component<Props, {}, "tab">;
36
+ type StudioInspector = ReturnType<typeof StudioInspector>;
37
+ export default StudioInspector;
@@ -0,0 +1,79 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '../../utils.js';
4
+ import Icon from '../Icon.svelte';
5
+
6
+ /**
7
+ * StudioInspectorSection - a titled group of fields in the inspector.
8
+ *
9
+ * Set `collapsible` for settings most people never touch ("Advanced"); they
10
+ * start closed unless `open` is set, and the heading says how many are set
11
+ * via `summary` so nothing hides silently.
12
+ */
13
+ interface Props {
14
+ title?: string;
15
+ /** Short hint under the title */
16
+ description?: string;
17
+ /** Collapse behind the title */
18
+ collapsible?: boolean;
19
+ /** Expanded (bindable); only used when collapsible */
20
+ open?: boolean;
21
+ /** Shown beside a collapsed title, e.g. "2 set" */
22
+ summary?: string;
23
+ children: Snippet;
24
+ class?: string;
25
+ }
26
+
27
+ let {
28
+ title,
29
+ description,
30
+ collapsible = false,
31
+ open = $bindable(false),
32
+ summary,
33
+ children,
34
+ class: className,
35
+ }: Props = $props();
36
+
37
+ const uid = $props.id();
38
+ const expanded = $derived(!collapsible || open);
39
+ </script>
40
+
41
+ <section
42
+ class={cn('border-b border-border px-4 py-4 last:border-b-0', className)}
43
+ aria-labelledby={title ? `${uid}-title` : undefined}
44
+ >
45
+ {#if title}
46
+ {#if collapsible}
47
+ <h3 id="{uid}-title" class="-my-1">
48
+ <button
49
+ type="button"
50
+ aria-expanded={open}
51
+ aria-controls="{uid}-body"
52
+ class="flex w-full items-center gap-2 rounded-md py-1 text-left text-sm font-semibold focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
53
+ onclick={() => (open = !open)}
54
+ >
55
+ <Icon
56
+ name="chevron-right"
57
+ size={16}
58
+ class={cn('shrink-0 text-muted-foreground transition-transform', open && 'rotate-90')}
59
+ aria-hidden="true"
60
+ />
61
+ <span class="flex-1">{title}</span>
62
+ {#if summary && !open}
63
+ <span class="text-xs font-normal text-muted-foreground">{summary}</span>
64
+ {/if}
65
+ </button>
66
+ </h3>
67
+ {:else}
68
+ <h3 id="{uid}-title" class="text-sm font-semibold">{title}</h3>
69
+ {/if}
70
+ {#if description && expanded}
71
+ <p class="mt-1 text-xs text-muted-foreground">{description}</p>
72
+ {/if}
73
+ {/if}
74
+ {#if expanded}
75
+ <div id="{uid}-body" class={cn('space-y-4', title && 'mt-3')}>
76
+ {@render children()}
77
+ </div>
78
+ {/if}
79
+ </section>
@@ -0,0 +1,24 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * StudioInspectorSection - a titled group of fields in the inspector.
4
+ *
5
+ * Set `collapsible` for settings most people never touch ("Advanced"); they
6
+ * start closed unless `open` is set, and the heading says how many are set
7
+ * via `summary` so nothing hides silently.
8
+ */
9
+ interface Props {
10
+ title?: string;
11
+ /** Short hint under the title */
12
+ description?: string;
13
+ /** Collapse behind the title */
14
+ collapsible?: boolean;
15
+ /** Expanded (bindable); only used when collapsible */
16
+ open?: boolean;
17
+ /** Shown beside a collapsed title, e.g. "2 set" */
18
+ summary?: string;
19
+ children: Snippet;
20
+ class?: string;
21
+ }
22
+ declare const StudioInspectorSection: import("svelte").Component<Props, {}, "open">;
23
+ type StudioInspectorSection = ReturnType<typeof StudioInspectorSection>;
24
+ export default StudioInspectorSection;