@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
@@ -387,12 +387,9 @@
387
387
  requestAnimationFrame(() => {
388
388
  const el = sourceContainer.querySelector('.highlighted-line')
389
389
  if (el) {
390
- // Jump near the target instantly, then smooth-scroll the last bit
391
- const targetTop = el.offsetTop - sourceContainer.clientHeight / 2
392
- sourceContainer.scrollTop = targetTop - 100
393
- requestAnimationFrame(() => {
394
- el.scrollIntoView({ block: 'center', behavior: 'smooth' })
395
- })
390
+ // Align the highlighted line to the top of the code viewport.
391
+ const targetTop = Math.max(el.offsetTop - 24, 0)
392
+ sourceContainer.scrollTo({ top: targetTop, behavior: 'smooth' })
396
393
  } else {
397
394
  sourceContainer.scrollTop = 0
398
395
  }
@@ -531,7 +528,7 @@
531
528
  </p>
532
529
  <button
533
530
  class="mt-2 px-4 py-1.5 text-xs font-medium rounded-md border-none cursor-pointer transition-colors"
534
- style:background="var(--color-purple, #7655a4)"
531
+ style:background="var(--sb--color-purple, #7655a4)"
535
532
  style:color="#fff"
536
533
  onclick={startInspecting}
537
534
  >
@@ -552,7 +549,7 @@
552
549
  class="mt-2 px-4 py-1.5 text-xs font-medium rounded-md border cursor-pointer transition-colors"
553
550
  style:background="transparent"
554
551
  style:color="var(--fgColor-muted)"
555
- style:border-color="var(--borderColor-default, var(--color-border, #d1d9e0))"
552
+ style:border-color="var(--borderColor-default, var(--sb--color-border, #d1d9e0))"
556
553
  onclick={stopInspecting}
557
554
  >
558
555
  Cancel
@@ -564,7 +561,7 @@
564
561
  <div class="flex flex-col flex-1 min-h-0 p-3 pt-0 gap-3">
565
562
  <!-- Component name -->
566
563
  <div>
567
- <h3 class="text-base font-bold m-0 inspector-mono" style:color="var(--color-purple, #7655a4)">
564
+ <h3 class="text-base font-bold m-0 inspector-mono" style:color="var(--sb--color-purple, #7655a4)">
568
565
  {componentInfo.name}
569
566
  </h3>
570
567
  </div>
@@ -621,7 +618,7 @@
621
618
  <!-- Re-select button -->
622
619
  <button
623
620
  class="flex items-center justify-center gap-1.5 w-full px-3 py-1.5 text-xs font-medium rounded-md border-none cursor-pointer transition-colors shrink-0"
624
- style:background="var(--color-purple, #7655a4)"
621
+ style:background="var(--sb--color-purple, #7655a4)"
625
622
  style:color="#fff"
626
623
  onclick={startInspecting}
627
624
  >
@@ -642,7 +639,7 @@
642
639
  width: 8px;
643
640
  height: 8px;
644
641
  border-radius: 50%;
645
- background: var(--color-purple, #7655a4);
642
+ background: var(--sb--color-purple, #7655a4);
646
643
  animation: inspector-pulse 1.5s ease-in-out infinite;
647
644
  flex-shrink: 0;
648
645
  }
@@ -681,8 +678,8 @@
681
678
  }
682
679
 
683
680
  .source-pre :global(.highlighted-line) {
684
- background: color-mix(in srgb, var(--color-purple, #7655a4) 20%, transparent);
685
- border-left: 2px solid var(--color-purple, #7655a4);
681
+ background: color-mix(in srgb, var(--sb--color-purple, #7655a4) 20%, transparent);
682
+ border-left: 2px solid var(--sb--color-purple, #7655a4);
686
683
  padding-left: 10px;
687
684
  }
688
685
 
@@ -734,8 +731,8 @@
734
731
  }
735
732
 
736
733
  .code-wrapper :global(.highlighted-line) {
737
- background: color-mix(in srgb, var(--color-purple, #7655a4) 20%, transparent);
738
- border-left: 2px solid var(--color-purple, #7655a4);
734
+ background: color-mix(in srgb, var(--sb--color-purple, #7655a4) 20%, transparent);
735
+ border-left: 2px solid var(--sb--color-purple, #7655a4);
739
736
  padding-left: 10px;
740
737
  }
741
738
 
@@ -52,14 +52,14 @@
52
52
  // Sync panel width to CSS custom property
53
53
  $effect(() => {
54
54
  if (typeof document !== 'undefined') {
55
- document.documentElement.style.setProperty('--sidepanel-width', `${panelWidth}px`)
55
+ document.documentElement.style.setProperty('--sb--sidepanel-width', `${panelWidth}px`)
56
56
  }
57
57
  })
58
58
 
59
59
  // Sync panel height to CSS custom property
60
60
  $effect(() => {
61
61
  if (typeof document !== 'undefined') {
62
- document.documentElement.style.setProperty('--sidepanel-height', `${panelHeight}px`)
62
+ document.documentElement.style.setProperty('--sb--sidepanel-height', `${panelHeight}px`)
63
63
  }
64
64
  })
65
65
 
@@ -272,15 +272,15 @@
272
272
  top: 0;
273
273
  right: 0;
274
274
  bottom: 0;
275
- width: var(--sidepanel-width, 420px);
275
+ width: var(--sb--sidepanel-width, 420px);
276
276
  z-index: 9998;
277
277
  display: flex;
278
278
  flex-direction: column;
279
- background-color: var(--bgColor-default, var(--color-background, #ffffff));
280
- border-left: 1px solid var(--borderColor-default, var(--color-border, #d0d7de));
279
+ background-color: var(--bgColor-default, var(--sb--color-background, #ffffff));
280
+ border-left: 1px solid var(--borderColor-default, var(--sb--color-border, #d0d7de));
281
281
  box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
282
282
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
283
- color: var(--fgColor-default, var(--color-foreground, #1f2328));
283
+ color: var(--fgColor-default, var(--sb--color-foreground, #1f2328));
284
284
  animation: sb-sidepanel-slide-in 0.25s ease;
285
285
  }
286
286
 
@@ -291,9 +291,9 @@
291
291
  bottom: 0;
292
292
  left: 0;
293
293
  width: 100% !important;
294
- height: var(--sidepanel-height, 300px);
294
+ height: var(--sb--sidepanel-height, 300px);
295
295
  border-left: none;
296
- border-top: 1px solid var(--borderColor-default, var(--color-border, #d0d7de));
296
+ border-top: 1px solid var(--borderColor-default, var(--sb--color-border, #d0d7de));
297
297
  box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15);
298
298
  animation: sb-sidepanel-slide-up 0.25s ease;
299
299
  }
@@ -320,7 +320,7 @@
320
320
  left: 0;
321
321
  right: 0;
322
322
  height: 3px;
323
- /* background: var(--mode-color, var(--borderColor-default, var(--color-border, #d0d7de))); */
323
+ /* background: var(--sb--mode-color, var(--borderColor-default, var(--sb--color-border, #d0d7de))); */
324
324
  }
325
325
 
326
326
  /* Drag handle — side mode (left edge, vertical) */
@@ -378,7 +378,7 @@
378
378
  font-weight: 600;
379
379
  text-transform: uppercase;
380
380
  letter-spacing: 0.05em;
381
- color: var(--fgColor-muted, var(--color-muted-foreground, #656d76));
381
+ color: var(--fgColor-muted, var(--sb--color-muted-foreground, #656d76));
382
382
  padding-left: 4px;
383
383
  }
384
384
 
@@ -392,7 +392,7 @@
392
392
  appearance: none;
393
393
  border: none;
394
394
  background: transparent;
395
- color: var(--fgColor-muted, var(--color-muted-foreground, #656d76));
395
+ color: var(--fgColor-muted, var(--sb--color-muted-foreground, #656d76));
396
396
  cursor: pointer;
397
397
  padding: 6px;
398
398
  border-radius: 6px;
@@ -404,7 +404,7 @@
404
404
 
405
405
  .sb-sidepanel-action-btn:hover {
406
406
  background: var(--bgColor-neutral-muted, rgba(110, 118, 129, 0.1));
407
- color: var(--fgColor-default, var(--color-foreground, #1f2328));
407
+ color: var(--fgColor-default, var(--sb--color-foreground, #1f2328));
408
408
  }
409
409
 
410
410
  /* Body */
@@ -425,8 +425,8 @@
425
425
  .sb-sidepanel-spinner {
426
426
  width: 20px;
427
427
  height: 20px;
428
- border: 2px solid var(--borderColor-default, var(--color-border, #d0d7de));
429
- border-top-color: var(--fgColor-muted, var(--color-muted-foreground, #656d76));
428
+ border: 2px solid var(--borderColor-default, var(--sb--color-border, #d0d7de));
429
+ border-top-color: var(--fgColor-muted, var(--sb--color-muted-foreground, #656d76));
430
430
  border-radius: 50%;
431
431
  animation: sb-spin 0.6s linear infinite;
432
432
  }
@@ -6,6 +6,7 @@
6
6
  <script lang="ts">
7
7
  import { onMount } from 'svelte'
8
8
  import { setToken, validateToken } from '../auth.js'
9
+ import { getCommentsConfig } from '../config.js'
9
10
  import { Button } from '../../lib/components/ui/button/index.js'
10
11
  import { Input } from '../../lib/components/ui/input/index.js'
11
12
  import { Label } from '../../lib/components/ui/label/index.js'
@@ -15,17 +16,38 @@
15
16
  interface Props {
16
17
  onDone?: (user: { login: string; avatarUrl: string }) => void
17
18
  onClose?: () => void
19
+ initialError?: string | null
18
20
  }
19
21
 
20
- let { onDone, onClose }: Props = $props()
22
+ let { onDone, onClose, initialError = null }: Props = $props()
21
23
 
22
24
  let token = $state('')
23
25
  let submitting = $state(false)
24
26
  let error: string | null = $state(null)
25
27
  let user: { login: string; avatarUrl: string } | null = $state(null)
26
- let inputEl: HTMLInputElement | undefined = $state()
28
+ let inputEl: HTMLInputElement | null = $state(null)
29
+ const commentsConfig = getCommentsConfig()
30
+ const repoOwner = commentsConfig?.repo?.owner || 'github'
31
+ const repoName = commentsConfig?.repo?.name || 'storyboard'
32
+ const repoSlug = `${repoOwner}/${repoName}`
33
+ const tokenTemplateName = 'Storyboard Comments'
34
+ const tokenTemplateDescription =
35
+ `Token for enabling comments on ${repoSlug} prototype. Configure as:\n\n` +
36
+ `Owner: ${repoOwner}\n` +
37
+ 'Expiration: 366 days (recommended)\n' +
38
+ `Repository access: Only select repositories > ${repoSlug}\n` +
39
+ 'Permissions: Repositories > Discussions > Access: Read and Write'
40
+ const tokenCreateUrl =
41
+ `https://github.com/settings/personal-access-tokens/new?` +
42
+ new URLSearchParams({
43
+ name: tokenTemplateName,
44
+ description: tokenTemplateDescription,
45
+ }).toString()
27
46
 
28
- onMount(() => { inputEl?.focus() })
47
+ onMount(() => {
48
+ error = initialError
49
+ inputEl?.focus()
50
+ })
29
51
 
30
52
  async function submit() {
31
53
  const val = token.trim()
@@ -43,30 +65,41 @@
43
65
  }
44
66
  </script>
45
67
 
46
- <div class="bg-popover text-popover-foreground border border-border rounded-lg shadow-lg overflow-hidden max-w-[420px] w-full font-sans">
68
+ <div class="bg-popover text-popover-foreground border border-border rounded-lg shadow-lg overflow-hidden max-w-[600px] w-full font-sans">
47
69
  <div class="flex items-center justify-between px-4 py-3 border-b border-border">
48
- <h2 class="text-base font-semibold">Sign in for comments</h2>
70
+ <h2 class="text-medium font-semibold">Sign in for comments</h2>
49
71
  <Button variant="ghost" size="icon" onclick={onClose} aria-label="Close" class="h-7 w-7 text-muted-foreground">&#215;</Button>
50
72
  </div>
51
73
  <div class="p-4 space-y-3">
74
+ {#if error}<Alert.Root variant="destructive"><Alert.Description>{error}</Alert.Description></Alert.Root>{/if}
75
+ <p class="text-sm text-muted-foreground leading-relaxed">
76
+ Leave comments for other users to see and respond, and react to! Storyboard comments use Discussions as a back-end and require a GitHub PAT to be enabled.
77
+ </p>
52
78
  <p class="text-sm text-muted-foreground leading-relaxed">
53
- Create a <a class="text-primary underline" href="https://github.com/settings/personal-access-tokens/new" target="_blank" rel="noopener">GitHub Fine-Grained Personal Access Token</a> with <b>Discussions</b> read/write scope.
79
+ Create a <a class="text-primary underline" href={tokenCreateUrl} target="_blank" rel="noopener">GitHub Fine-Grained Personal Access Token</a> with the settings below to get started:
54
80
  </p>
81
+ <div class="px-3 py-2 bg-muted border border-border rounded text-xs text-muted-foreground leading-relaxed">
82
+ <div class="mb-1"><strong class="text-foreground">Fine-grained Personal Access Token</strong></div>
83
+ <div>Owner: <code class="px-1 bg-background rounded font-mono text-foreground">{repoOwner}</code></div>
84
+ <div>Expiration: <code class="px-1 bg-background rounded font-mono text-foreground">366 days</code> (recommended)</div>
85
+ <div>Repository access: <code class="px-1 bg-background rounded font-mono text-foreground">Only select repositories &gt; {repoSlug}</code></div>
86
+ <div>Permissions: <code class="px-1 bg-background rounded font-mono text-foreground">Repositories > Discussions > Access: Read and Write</code></div>
87
+ </div>
55
88
  <div class="space-y-1">
56
89
  <Label for="sb-auth-token-input">Personal Access Token</Label>
57
- <Input id="sb-auth-token-input" type="password" placeholder="github_pat_\u2026 or ghp_\u2026" autocomplete="off" spellcheck="false" class="font-mono" bind:value={token} bind:this={inputEl} onkeydown={handleKeydown} />
58
- </div>
59
- <div class="px-3 py-2 bg-muted border border-border rounded text-xs text-muted-foreground leading-relaxed">
60
- <div class="mb-1"><strong class="text-foreground">Fine-grained</strong> (recommended): <code class="px-1 bg-background rounded font-mono text-foreground">Discussions: Read and write</code></div>
61
- <div><strong class="text-foreground">Classic</strong>: <code class="px-1 bg-background rounded font-mono text-foreground">repo</code></div>
90
+ <Input id="sb-auth-token-input" type="password" placeholder="github_pat_\u2026 or ghp_\u2026" autocomplete="off" spellcheck="false" class="font-mono" bind:value={token} bind:ref={inputEl} onkeydown={handleKeydown} />
62
91
  </div>
63
- {#if error}<Alert.Root variant="destructive"><Alert.Description>{error}</Alert.Description></Alert.Root>{/if}
64
92
  {#if user}
65
93
  <div class="flex items-center py-1 gap-3">
66
94
  <Avatar.Root class="h-10 w-10"><Avatar.Image src={user.avatarUrl} alt={user.login} /><Avatar.Fallback>{user.login[0]?.toUpperCase()}</Avatar.Fallback></Avatar.Root>
67
95
  <div class="text-sm"><span class="text-foreground">{user.login}</span><span class="block text-xs text-success mt-0.5">&#10003; Signed in</span></div>
68
96
  </div>
69
97
  {/if}
98
+ <Alert.Root variant="warning" class="bg-amber-100 border-amber-300">
99
+ <Alert.Description class="text-amber-700">
100
+ ⚠️ Comments are an experimental feature and may be unstable.
101
+ </Alert.Description>
102
+ </Alert.Root>
70
103
  </div>
71
104
  <div class="flex items-center justify-end px-4 py-3 border-t border-border gap-2">
72
105
  <Button variant="outline" size="sm" onclick={onClose}>Cancel</Button>
@@ -7,15 +7,19 @@
7
7
  import { mount, unmount } from 'svelte'
8
8
  import AuthModal from './AuthModal.svelte'
9
9
  import { getCachedUser, clearToken } from '../auth.js'
10
+ import './comment-layout.css'
10
11
 
11
12
  const MODAL_ID = 'sb-auth-modal'
12
13
 
13
14
  /**
14
15
  * Open the auth modal. Returns a promise that resolves with the user info
15
16
  * on successful sign-in, or null if cancelled.
17
+ * @param {{ initialError?: string|null }} [options]
16
18
  * @returns {Promise<{ login: string, avatarUrl: string }|null>}
17
19
  */
18
- export function openAuthModal() {
20
+ export function openAuthModal(options = {}) {
21
+ const { initialError = null } = options
22
+
19
23
  return new Promise((resolve) => {
20
24
  const existing = document.getElementById(MODAL_ID)
21
25
  if (existing) existing.remove()
@@ -55,6 +59,7 @@ export function openAuthModal() {
55
59
  instance = mount(AuthModal, {
56
60
  target: backdrop,
57
61
  props: {
62
+ initialError,
58
63
  onDone: (user) => {
59
64
  cleanup()
60
65
  resolve(user)
@@ -28,7 +28,7 @@
28
28
  /* Comment pin */
29
29
  .sb-comment-pin {
30
30
  position: absolute;
31
- border: 3px solid hsl(var(--pin-hue, 140), 50%, 38%);
31
+ border: 3px solid hsl(var(--sb--pin-hue, 140), 50%, 38%);
32
32
  border-radius: 50%;
33
33
  z-index: 100000;
34
34
  width: 32px; height: 32px;
@@ -38,21 +38,21 @@
38
38
  pointer-events: auto;
39
39
  overflow: hidden;
40
40
  box-shadow: 0 2px 6px rgba(0,0,0,0.2);
41
- background: var(--color-popover, #fff);
41
+ background: var(--sb--color-popover, #fff);
42
42
  }
43
43
  .sb-comment-pin:active { cursor: grabbing; }
44
44
  .sb-pin-img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 50%; }
45
45
  .sb-comment-pin[data-resolved="true"] {
46
- border-color: var(--color-muted-foreground, #848d97);
46
+ border-color: var(--sb--color-muted-foreground, #848d97);
47
47
  opacity: 0.5;
48
48
  }
49
49
  .sb-comment-pin-pending {
50
- border-color: var(--color-muted-foreground, #848d97) !important;
50
+ border-color: var(--sb--color-muted-foreground, #848d97) !important;
51
51
  opacity: 0.6;
52
52
  animation: sb-pin-pulse 1.2s ease-in-out infinite;
53
53
  }
54
54
  .sb-comment-pin-failed {
55
- border-color: var(--color-destructive, #d1242f) !important;
55
+ border-color: var(--sb--color-destructive, #d1242f) !important;
56
56
  cursor: pointer;
57
57
  animation: sb-pin-shake 0.4s ease-in-out;
58
58
  }
@@ -71,9 +71,9 @@
71
71
  position: absolute;
72
72
  width: 280px;
73
73
  z-index: 100001;
74
- background: var(--color-popover, #fff);
75
- color: var(--color-popover-foreground, #1f2328);
76
- border: 1px solid var(--color-border, #d1d5db);
74
+ background: var(--sb--color-popover, #fff);
75
+ color: var(--sb--color-popover-foreground, #1f2328);
76
+ border: 1px solid var(--sb--color-border, #d1d5db);
77
77
  border-radius: 0.5rem;
78
78
  box-shadow: 0 8px 24px rgba(0,0,0,0.15);
79
79
  overflow: hidden;
@@ -86,9 +86,9 @@
86
86
  max-height: 480px;
87
87
  z-index: 100001;
88
88
  scrollbar-width: none;
89
- background: var(--color-popover, #fff);
90
- color: var(--color-popover-foreground, #1f2328);
91
- border: 1px solid var(--color-border, #d1d5db);
89
+ background: var(--sb--color-popover, #fff);
90
+ color: var(--sb--color-popover-foreground, #1f2328);
91
+ border: 1px solid var(--sb--color-border, #d1d5db);
92
92
  border-radius: 0.5rem;
93
93
  box-shadow: 0 8px 24px rgba(0,0,0,0.15);
94
94
  overflow: hidden;
@@ -110,8 +110,8 @@
110
110
  z-index: 99998;
111
111
  width: 420px;
112
112
  max-width: 90vw;
113
- background: var(--color-background, #fff);
114
- color: var(--color-foreground, #1f2328);
113
+ background: var(--sb--color-background, #fff);
114
+ color: var(--sb--color-foreground, #1f2328);
115
115
  }
116
116
 
117
117
  /* Drawer animation */
@@ -126,8 +126,8 @@
126
126
  position: fixed;
127
127
  bottom: 12px; left: 50%; transform: translateX(-50%);
128
128
  z-index: 99999;
129
- background: var(--color-popover, #fff);
130
- color: var(--color-popover-foreground, #1f2328);
129
+ background: var(--sb--color-popover, #fff);
130
+ color: var(--sb--color-popover-foreground, #1f2328);
131
131
  padding: 6px 16px; border-radius: 8px; font-size: 13px;
132
132
  line-height: 1.4; backdrop-filter: blur(12px);
133
133
  pointer-events: none;
@@ -10,6 +10,7 @@ import { mount, unmount } from 'svelte'
10
10
  import CommentWindowComponent from './CommentWindow.svelte'
11
11
  import { getCachedUser } from '../auth.js'
12
12
  import { saveDraft, replyDraftKey } from '../commentDrafts.js'
13
+ import './comment-layout.css'
13
14
 
14
15
  // Track the currently open window so only one is open at a time
15
16
  let activeWindow = null
@@ -20,6 +21,7 @@ let activeWindow = null
20
21
  * @param {object} comment - The parsed comment object (with meta, text, replies, reactionGroups, author, etc.)
21
22
  * @param {object} discussion - The discussion object (id needed for replies)
22
23
  * @param {object} [callbacks] - Optional callbacks
24
+ * @param {(xPct: number, yPct: number) => { left: string, top: string }} [callbacks.getAnchorPosition] - Resolve coordinates to overlay position
23
25
  * @param {() => void} [callbacks.onClose] - Called when window is closed
24
26
  * @param {() => void} [callbacks.onMove] - Called after comment is moved (for re-rendering pins)
25
27
  * @returns {{ el: HTMLElement, destroy: () => void }}
@@ -32,8 +34,11 @@ export function showCommentWindow(container, comment, discussion, callbacks = {}
32
34
 
33
35
  const user = getCachedUser()
34
36
  const win = document.createElement('div')
37
+ const anchor = callbacks.getAnchorPosition
38
+ ? callbacks.getAnchorPosition(comment.meta?.x ?? 0, comment.meta?.y ?? 0)
39
+ : { left: `${comment.meta?.x ?? 0}%`, top: `${comment.meta?.y ?? 0}%` }
35
40
  win.className = 'sb-comment-window absolute'
36
- win.style.cssText = `z-index:100001;width:360px;max-height:480px;left:${comment.meta?.x ?? 0}%;top:${comment.meta?.y ?? 0}%;transform:translate(12px,-50%)`
41
+ win.style.cssText = `z-index:100001;width:360px;max-height:480px;left:${anchor.left};top:${anchor.top};transform:translate(12px,-50%)`
37
42
 
38
43
  // Stop click from propagating to overlay
39
44
  win.addEventListener('click', (e) => e.stopPropagation())
@@ -7,77 +7,77 @@
7
7
 
8
8
  /* --- Light theme (default) --- */
9
9
  :root {
10
- --sb-bg: #ffffff;
11
- --sb-bg-inset: #f6f8fa;
12
- --sb-bg-muted: #f3f4f6;
13
- --sb-border: #d1d5db;
14
- --sb-border-muted: #e5e7eb;
15
- --sb-fg: #1f2328;
16
- --sb-fg-muted: #656d76;
17
- --sb-fg-accent: #0969da;
18
- --sb-fg-success: #1a7f37;
19
- --sb-fg-danger: #d1242f;
20
- --sb-btn-success: #1a7f37;
10
+ --sb--bg: #ffffff;
11
+ --sb--bg-inset: #f6f8fa;
12
+ --sb--bg-muted: #f3f4f6;
13
+ --sb--border: #d1d5db;
14
+ --sb--border-muted: #e5e7eb;
15
+ --sb--fg: #1f2328;
16
+ --sb--fg-muted: #656d76;
17
+ --sb--fg-accent: #0969da;
18
+ --sb--fg-success: #1a7f37;
19
+ --sb--fg-danger: #d1242f;
20
+ --sb--btn-success: #1a7f37;
21
21
  }
22
22
 
23
23
  /* --- Dark theme (any dark-* variant) --- */
24
24
  [data-sb-theme^="dark"] {
25
- --sb-bg: #161b22;
26
- --sb-bg-inset: #0d1117;
27
- --sb-bg-muted: #21262d;
28
- --sb-border: #30363d;
29
- --sb-border-muted: #21262d;
30
- --sb-fg: #e6edf3;
31
- --sb-fg-muted: #8b949e;
32
- --sb-fg-accent: #58a6ff;
33
- --sb-fg-success: #3fb950;
34
- --sb-fg-danger: #f85149;
35
- --sb-btn-success: #238636;
25
+ --sb--bg: #161b22;
26
+ --sb--bg-inset: #0d1117;
27
+ --sb--bg-muted: #21262d;
28
+ --sb--border: #30363d;
29
+ --sb--border-muted: #21262d;
30
+ --sb--fg: #e6edf3;
31
+ --sb--fg-muted: #8b949e;
32
+ --sb--fg-accent: #58a6ff;
33
+ --sb--fg-success: #3fb950;
34
+ --sb--fg-danger: #f85149;
35
+ --sb--btn-success: #238636;
36
36
  }
37
37
 
38
38
  /* --- Semantic utility classes (supplement Tachyons) --- */
39
- .sb-bg { background-color: var(--sb-bg); }
40
- .sb-bg-inset { background-color: var(--sb-bg-inset); }
41
- .sb-bg-muted { background-color: var(--sb-bg-muted); }
42
- .sb-b-default { border-color: var(--sb-border); }
43
- .sb-b-muted { border-color: var(--sb-border-muted); }
44
- .sb-fg { color: var(--sb-fg); }
45
- .sb-fg-muted { color: var(--sb-fg-muted); }
46
- .sb-fg-accent { color: var(--sb-fg-accent); }
47
- .sb-fg-success { color: var(--sb-fg-success); }
48
- .sb-fg-danger { color: var(--sb-fg-danger); }
49
- .sb-btn-success { background-color: var(--sb-btn-success); color: #fff; }
39
+ .sb-bg { background-color: var(--sb--bg); }
40
+ .sb-bg-inset { background-color: var(--sb--bg-inset); }
41
+ .sb-bg-muted { background-color: var(--sb--bg-muted); }
42
+ .sb-b-default { border-color: var(--sb--border); }
43
+ .sb-b-muted { border-color: var(--sb--border-muted); }
44
+ .sb-fg { color: var(--sb--fg); }
45
+ .sb-fg-muted { color: var(--sb--fg-muted); }
46
+ .sb-fg-accent { color: var(--sb--fg-accent); }
47
+ .sb-fg-success { color: var(--sb--fg-success); }
48
+ .sb-fg-danger { color: var(--sb--fg-danger); }
49
+ .sb-btn-success { background-color: var(--sb--btn-success); color: #fff; }
50
50
  .sb-btn-success:hover { filter: brightness(1.15); }
51
- .sb-btn-cancel { background-color: var(--sb-bg-muted); border: 1px solid var(--sb-border); color: var(--sb-fg); }
52
- .sb-btn-cancel:hover { background-color: var(--sb-border-muted); }
51
+ .sb-btn-cancel { background-color: var(--sb--bg-muted); border: 1px solid var(--sb--border); color: var(--sb--fg); }
52
+ .sb-btn-cancel:hover { background-color: var(--sb--border-muted); }
53
53
 
54
54
  .sb-input {
55
- background: var(--sb-bg-inset);
56
- border: 1px solid var(--sb-border);
57
- color: var(--sb-fg);
55
+ background: var(--sb--bg-inset);
56
+ border: 1px solid var(--sb--border);
57
+ color: var(--sb--fg);
58
58
  outline: none;
59
59
  }
60
60
  .sb-input:focus {
61
- border-color: var(--sb-fg-accent);
62
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--sb-fg-accent) 15%, transparent);
61
+ border-color: var(--sb--fg-accent);
62
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--sb--fg-accent) 15%, transparent);
63
63
  }
64
- .sb-input::placeholder { color: var(--sb-fg-muted); }
64
+ .sb-input::placeholder { color: var(--sb--fg-muted); }
65
65
 
66
66
  .sb-shadow { box-shadow: 0 8px 24px rgba(0,0,0,0.15), 0 2px 6px rgba(0,0,0,0.1); }
67
67
  [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); }
68
68
 
69
69
  .sb-pill {
70
- border: 1px solid var(--sb-border);
70
+ border: 1px solid var(--sb--border);
71
71
  background: none;
72
- color: var(--sb-fg-muted);
72
+ color: var(--sb--fg-muted);
73
73
  cursor: pointer;
74
74
  transition: border-color 100ms, background 100ms;
75
75
  }
76
- .sb-pill:hover { border-color: var(--sb-fg-accent); }
76
+ .sb-pill:hover { border-color: var(--sb--fg-accent); }
77
77
  .sb-pill-active {
78
- border-color: color-mix(in srgb, var(--sb-fg-accent) 40%, transparent);
79
- background: color-mix(in srgb, var(--sb-fg-accent) 10%, transparent);
80
- color: var(--sb-fg-accent);
78
+ border-color: color-mix(in srgb, var(--sb--fg-accent) 40%, transparent);
79
+ background: color-mix(in srgb, var(--sb--fg-accent) 10%, transparent);
80
+ color: var(--sb--fg-accent);
81
81
  }
82
82
 
83
83
  /* --- Comment mode cursor --- */
@@ -162,15 +162,15 @@
162
162
  .sb-reaction-btn { width: 28px; height: 28px; transition: background 100ms; }
163
163
  .sb-reaction-btn-active {
164
164
  width: 28px; height: 28px;
165
- background: color-mix(in srgb, var(--sb-fg-accent) 10%, transparent);
166
- box-shadow: inset 0 0 0 1px var(--sb-fg-accent);
165
+ background: color-mix(in srgb, var(--sb--fg-accent) 10%, transparent);
166
+ box-shadow: inset 0 0 0 1px var(--sb--fg-accent);
167
167
  transition: background 100ms;
168
168
  }
169
169
 
170
170
  /* Banner */
171
171
  .sb-banner {
172
172
  bottom: 12px; left: 50%; transform: translateX(-50%);
173
- z-index: 99999; background: var(--sb-bg); color: var(--sb-fg);
173
+ z-index: 99999; background: var(--sb--bg); color: var(--sb--fg);
174
174
  padding: 6px 16px; border-radius: 8px; font-size: 13px;
175
175
  line-height: 1.4; backdrop-filter: blur(12px);
176
176
  pointer-events: none;
@@ -183,7 +183,7 @@
183
183
 
184
184
  /* Pin */
185
185
  .sb-comment-pin {
186
- border: 3px solid hsl(var(--pin-hue, 140), 50%, 38%);
186
+ border: 3px solid hsl(var(--sb--pin-hue, 140), 50%, 38%);
187
187
  z-index: 100000; width: 32px; height: 32px;
188
188
  margin-left: -16px; margin-top: -16px;
189
189
  transition: transform 100ms ease-in-out;
@@ -193,16 +193,16 @@
193
193
  .sb-comment-pin:active { cursor: grabbing; }
194
194
  .sb-pin-img { width: 100%; height: 100%; object-fit: cover; }
195
195
  .sb-comment-pin[data-resolved="true"] {
196
- border-color: var(--sb-fg-muted);
196
+ border-color: var(--sb--fg-muted);
197
197
  opacity: 0.5;
198
198
  }
199
199
  .sb-comment-pin-pending {
200
- border-color: var(--sb-fg-muted) !important;
200
+ border-color: var(--sb--fg-muted) !important;
201
201
  opacity: 0.6;
202
202
  animation: sb-pin-pulse 1.2s ease-in-out infinite;
203
203
  }
204
204
  .sb-comment-pin-failed {
205
- border-color: var(--sb-fg-danger) !important;
205
+ border-color: var(--sb--fg-danger) !important;
206
206
  cursor: pointer;
207
207
  animation: sb-pin-shake 0.4s ease-in-out;
208
208
  }
@@ -218,14 +218,14 @@
218
218
 
219
219
  /* Error alert */
220
220
  .sb-error-alert {
221
- background: color-mix(in srgb, var(--sb-fg-danger) 10%, transparent);
222
- border: 1px solid color-mix(in srgb, var(--sb-fg-danger) 30%, transparent);
221
+ background: color-mix(in srgb, var(--sb--fg-danger) 10%, transparent);
222
+ border: 1px solid color-mix(in srgb, var(--sb--fg-danger) 30%, transparent);
223
223
  }
224
224
 
225
225
  /* Resolved badge */
226
226
  .sb-badge-resolved {
227
227
  font-size: 10px;
228
- background: color-mix(in srgb, var(--sb-fg-success) 10%, transparent);
228
+ background: color-mix(in srgb, var(--sb--fg-success) 10%, transparent);
229
229
  }
230
230
 
231
231
  /* Drawer button */
@@ -10,6 +10,8 @@ import { mount, unmount } from 'svelte'
10
10
  import CommentsDrawerComponent from './CommentsDrawer.svelte'
11
11
  import { isAuthenticated } from '../auth.js'
12
12
  import { setCommentMode } from '../commentMode.js'
13
+ import './comment-layout.css'
14
+ import './comments.css'
13
15
 
14
16
  let activeDrawer = null
15
17
 
@@ -9,6 +9,7 @@ import { mount, unmount } from 'svelte'
9
9
  import ComposerComponent from './Composer.svelte'
10
10
  import { getCachedUser } from '../auth.js'
11
11
  import { saveDraft, clearDraft, composerDraftKey } from '../commentDrafts.js'
12
+ import './comment-layout.css'
12
13
 
13
14
  /**
14
15
  * Show the comment composer at a given position within a container.
@@ -17,6 +18,7 @@ import { saveDraft, clearDraft, composerDraftKey } from '../commentDrafts.js'
17
18
  * @param {number} yPct - Y coordinate as percentage of container height
18
19
  * @param {string} route - Current route path
19
20
  * @param {object} [callbacks] - Optional callbacks
21
+ * @param {(xPct: number, yPct: number) => { left: string, top: string }} [callbacks.getAnchorPosition] - Resolve coordinates to overlay position
20
22
  * @param {() => void} [callbacks.onCancel] - Called when composer is dismissed
21
23
  * @param {(text: string) => void} [callbacks.onSubmitOptimistic] - Called with text for optimistic submission
22
24
  * @returns {{ el: HTMLElement, destroy: () => void }}
@@ -39,8 +41,11 @@ export function showComposer(container, xPct, yPct, route, callbacks = {}) {
39
41
  const draftKey = composerDraftKey(route)
40
42
 
41
43
  function applyPosition() {
42
- composer.style.left = `${pos.x}%`
43
- composer.style.top = `${pos.y}%`
44
+ const anchor = callbacks.getAnchorPosition
45
+ ? callbacks.getAnchorPosition(pos.x, pos.y)
46
+ : { left: `${pos.x}%`, top: `${pos.y}%` }
47
+ composer.style.left = anchor.left
48
+ composer.style.top = anchor.top
44
49
  composer.style.transform = 'translate(12px, -50%)'
45
50
 
46
51
  requestAnimationFrame(() => {