@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,189 @@
1
+ <script lang="ts">
2
+ import { onMount, type Snippet } from 'svelte';
3
+ import { cn } from '../../utils.js';
4
+ import Icon from '../Icon.svelte';
5
+ import Sheet from '../Sheet.svelte';
6
+ import { setStudioContext, type StudioLayout } from './context.js';
7
+
8
+ /**
9
+ * Studio - full-screen frame for editors (survey builders, slide decks,
10
+ * page and form builders).
11
+ *
12
+ * One fixed layout that never page-scrolls:
13
+ *
14
+ * ```
15
+ * ┌ bar ─────────────────────────────────────────┐
16
+ * │ banner (optional) │
17
+ * ├ rail ──┬ canvas ─────────────┬ inspector ────┤
18
+ * │ outline│ the real thing, │ settings for │
19
+ * │ │ click to select │ the selection │
20
+ * └────────┴─────────────────────┴───────────────┘
21
+ * ```
22
+ *
23
+ * Rail, canvas and inspector scroll on their own. Below 1280px the rail
24
+ * moves into a left sheet; below 768px the inspector moves into a bottom
25
+ * sheet as well, leaving the canvas full screen. Parts read the layout
26
+ * through `getStudioContext()`, so `StudioOutline` closes its sheet after a
27
+ * pick and `StudioCanvasItem` opens the inspector sheet on phones.
28
+ *
29
+ * Leave out `rail` and `inspector` for full-width modes such as a settings
30
+ * page (render a `StudioPage` as the canvas).
31
+ */
32
+ interface Props {
33
+ /** Top bar, normally a `StudioBar` */
34
+ bar: Snippet;
35
+ /** One message slot under the bar, for conflicts and decisions that block editing */
36
+ banner?: Snippet;
37
+ /** Left column, normally a `StudioOutline` */
38
+ rail?: Snippet;
39
+ /** Main area, normally a `StudioCanvas` or `StudioPage` */
40
+ canvas: Snippet;
41
+ /** Right column, normally a `StudioInspector` */
42
+ inspector?: Snippet;
43
+ /** Accessible name of the rail and the button that opens it (default: "Outline") */
44
+ railLabel?: string;
45
+ /** Accessible name of the inspector and the button that opens it (default: "Properties") */
46
+ inspectorLabel?: string;
47
+ /** Rail sheet open below 1280px (bindable) */
48
+ railOpen?: boolean;
49
+ /** Inspector sheet open below 768px (bindable) */
50
+ inspectorOpen?: boolean;
51
+ class?: string;
52
+ }
53
+
54
+ let {
55
+ bar,
56
+ banner,
57
+ rail,
58
+ canvas,
59
+ inspector,
60
+ railLabel = 'Outline',
61
+ inspectorLabel = 'Properties',
62
+ railOpen = $bindable(false),
63
+ inspectorOpen = $bindable(false),
64
+ class: className,
65
+ }: Props = $props();
66
+
67
+ // Starts wide so SSR and desktop's first paint match. The columns also carry
68
+ // the breakpoints as CSS, so server-rendered HTML is already right on phones.
69
+ let layout = $state<StudioLayout>('wide');
70
+
71
+ onMount(() => {
72
+ const wide = window.matchMedia('(min-width: 1280px)');
73
+ const medium = window.matchMedia('(min-width: 768px)');
74
+ const update = () => {
75
+ layout = wide.matches ? 'wide' : medium.matches ? 'medium' : 'compact';
76
+ if (layout === 'wide') railOpen = false;
77
+ if (layout !== 'compact') inspectorOpen = false;
78
+ };
79
+ update();
80
+ wide.addEventListener('change', update);
81
+ medium.addEventListener('change', update);
82
+ return () => {
83
+ wide.removeEventListener('change', update);
84
+ medium.removeEventListener('change', update);
85
+ };
86
+ });
87
+
88
+ const railInSheet = $derived(!!rail && layout !== 'wide');
89
+ const inspectorInSheet = $derived(!!inspector && layout === 'compact');
90
+
91
+ setStudioContext({
92
+ get layout() {
93
+ return layout;
94
+ },
95
+ get railInSheet() {
96
+ return railInSheet;
97
+ },
98
+ get inspectorInSheet() {
99
+ return inspectorInSheet;
100
+ },
101
+ openRail: () => {
102
+ if (railInSheet) railOpen = true;
103
+ },
104
+ closeRail: () => (railOpen = false),
105
+ revealInspector: () => {
106
+ railOpen = false;
107
+ if (inspectorInSheet) inspectorOpen = true;
108
+ },
109
+ closeInspector: () => (inspectorOpen = false),
110
+ });
111
+
112
+ const floatingButton =
113
+ 'pointer-events-auto inline-flex h-9 items-center gap-2 rounded-full border border-border bg-background px-3 text-sm font-medium shadow-md hover:bg-muted-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
114
+ </script>
115
+
116
+ <div class={cn('flex h-dvh flex-col overflow-hidden bg-background text-foreground', className)}>
117
+ {@render bar()}
118
+
119
+ {#if banner}
120
+ <div class="shrink-0">{@render banner()}</div>
121
+ {/if}
122
+
123
+ <div class="relative flex min-h-0 flex-1">
124
+ {#if rail && !railInSheet}
125
+ <aside
126
+ aria-label={railLabel}
127
+ class="hidden w-64 shrink-0 flex-col overflow-hidden border-r border-border bg-background xl:flex"
128
+ >
129
+ {@render rail()}
130
+ </aside>
131
+ {/if}
132
+
133
+ <div class="relative min-w-0 flex-1 overflow-hidden bg-muted/40">
134
+ {@render canvas()}
135
+
136
+ {#if railInSheet || inspectorInSheet}
137
+ <div
138
+ class="pointer-events-none absolute inset-x-3 bottom-3 z-10 flex items-end justify-between gap-2"
139
+ >
140
+ {#if railInSheet}
141
+ <button
142
+ type="button"
143
+ class={floatingButton}
144
+ aria-expanded={railOpen}
145
+ onclick={() => (railOpen = true)}
146
+ >
147
+ <Icon name="list" size={16} aria-hidden="true" />
148
+ {railLabel}
149
+ </button>
150
+ {:else}
151
+ <span></span>
152
+ {/if}
153
+ {#if inspectorInSheet}
154
+ <button
155
+ type="button"
156
+ class={floatingButton}
157
+ aria-expanded={inspectorOpen}
158
+ onclick={() => (inspectorOpen = true)}
159
+ >
160
+ <Icon name="settings" size={16} aria-hidden="true" />
161
+ {inspectorLabel}
162
+ </button>
163
+ {/if}
164
+ </div>
165
+ {/if}
166
+ </div>
167
+
168
+ {#if inspector && !inspectorInSheet}
169
+ <aside
170
+ aria-label={inspectorLabel}
171
+ class="hidden w-80 shrink-0 flex-col overflow-hidden border-l border-border bg-background md:flex 2xl:w-96"
172
+ >
173
+ {@render inspector()}
174
+ </aside>
175
+ {/if}
176
+ </div>
177
+ </div>
178
+
179
+ {#if rail && railInSheet}
180
+ <Sheet bind:open={railOpen} side="left" size="sm" title={railLabel}>
181
+ <div class="flex h-full flex-col">{@render rail()}</div>
182
+ </Sheet>
183
+ {/if}
184
+
185
+ {#if inspector && inspectorInSheet}
186
+ <Sheet bind:open={inspectorOpen} side="bottom" title={inspectorLabel} hideTitle>
187
+ {@render inspector()}
188
+ </Sheet>
189
+ {/if}
@@ -0,0 +1,49 @@
1
+ import { type Snippet } from 'svelte';
2
+ /**
3
+ * Studio - full-screen frame for editors (survey builders, slide decks,
4
+ * page and form builders).
5
+ *
6
+ * One fixed layout that never page-scrolls:
7
+ *
8
+ * ```
9
+ * ┌ bar ─────────────────────────────────────────┐
10
+ * │ banner (optional) │
11
+ * ├ rail ──┬ canvas ─────────────┬ inspector ────┤
12
+ * │ outline│ the real thing, │ settings for │
13
+ * │ │ click to select │ the selection │
14
+ * └────────┴─────────────────────┴───────────────┘
15
+ * ```
16
+ *
17
+ * Rail, canvas and inspector scroll on their own. Below 1280px the rail
18
+ * moves into a left sheet; below 768px the inspector moves into a bottom
19
+ * sheet as well, leaving the canvas full screen. Parts read the layout
20
+ * through `getStudioContext()`, so `StudioOutline` closes its sheet after a
21
+ * pick and `StudioCanvasItem` opens the inspector sheet on phones.
22
+ *
23
+ * Leave out `rail` and `inspector` for full-width modes such as a settings
24
+ * page (render a `StudioPage` as the canvas).
25
+ */
26
+ interface Props {
27
+ /** Top bar, normally a `StudioBar` */
28
+ bar: Snippet;
29
+ /** One message slot under the bar, for conflicts and decisions that block editing */
30
+ banner?: Snippet;
31
+ /** Left column, normally a `StudioOutline` */
32
+ rail?: Snippet;
33
+ /** Main area, normally a `StudioCanvas` or `StudioPage` */
34
+ canvas: Snippet;
35
+ /** Right column, normally a `StudioInspector` */
36
+ inspector?: Snippet;
37
+ /** Accessible name of the rail and the button that opens it (default: "Outline") */
38
+ railLabel?: string;
39
+ /** Accessible name of the inspector and the button that opens it (default: "Properties") */
40
+ inspectorLabel?: string;
41
+ /** Rail sheet open below 1280px (bindable) */
42
+ railOpen?: boolean;
43
+ /** Inspector sheet open below 768px (bindable) */
44
+ inspectorOpen?: boolean;
45
+ class?: string;
46
+ }
47
+ declare const Studio: import("svelte").Component<Props, {}, "railOpen" | "inspectorOpen">;
48
+ type Studio = ReturnType<typeof Studio>;
49
+ export default Studio;
@@ -0,0 +1,168 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '../../utils.js';
4
+ import Icon from '../Icon.svelte';
5
+ import DropdownMenu, {
6
+ type DropdownMenuItem,
7
+ type DropdownMenuGroup,
8
+ } from '../DropdownMenu.svelte';
9
+ import SegmentedControl, { type SegmentedControlOption } from '../SegmentedControl.svelte';
10
+
11
+ /**
12
+ * StudioBar - the one bar across the top of a Studio.
13
+ *
14
+ * Left: back link and the document title, edited in place. Middle: the
15
+ * modes that separate building from configuring (e.g. Build · Settings ·
16
+ * Share). Right: save status, issues, the primary action, and one overflow
17
+ * menu for everything else (export, history, delete...). On phones the
18
+ * modes wrap onto their own row.
19
+ */
20
+ interface Props {
21
+ /** Title of the document being edited (bindable) */
22
+ title?: string;
23
+ /** Called when an edited title is committed (blur or Enter) */
24
+ onTitleChange?: (title: string) => void;
25
+ /** Show the title as text instead of an input */
26
+ titleReadonly?: boolean;
27
+ titlePlaceholder?: string;
28
+ /** Small line under the title, e.g. "Survey · 5 questions" */
29
+ subtitle?: string;
30
+ /** Back link, e.g. to the list of surveys */
31
+ backHref?: string;
32
+ /** Accessible name of the back link (default: "Back") */
33
+ backLabel?: string;
34
+ /** Mode tabs; omit for single-mode editors */
35
+ modes?: SegmentedControlOption[];
36
+ /** Active mode (bindable) */
37
+ mode?: string;
38
+ onModeChange?: (mode: string) => void;
39
+ /** Save state, normally a `SaveStatus` */
40
+ status?: Snippet;
41
+ /** Issues, normally an `IssuesButton` */
42
+ issues?: Snippet;
43
+ /** Primary action(s), e.g. a Publish or Present button */
44
+ actions?: Snippet;
45
+ /** Overflow menu items (export, history, delete...) */
46
+ menuItems?: (DropdownMenuItem | DropdownMenuGroup)[];
47
+ /** Accessible name of the overflow menu (default: "More actions") */
48
+ menuLabel?: string;
49
+ class?: string;
50
+ }
51
+
52
+ let {
53
+ title = $bindable(''),
54
+ onTitleChange,
55
+ titleReadonly = false,
56
+ titlePlaceholder = 'Untitled',
57
+ subtitle,
58
+ backHref,
59
+ backLabel = 'Back',
60
+ modes,
61
+ mode = $bindable(modes?.[0]?.value ?? ''),
62
+ onModeChange,
63
+ status,
64
+ issues,
65
+ actions,
66
+ menuItems,
67
+ menuLabel = 'More actions',
68
+ class: className,
69
+ }: Props = $props();
70
+
71
+ // Title value when editing began, so Escape can put it back
72
+ let titleAtFocus = '';
73
+
74
+ function commitTitle(input: HTMLInputElement) {
75
+ const next = input.value.trim();
76
+ if (!next) {
77
+ title = titleAtFocus;
78
+ return;
79
+ }
80
+ if (next !== titleAtFocus) onTitleChange?.(next);
81
+ titleAtFocus = next;
82
+ }
83
+
84
+ function titleKeydown(event: KeyboardEvent) {
85
+ const input = event.currentTarget as HTMLInputElement;
86
+ if (event.key === 'Enter') {
87
+ event.preventDefault();
88
+ input.blur();
89
+ } else if (event.key === 'Escape') {
90
+ event.preventDefault();
91
+ title = titleAtFocus;
92
+ input.value = titleAtFocus;
93
+ input.blur();
94
+ }
95
+ }
96
+ </script>
97
+
98
+ <header
99
+ class={cn(
100
+ 'flex shrink-0 flex-wrap items-center gap-x-2 gap-y-2 border-b border-border bg-background px-2 py-2 md:h-14 md:flex-nowrap md:gap-x-3 md:px-3 md:py-0',
101
+ className
102
+ )}
103
+ >
104
+ <div class="flex min-w-0 flex-1 items-center gap-1 md:basis-0">
105
+ {#if backHref}
106
+ <a
107
+ href={backHref}
108
+ class="flex h-9 w-9 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted-hover hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
109
+ aria-label={backLabel}
110
+ title={backLabel}
111
+ >
112
+ <Icon name="arrow-left" size={18} aria-hidden="true" />
113
+ </a>
114
+ {/if}
115
+ <div class="min-w-0 flex-1">
116
+ <h1 class={titleReadonly ? 'truncate px-1.5 text-base font-semibold' : 'sr-only'}>
117
+ {title || titlePlaceholder}
118
+ </h1>
119
+ {#if !titleReadonly}
120
+ <input
121
+ bind:value={title}
122
+ aria-label="Title"
123
+ placeholder={titlePlaceholder}
124
+ class="block w-full max-w-md truncate rounded-md border border-transparent bg-transparent px-1.5 py-0.5 text-base font-semibold leading-tight outline-none hover:border-border focus:border-input focus:bg-background focus:ring-2 focus:ring-ring"
125
+ onfocus={(e) => (titleAtFocus = (e.currentTarget as HTMLInputElement).value)}
126
+ onblur={(e) => commitTitle(e.currentTarget as HTMLInputElement)}
127
+ onkeydown={titleKeydown}
128
+ />
129
+ {/if}
130
+ {#if subtitle}
131
+ <p class="truncate px-1.5 text-xs text-muted-foreground">{subtitle}</p>
132
+ {/if}
133
+ </div>
134
+ </div>
135
+
136
+ {#if modes && modes.length > 1}
137
+ <div class="order-last flex w-full justify-center md:order-none md:w-auto">
138
+ <SegmentedControl
139
+ options={modes}
140
+ bind:value={mode}
141
+ onValueChange={onModeChange}
142
+ label="Editor mode"
143
+ size="sm"
144
+ class="w-full md:w-auto [&>*]:flex-1 md:[&>*]:flex-none"
145
+ />
146
+ </div>
147
+ {/if}
148
+
149
+ <div class="flex shrink-0 items-center justify-end gap-1 md:flex-1 md:basis-0 md:gap-2">
150
+ {#if status}{@render status()}{/if}
151
+ {#if issues}{@render issues()}{/if}
152
+ {#if actions}{@render actions()}{/if}
153
+ {#if menuItems && menuItems.length > 0}
154
+ <DropdownMenu
155
+ items={menuItems}
156
+ align="end"
157
+ triggerVariant="ghost"
158
+ triggerSize="icon"
159
+ triggerClass="h-9 w-9"
160
+ triggerLabel={menuLabel}
161
+ >
162
+ {#snippet trigger()}
163
+ <Icon name="more-horizontal" size={18} aria-hidden="true" />
164
+ {/snippet}
165
+ </DropdownMenu>
166
+ {/if}
167
+ </div>
168
+ </header>
@@ -0,0 +1,46 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type DropdownMenuItem, type DropdownMenuGroup } from '../DropdownMenu.svelte';
3
+ import { type SegmentedControlOption } from '../SegmentedControl.svelte';
4
+ /**
5
+ * StudioBar - the one bar across the top of a Studio.
6
+ *
7
+ * Left: back link and the document title, edited in place. Middle: the
8
+ * modes that separate building from configuring (e.g. Build · Settings ·
9
+ * Share). Right: save status, issues, the primary action, and one overflow
10
+ * menu for everything else (export, history, delete...). On phones the
11
+ * modes wrap onto their own row.
12
+ */
13
+ interface Props {
14
+ /** Title of the document being edited (bindable) */
15
+ title?: string;
16
+ /** Called when an edited title is committed (blur or Enter) */
17
+ onTitleChange?: (title: string) => void;
18
+ /** Show the title as text instead of an input */
19
+ titleReadonly?: boolean;
20
+ titlePlaceholder?: string;
21
+ /** Small line under the title, e.g. "Survey · 5 questions" */
22
+ subtitle?: string;
23
+ /** Back link, e.g. to the list of surveys */
24
+ backHref?: string;
25
+ /** Accessible name of the back link (default: "Back") */
26
+ backLabel?: string;
27
+ /** Mode tabs; omit for single-mode editors */
28
+ modes?: SegmentedControlOption[];
29
+ /** Active mode (bindable) */
30
+ mode?: string;
31
+ onModeChange?: (mode: string) => void;
32
+ /** Save state, normally a `SaveStatus` */
33
+ status?: Snippet;
34
+ /** Issues, normally an `IssuesButton` */
35
+ issues?: Snippet;
36
+ /** Primary action(s), e.g. a Publish or Present button */
37
+ actions?: Snippet;
38
+ /** Overflow menu items (export, history, delete...) */
39
+ menuItems?: (DropdownMenuItem | DropdownMenuGroup)[];
40
+ /** Accessible name of the overflow menu (default: "More actions") */
41
+ menuLabel?: string;
42
+ class?: string;
43
+ }
44
+ declare const StudioBar: import("svelte").Component<Props, {}, "title" | "mode">;
45
+ type StudioBar = ReturnType<typeof StudioBar>;
46
+ export default StudioBar;
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { cn } from '../../utils.js';
4
+ import SegmentedControl from '../SegmentedControl.svelte';
5
+ import { getStudioContext } from './context.js';
6
+
7
+ /**
8
+ * StudioCanvas - the middle of a Studio: the real thing, as people will see it.
9
+ *
10
+ * Scrolls on its own and centres its content on a quiet backdrop. With
11
+ * `deviceToggle`, a Desktop/Phone switch narrows the frame to phone width.
12
+ * Wrap each selectable part in a `StudioCanvasItem`, or, for content
13
+ * rendered elsewhere (an iframe), handle selection yourself.
14
+ */
15
+ interface Props {
16
+ children: Snippet;
17
+ /** Show the Desktop/Phone switch */
18
+ deviceToggle?: boolean;
19
+ /** Previewed device (bindable) */
20
+ device?: 'desktop' | 'phone';
21
+ /** Width of the frame on desktop (default: 48rem, a readable column) */
22
+ maxWidth?: string;
23
+ /** Extra controls beside the device switch */
24
+ toolbar?: Snippet;
25
+ /** Accessible name of the canvas region (default: "Canvas") */
26
+ label?: string;
27
+ /** Clicked on the backdrop, outside any item: usually clears the selection */
28
+ onBackgroundClick?: () => void;
29
+ /** Classes for the frame that holds the content */
30
+ frameClass?: string;
31
+ class?: string;
32
+ }
33
+
34
+ let {
35
+ children,
36
+ deviceToggle = false,
37
+ device = $bindable('desktop'),
38
+ maxWidth = '48rem',
39
+ toolbar,
40
+ label = 'Canvas',
41
+ onBackgroundClick,
42
+ frameClass,
43
+ class: className,
44
+ }: Props = $props();
45
+
46
+ const studio = getStudioContext();
47
+ // On a phone the canvas already is phone width; the switch would do nothing
48
+ const showDevices = $derived(deviceToggle && studio?.layout !== 'compact');
49
+ const width = $derived(device === 'phone' && showDevices ? '390px' : maxWidth);
50
+ </script>
51
+
52
+ <section aria-label={label} class={cn('flex h-full flex-col', className)}>
53
+ {#if showDevices || toolbar}
54
+ <div class="flex shrink-0 items-center justify-end gap-2 px-3 pt-3">
55
+ {#if toolbar}{@render toolbar()}{/if}
56
+ {#if showDevices}
57
+ <SegmentedControl
58
+ bind:value={device}
59
+ label="Preview size"
60
+ size="sm"
61
+ options={[
62
+ { value: 'desktop', label: 'Desktop', icon: 'monitor' },
63
+ { value: 'phone', label: 'Phone', icon: 'smartphone' },
64
+ ]}
65
+ />
66
+ {/if}
67
+ </div>
68
+ {/if}
69
+
70
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
71
+ <div
72
+ class="min-h-0 flex-1 overflow-y-auto px-3 pb-20 pt-3 md:px-6 md:pb-10"
73
+ onclick={(e) => {
74
+ if (e.target === e.currentTarget) onBackgroundClick?.();
75
+ }}
76
+ >
77
+ <div
78
+ class={cn(
79
+ 'mx-auto w-full rounded-lg border border-border bg-background shadow-sm transition-[max-width] duration-200',
80
+ frameClass
81
+ )}
82
+ style:max-width={width}
83
+ >
84
+ {@render children()}
85
+ </div>
86
+ </div>
87
+ </section>
@@ -0,0 +1,30 @@
1
+ import type { Snippet } from 'svelte';
2
+ /**
3
+ * StudioCanvas - the middle of a Studio: the real thing, as people will see it.
4
+ *
5
+ * Scrolls on its own and centres its content on a quiet backdrop. With
6
+ * `deviceToggle`, a Desktop/Phone switch narrows the frame to phone width.
7
+ * Wrap each selectable part in a `StudioCanvasItem`, or, for content
8
+ * rendered elsewhere (an iframe), handle selection yourself.
9
+ */
10
+ interface Props {
11
+ children: Snippet;
12
+ /** Show the Desktop/Phone switch */
13
+ deviceToggle?: boolean;
14
+ /** Previewed device (bindable) */
15
+ device?: 'desktop' | 'phone';
16
+ /** Width of the frame on desktop (default: 48rem, a readable column) */
17
+ maxWidth?: string;
18
+ /** Extra controls beside the device switch */
19
+ toolbar?: Snippet;
20
+ /** Accessible name of the canvas region (default: "Canvas") */
21
+ label?: string;
22
+ /** Clicked on the backdrop, outside any item: usually clears the selection */
23
+ onBackgroundClick?: () => void;
24
+ /** Classes for the frame that holds the content */
25
+ frameClass?: string;
26
+ class?: string;
27
+ }
28
+ declare const StudioCanvas: import("svelte").Component<Props, {}, "device">;
29
+ type StudioCanvas = ReturnType<typeof StudioCanvas>;
30
+ export default StudioCanvas;