@dfosco/storyboard-core 3.6.0 → 3.7.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.
Files changed (50) hide show
  1. package/dist/storyboard-ui.css +1 -1
  2. package/dist/storyboard-ui.js +12274 -11387
  3. package/dist/storyboard-ui.js.map +1 -1
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +1 -1
  6. package/src/CanvasZoomControl.svelte +8 -8
  7. package/src/CommentsMenuButton.svelte +7 -21
  8. package/src/CoreUIBar.svelte +19 -3
  9. package/src/CreateMenuButton.svelte +8 -12
  10. package/src/InspectorPanel.svelte +12 -15
  11. package/src/SidePanel.svelte +14 -14
  12. package/src/assets/fonts/IoskeleyMono-Bold.woff2 +0 -0
  13. package/src/assets/fonts/IoskeleyMono-Italic.woff2 +0 -0
  14. package/src/assets/fonts/IoskeleyMono-Medium.woff2 +0 -0
  15. package/src/assets/fonts/IoskeleyMono-Regular.woff2 +0 -0
  16. package/src/assets/fonts/IoskeleyMono-SemiBold.woff2 +0 -0
  17. package/src/comments/ui/AuthModal.svelte +45 -12
  18. package/src/comments/ui/authModal.js +6 -1
  19. package/src/comments/ui/comment-layout.css +15 -15
  20. package/src/comments/ui/commentWindow.js +6 -1
  21. package/src/comments/ui/comments.css +57 -57
  22. package/src/comments/ui/commentsDrawer.js +2 -0
  23. package/src/comments/ui/composer.js +7 -2
  24. package/src/comments/ui/mount.js +252 -33
  25. package/src/comments/ui/mount.test.js +138 -0
  26. package/src/core-ui-colors.css +28 -28
  27. package/src/inspector/mouseMode.js +2 -2
  28. package/src/lib/components/ui/button/button.svelte +9 -9
  29. package/src/lib/components/ui/panel/panel-content.svelte +2 -2
  30. package/src/lib/components/ui/select/select-trigger.svelte +1 -1
  31. package/src/lib/components/ui/toggle/toggle.svelte +1 -1
  32. package/src/lib/components/ui/toggle-group/toggle-group.svelte +2 -2
  33. package/src/lib/components/ui/trigger-button/trigger-button.svelte +13 -13
  34. package/src/modes.css +21 -21
  35. package/src/mountStoryboardCore.js +4 -4
  36. package/src/sidepanel.css +11 -11
  37. package/src/styles/tailwind.css +89 -1
  38. package/src/svelte-plugin-ui/components/ModeSwitch.svelte +3 -3
  39. package/src/svelte-plugin-ui/components/Viewfinder.svelte +31 -11
  40. package/src/svelte-plugin-ui/styles/base.css +41 -41
  41. package/src/workshop/features/createFlow/CreateFlowForm.svelte +187 -25
  42. package/src/workshop/features/createFlow/server.js +437 -40
  43. package/src/workshop/features/createPage/CreatePageForm.svelte +249 -0
  44. package/src/workshop/features/createPage/index.js +11 -0
  45. package/src/workshop/features/createPrototype/CreatePrototypeForm.svelte +77 -24
  46. package/src/workshop/features/createPrototype/server.js +14 -16
  47. package/src/workshop/features/registry-server.js +1 -0
  48. package/src/workshop/features/registry.js +2 -0
  49. package/src/workshop/features/templateIndex.js +155 -0
  50. package/toolbar.config.json +2 -1
@@ -9,8 +9,10 @@
9
9
  -->
10
10
 
11
11
  <script lang="ts">
12
+ import { onMount, onDestroy } from 'svelte'
12
13
  import { buildPrototypeIndex } from '../../viewfinder.js'
13
14
  import { getLocal, setLocal } from '../../localStorage.js'
15
+ import { getParam, setParam, removeParam } from '../../session.js'
14
16
  import Icon from './Icon.svelte'
15
17
 
16
18
  interface Props {
@@ -86,6 +88,24 @@
86
88
  type ViewMode = 'prototypes' | 'canvases'
87
89
  let viewMode: ViewMode = $state('prototypes')
88
90
 
91
+ function syncViewModeFromHash() {
92
+ viewMode = getParam('canvas') != null ? 'canvases' : 'prototypes'
93
+ }
94
+
95
+ onMount(() => {
96
+ syncViewModeFromHash()
97
+ window.addEventListener('hashchange', syncViewModeFromHash)
98
+ })
99
+
100
+ onDestroy(() => {
101
+ window.removeEventListener('hashchange', syncViewModeFromHash)
102
+ })
103
+
104
+ $effect(() => {
105
+ if (viewMode === 'canvases') setParam('canvas', '1')
106
+ else removeParam('canvas')
107
+ })
108
+
89
109
  // Canvas folder data: extract folders that contain canvases for canvas view
90
110
  const canvasFolders = $derived.by(() => {
91
111
  const src = prototypeIndex.sorted?.[sortBy]?.folders ?? folders
@@ -152,7 +172,7 @@
152
172
  const w = 20 + (s * (i + 3)) % 80
153
173
  const ht = 8 + (s * (i + 7)) % 40
154
174
  const opacity = 0.06 + ((s * (i + 2)) % 20) / 100
155
- const fill = i % 3 === 0 ? 'var(--placeholder-accent)' : i % 3 === 1 ? 'var(--placeholder-fg)' : 'var(--placeholder-muted)'
175
+ const fill = i % 3 === 0 ? 'var(--sb--placeholder-accent)' : i % 3 === 1 ? 'var(--sb--placeholder-fg)' : 'var(--sb--placeholder-muted)'
156
176
  rects += `<rect x="${x}" y="${y}" width="${w}" height="${ht}" rx="2" fill="${fill}" opacity="${opacity}" />`
157
177
  }
158
178
 
@@ -160,15 +180,15 @@
160
180
  for (let i = 0; i < 6; i++) {
161
181
  const s = h * (i + 5)
162
182
  const y = 10 + (s % 180)
163
- lines += `<line x1="0" y1="${y}" x2="320" y2="${y}" stroke="var(--placeholder-grid)" stroke-width="0.5" opacity="0.4" />`
183
+ lines += `<line x1="0" y1="${y}" x2="320" y2="${y}" stroke="var(--sb--placeholder-grid)" stroke-width="0.5" opacity="0.4" />`
164
184
  }
165
185
  for (let i = 0; i < 8; i++) {
166
186
  const s = h * (i + 9)
167
187
  const x = 10 + (s % 300)
168
- lines += `<line x1="${x}" y1="0" x2="${x}" y2="200" stroke="var(--placeholder-grid)" stroke-width="0.5" opacity="0.3" />`
188
+ lines += `<line x1="${x}" y1="0" x2="${x}" y2="200" stroke="var(--sb--placeholder-grid)" stroke-width="0.5" opacity="0.3" />`
169
189
  }
170
190
 
171
- return `<svg viewBox="0 0 320 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="320" height="200" fill="var(--placeholder-bg)" />${lines}${rects}</svg>`
191
+ return `<svg viewBox="0 0 320 200" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect width="320" height="200" fill="var(--sb--placeholder-bg)" />${lines}${rects}</svg>`
172
192
  }
173
193
 
174
194
  // Branch switching
@@ -450,7 +470,7 @@
450
470
  <a class="listItem" href={canvas.route}>
451
471
  <div class="cardBody">
452
472
  <p class="protoName">
453
- <span class="protoIcon">{canvas.icon || '🎨'}</span>
473
+ <span class="protoIcon">{canvas.icon || ''}</span>
454
474
  {canvas.name}
455
475
  </p>
456
476
  {#if canvas.description}
@@ -947,11 +967,11 @@
947
967
  overflow: hidden;
948
968
  background: var(--bgColor-inset, #010409);
949
969
 
950
- --placeholder-bg: var(--bgColor-inset, #010409);
951
- --placeholder-grid: var(--borderColor-default, #30363d);
952
- --placeholder-accent: var(--fgColor-accent, #58a6ff);
953
- --placeholder-fg: var(--fgColor-default, #c9d1d9);
954
- --placeholder-muted: var(--fgColor-muted, #484f58);
970
+ --sb--placeholder-bg: var(--bgColor-inset, #010409);
971
+ --sb--placeholder-grid: var(--borderColor-default, #30363d);
972
+ --sb--placeholder-accent: var(--fgColor-accent, #58a6ff);
973
+ --sb--placeholder-fg: var(--fgColor-default, #c9d1d9);
974
+ --sb--placeholder-muted: var(--fgColor-muted, #484f58);
955
975
  }
956
976
 
957
977
  .thumbnail :global(svg) {
@@ -975,7 +995,7 @@
975
995
  padding: 10px 14px;
976
996
  margin-bottom: 16px;
977
997
  border-radius: 8px;
978
- border: 1px solid var(--borderColor-default, var(--color-border, #d0d7de));
998
+ border: 1px solid var(--borderColor-default, var(--sb--color-border, #d0d7de));
979
999
  background: var(--bgColor-attention-muted, #3d2e00);
980
1000
  color: var(--fgColor-attention, #9a6700);
981
1001
  font-size: 13px;
@@ -9,61 +9,61 @@
9
9
 
10
10
  /* --- Light theme (default) --- */
11
11
  :root {
12
- --sb-bg: #ffffff;
13
- --sb-bg-inset: #f6f8fa;
14
- --sb-bg-muted: #f3f4f6;
15
- --sb-border: #d1d5db;
16
- --sb-border-muted: #e5e7eb;
17
- --sb-fg: #1f2328;
18
- --sb-fg-muted: #656d76;
19
- --sb-fg-accent: #0969da;
20
- --sb-fg-success: #1a7f37;
21
- --sb-fg-danger: #d1242f;
22
- --sb-btn-success: #1a7f37;
12
+ --sb--bg: #ffffff;
13
+ --sb--bg-inset: #f6f8fa;
14
+ --sb--bg-muted: #f3f4f6;
15
+ --sb--border: #d1d5db;
16
+ --sb--border-muted: #e5e7eb;
17
+ --sb--fg: #1f2328;
18
+ --sb--fg-muted: #656d76;
19
+ --sb--fg-accent: #0969da;
20
+ --sb--fg-success: #1a7f37;
21
+ --sb--fg-danger: #d1242f;
22
+ --sb--btn-success: #1a7f37;
23
23
  }
24
24
 
25
25
  /* --- Dark theme (any dark-* variant) --- */
26
26
  [data-sb-theme^="dark"] {
27
- --sb-bg: #161b22;
28
- --sb-bg-inset: #0d1117;
29
- --sb-bg-muted: #21262d;
30
- --sb-border: #30363d;
31
- --sb-border-muted: #21262d;
32
- --sb-fg: #e6edf3;
33
- --sb-fg-muted: #8b949e;
34
- --sb-fg-accent: #58a6ff;
35
- --sb-fg-success: #3fb950;
36
- --sb-fg-danger: #f85149;
37
- --sb-btn-success: #238636;
27
+ --sb--bg: #161b22;
28
+ --sb--bg-inset: #0d1117;
29
+ --sb--bg-muted: #21262d;
30
+ --sb--border: #30363d;
31
+ --sb--border-muted: #21262d;
32
+ --sb--fg: #e6edf3;
33
+ --sb--fg-muted: #8b949e;
34
+ --sb--fg-accent: #58a6ff;
35
+ --sb--fg-success: #3fb950;
36
+ --sb--fg-danger: #f85149;
37
+ --sb--btn-success: #238636;
38
38
  }
39
39
 
40
40
  /* --- Semantic utility classes (supplement Tachyons) --- */
41
- .sb-bg { background-color: var(--sb-bg); }
42
- .sb-bg-inset { background-color: var(--sb-bg-inset); }
43
- .sb-bg-muted { background-color: var(--sb-bg-muted); }
44
- .sb-b-default { border-color: var(--sb-border); }
45
- .sb-b-muted { border-color: var(--sb-border-muted); }
46
- .sb-fg { color: var(--sb-fg); }
47
- .sb-fg-muted { color: var(--sb-fg-muted); }
48
- .sb-fg-accent { color: var(--sb-fg-accent); }
49
- .sb-fg-success { color: var(--sb-fg-success); }
50
- .sb-fg-danger { color: var(--sb-fg-danger); }
51
- .sb-btn-success { background-color: var(--sb-btn-success); color: #fff; }
41
+ .sb-bg { background-color: var(--sb--bg); }
42
+ .sb-bg-inset { background-color: var(--sb--bg-inset); }
43
+ .sb-bg-muted { background-color: var(--sb--bg-muted); }
44
+ .sb-b-default { border-color: var(--sb--border); }
45
+ .sb-b-muted { border-color: var(--sb--border-muted); }
46
+ .sb-fg { color: var(--sb--fg); }
47
+ .sb-fg-muted { color: var(--sb--fg-muted); }
48
+ .sb-fg-accent { color: var(--sb--fg-accent); }
49
+ .sb-fg-success { color: var(--sb--fg-success); }
50
+ .sb-fg-danger { color: var(--sb--fg-danger); }
51
+ .sb-btn-success { background-color: var(--sb--btn-success); color: #fff; }
52
52
  .sb-btn-success:hover { filter: brightness(1.15); }
53
- .sb-btn-cancel { background-color: var(--sb-bg-muted); border: 1px solid var(--sb-border); color: var(--sb-fg); }
54
- .sb-btn-cancel:hover { background-color: var(--sb-border-muted); }
53
+ .sb-btn-cancel { background-color: var(--sb--bg-muted); border: 1px solid var(--sb--border); color: var(--sb--fg); }
54
+ .sb-btn-cancel:hover { background-color: var(--sb--border-muted); }
55
55
 
56
56
  .sb-input {
57
- background: var(--sb-bg-inset);
58
- border: 1px solid var(--sb-border);
59
- color: var(--sb-fg);
57
+ background: var(--sb--bg-inset);
58
+ border: 1px solid var(--sb--border);
59
+ color: var(--sb--fg);
60
60
  outline: none;
61
61
  }
62
62
  .sb-input:focus {
63
- border-color: var(--sb-fg-accent);
64
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--sb-fg-accent) 15%, transparent);
63
+ border-color: var(--sb--fg-accent);
64
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--sb--fg-accent) 15%, transparent);
65
65
  }
66
- .sb-input::placeholder { color: var(--sb-fg-muted); }
66
+ .sb-input::placeholder { color: var(--sb--fg-muted); }
67
67
 
68
68
  .sb-shadow { box-shadow: 0 8px 24px rgba(0,0,0,0.15), 0 2px 6px rgba(0,0,0,0.1); }
69
69
  [data-sb-theme^="dark"] .sb-shadow { box-shadow: 0 8px 24px rgba(0,0,0,0.4), 0 2px 6px rgba(0,0,0,0.3); }
@@ -11,22 +11,36 @@
11
11
  import * as Panel from '../../../lib/components/ui/panel/index.js'
12
12
  import * as Alert from '../../../lib/components/ui/alert/index.js'
13
13
 
14
- interface Props { onClose?: () => void }
14
+ interface Props {
15
+ onClose?: () => void
16
+ }
15
17
  let { onClose }: Props = $props()
16
18
 
17
19
  let name = $state('')
18
20
  let title = $state('')
19
21
  let titleTouched = $state(false)
20
22
  let selectedPrototype = $state('')
21
- let author = $state('')
22
23
  let description = $state('')
23
24
  let copyFrom = $state('')
25
+ let startingPage = $state('')
26
+ let newPagePath = $state('')
27
+ let newPageTemplate = $state('')
28
+ const CREATE_NEW_PAGE_VALUE = '__create_new_page__'
24
29
 
25
- interface PrototypeEntry { name: string; folder?: string }
26
- interface FlowEntry { name: string; title: string; path: string }
30
+ interface PrototypeEntry { name: string; folder?: string; routes?: string[] }
31
+ interface FlowEntry { name: string; title: string; path: string; prototype?: string; folder?: string; route?: string }
32
+ interface PartialEntry {
33
+ id: string
34
+ name: string
35
+ kind: 'template' | 'recipe'
36
+ scope: 'global' | 'prototype'
37
+ prototype?: string
38
+ folder?: string
39
+ }
27
40
 
28
41
  let prototypes: PrototypeEntry[] = $state([])
29
42
  let flows: FlowEntry[] = $state([])
43
+ let partials: PartialEntry[] = $state([])
30
44
  let loading = $state(true)
31
45
  let submitting = $state(false)
32
46
  let error: string | null = $state(null)
@@ -45,11 +59,49 @@
45
59
  : name.trim() && !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(kebabName) ? 'Name must be kebab-case'
46
60
  : null
47
61
  )
48
- const canSubmit = $derived(!!kebabName && !nameError && !submitting)
49
-
50
62
  const selectedProtoEntry = $derived(
51
63
  selectedPrototype ? prototypes.find(p => p.name === selectedPrototype) : null
52
64
  )
65
+ const copyFromFlows = $derived(
66
+ selectedPrototype
67
+ ? flows.filter((f) => {
68
+ if (f.prototype !== selectedPrototype) return false
69
+ const flowFolder = f.folder || ''
70
+ const selectedFolder = selectedProtoEntry?.folder || ''
71
+ return flowFolder === selectedFolder
72
+ })
73
+ : []
74
+ )
75
+ const prototypeRoutes = $derived(selectedProtoEntry?.routes || [])
76
+ const showNewPageFields = $derived(startingPage === CREATE_NEW_PAGE_VALUE)
77
+ const newPagePrefix = $derived(selectedPrototype ? `/${selectedPrototype}/` : '/prototype-name/')
78
+ const templateChoices = $derived(
79
+ selectedPrototype
80
+ ? partials.filter((partial) => {
81
+ if (partial.scope === 'global') return true
82
+ return partial.prototype === selectedPrototype && (partial.folder || '') === (selectedProtoEntry?.folder || '')
83
+ })
84
+ : partials.filter((partial) => partial.scope === 'global')
85
+ )
86
+ const globalTemplateChoices = $derived(templateChoices.filter((partial) => partial.scope === 'global'))
87
+ const localTemplateChoices = $derived(
88
+ templateChoices.filter((partial) => partial.scope === 'prototype')
89
+ )
90
+ const localTemplateHeading = $derived(
91
+ selectedPrototype || ''
92
+ )
93
+ const startingPageError = $derived(
94
+ showNewPageFields && !newPagePath.trim()
95
+ ? 'Please provide a new page path'
96
+ : null
97
+ )
98
+ const canSubmit = $derived(
99
+ !!kebabName &&
100
+ !!selectedPrototype &&
101
+ !nameError &&
102
+ !startingPageError &&
103
+ !submitting
104
+ )
53
105
 
54
106
  function getApiUrl() {
55
107
  const basePath = document.querySelector('base')?.getAttribute('href') || '/'
@@ -63,10 +115,48 @@
63
115
  const data = await res.json()
64
116
  prototypes = data.prototypes || []
65
117
  flows = data.flows || []
118
+ partials = data.partials || []
66
119
  }
67
120
  } catch { /* defaults */ } finally { loading = false }
68
121
  })
69
122
 
123
+ $effect(() => {
124
+ if (!selectedPrototype) {
125
+ copyFrom = ''
126
+ startingPage = ''
127
+ newPagePath = ''
128
+ newPageTemplate = ''
129
+ return
130
+ }
131
+
132
+ const routes = selectedProtoEntry?.routes || []
133
+ if (!startingPage || (startingPage !== CREATE_NEW_PAGE_VALUE && !routes.includes(startingPage))) {
134
+ startingPage = routes[0] || ''
135
+ }
136
+
137
+ if (showNewPageFields) {
138
+ if (!newPagePath.startsWith(newPagePrefix)) {
139
+ newPagePath = `${newPagePrefix}new-page`
140
+ }
141
+ }
142
+
143
+ const activeSelectionStillValid = copyFrom && copyFromFlows.some((f) => f.path === copyFrom)
144
+ if (activeSelectionStillValid) return
145
+
146
+ const matchStartingPage = startingPage && startingPage !== CREATE_NEW_PAGE_VALUE
147
+ ? copyFromFlows.find((f) => f.route === startingPage)
148
+ : null
149
+ copyFrom = matchStartingPage?.path || ''
150
+ })
151
+
152
+ $effect(() => {
153
+ if (!newPageTemplate) return
154
+ const stillAvailable = templateChoices.some((choice) => choice.id === newPageTemplate)
155
+ if (!stillAvailable) {
156
+ newPageTemplate = ''
157
+ }
158
+ })
159
+
70
160
  function handleTitleInput(e: Event) { title = (e.target as HTMLInputElement).value; titleTouched = true }
71
161
  function handleTitleBlur() { if (!title.trim()) titleTouched = false }
72
162
 
@@ -80,11 +170,15 @@
80
170
  body: JSON.stringify({
81
171
  name: kebabName,
82
172
  title: displayTitle,
83
- prototype: selectedPrototype || undefined,
173
+ prototype: selectedPrototype,
84
174
  folder: selectedProtoEntry?.folder || undefined,
85
- author: author.trim() || undefined,
86
175
  description: description.trim() || undefined,
87
176
  copyFrom: copyFrom || undefined,
177
+ startingPage: showNewPageFields ? newPagePath.trim() : (startingPage || undefined),
178
+ createPage: showNewPageFields ? {
179
+ path: newPagePath.trim(),
180
+ template: newPageTemplate || undefined,
181
+ } : undefined,
88
182
  }),
89
183
  })
90
184
  const data = await res.json()
@@ -102,7 +196,7 @@
102
196
  </Panel.Header>
103
197
 
104
198
  <!-- svelte-ignore a11y_no_static_element_interactions -->
105
- <div class="p-4 pt-2 space-y-3" onkeydown={handleKeydown}>
199
+ <div class="p-4 pt-2 space-y-5" onkeydown={handleKeydown}>
106
200
  <div class="space-y-1">
107
201
  <Label for="sb-flow-name">Name</Label>
108
202
  <Input id="sb-flow-name" placeholder="e.g. empty-state" autocomplete="off" spellcheck="false" bind:value={name} />
@@ -115,27 +209,95 @@
115
209
  <Input id="sb-flow-title" placeholder={autoTitle || 'Auto-derived from name'} value={displayTitle} oninput={handleTitleInput} onblur={handleTitleBlur} />
116
210
  </div>
117
211
 
118
- <div class="grid grid-cols-2 gap-3">
119
- <div class="space-y-1">
120
- <Label for="sb-flow-prototype">Prototype</Label>
121
- <select class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" id="sb-flow-prototype" bind:value={selectedPrototype} disabled={loading}>
122
- <option value="">Global</option>
123
- {#each prototypes as p}<option value={p.name}>{p.folder ? `${p.folder} / ${p.name}` : p.name}</option>{/each}
124
- </select>
125
- </div>
126
- <div class="space-y-1">
127
- <Label for="sb-flow-copy">Copy from</Label>
128
- <select class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" id="sb-flow-copy" bind:value={copyFrom} disabled={loading}>
212
+ <div class="space-y-1">
213
+ <Label for="sb-flow-prototype">Prototype</Label>
214
+ <select class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50" id="sb-flow-prototype" bind:value={selectedPrototype} disabled={loading}>
215
+ <option value="">Select prototype</option>
216
+ {#each prototypes as p}<option value={p.name}>{p.folder ? `${p.folder} / ${p.name}` : p.name}</option>{/each}
217
+ </select>
218
+ </div>
219
+
220
+ <div class="space-y-1">
221
+ <Label for="sb-flow-copy">Copy from existing flow <span class="text-muted-foreground">(optional)</span></Label>
222
+ <select
223
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
224
+ id="sb-flow-copy"
225
+ bind:value={copyFrom}
226
+ disabled={loading || !selectedPrototype}
227
+ >
228
+ {#if !selectedPrototype}
229
+ <option value="">Select a prototype first</option>
230
+ {:else}
129
231
  <option value="">None</option>
130
- {#each flows as f}<option value={f.path}>{f.title} ({f.name})</option>{/each}
131
- </select>
132
- </div>
232
+ {#each copyFromFlows as f}<option value={f.path}>{f.title} ({f.name})</option>{/each}
233
+ {/if}
234
+ </select>
133
235
  </div>
134
236
 
135
237
  <div class="space-y-1">
136
- <Label for="sb-flow-author">Author</Label>
137
- <Input id="sb-flow-author" placeholder="GitHub handle(s), comma-separated" bind:value={author} />
238
+ <Label for="sb-flow-starting-page">Starting page <span class="text-muted-foreground">(optional)</span></Label>
239
+ <select
240
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
241
+ id="sb-flow-starting-page"
242
+ bind:value={startingPage}
243
+ disabled={loading || !selectedPrototype}
244
+ >
245
+ {#if !selectedPrototype}
246
+ <option value="">Select a prototype first</option>
247
+ {:else}
248
+ <option value="">None</option>
249
+ {#each prototypeRoutes as route}
250
+ <option value={route}>{route}</option>
251
+ {/each}
252
+ <option value={CREATE_NEW_PAGE_VALUE}>Create new page</option>
253
+ {/if}
254
+ </select>
255
+ <p class="text-xs text-muted-foreground">Users will be redirected to this page</p>
138
256
  </div>
257
+
258
+ {#if showNewPageFields}
259
+ <div class="space-y-1">
260
+ <Label for="sb-flow-new-page-path">New page path</Label>
261
+ <div class="flex items-center gap-2">
262
+ <span class="text-xs text-muted-foreground font-mono">{newPagePrefix}</span>
263
+ <Input
264
+ id="sb-flow-new-page-path"
265
+ placeholder="new-page"
266
+ value={newPagePath.startsWith(newPagePrefix) ? newPagePath.slice(newPagePrefix.length) : ''}
267
+ oninput={(e: Event) => {
268
+ const suffix = (e.target as HTMLInputElement).value.replace(/^\/+/, '')
269
+ newPagePath = `${newPagePrefix}${suffix}`
270
+ }}
271
+ />
272
+ </div>
273
+ </div>
274
+ <div class="space-y-1">
275
+ <Label for="sb-flow-new-page-template">Template / recipe</Label>
276
+ <select
277
+ class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
278
+ id="sb-flow-new-page-template"
279
+ bind:value={newPageTemplate}
280
+ disabled={!selectedPrototype}
281
+ >
282
+ <option value="">Blank page</option>
283
+ {#if globalTemplateChoices.length > 0}
284
+ <optgroup label="Global">
285
+ {#each globalTemplateChoices as partial}
286
+ <option value={partial.id}>{partial.name}</option>
287
+ {/each}
288
+ </optgroup>
289
+ {/if}
290
+ {#if localTemplateChoices.length > 0}
291
+ <optgroup label={localTemplateHeading}>
292
+ {#each localTemplateChoices as partial}
293
+ <option value={partial.id}>{partial.name}</option>
294
+ {/each}
295
+ </optgroup>
296
+ {/if}
297
+ </select>
298
+ </div>
299
+ {#if startingPageError}<p class="text-sm text-destructive">{startingPageError}</p>{/if}
300
+ {/if}
139
301
 
140
302
  <div class="space-y-1">
141
303
  <Label for="sb-flow-desc">Description</Label>