@dfosco/storyboard 0.5.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commandpalette.config.json +152 -0
- package/dist/storyboard-ui.css +1 -0
- package/dist/storyboard-ui.js +21328 -0
- package/dist/storyboard-ui.js.map +1 -0
- package/dist/tailwind.css +2 -0
- package/dist/tiny-canvas.css +1 -0
- package/dist/tiny-canvas.js +389 -0
- package/package.json +121 -0
- package/paste.config.json +67 -0
- package/scaffold/AGENTS.md +432 -0
- package/scaffold/agents/prompt-agent.agent.md +181 -0
- package/scaffold/agents/terminal-agent.agent.md +351 -0
- package/scaffold/codex/config.toml +246 -0
- package/scaffold/deploy.yml +103 -0
- package/scaffold/githooks/pre-push +114 -0
- package/scaffold/gitignore +64 -0
- package/scaffold/manifest.json +56 -0
- package/scaffold/preview.yml +181 -0
- package/scaffold/scripts/link.sh +26 -0
- package/scaffold/scripts/unlink.sh +10 -0
- package/scaffold/skills/agent-browser/SKILL.md +260 -0
- package/scaffold/skills/canvas/SKILL.md +364 -0
- package/scaffold/skills/create/SKILL.md +501 -0
- package/scaffold/skills/ship/SKILL.md +237 -0
- package/scaffold/skills/storyboard/SKILL.md +360 -0
- package/scaffold/skills/update-storyboard/SKILL.md +16 -0
- package/scaffold/skills/update-storyboard/update-storyboard-packages.sh +26 -0
- package/scaffold/skills/vitest/GENERATION.md +5 -0
- package/scaffold/skills/vitest/SKILL.md +52 -0
- package/scaffold/skills/vitest/references/advanced-environments.md +264 -0
- package/scaffold/skills/vitest/references/advanced-projects.md +300 -0
- package/scaffold/skills/vitest/references/advanced-type-testing.md +237 -0
- package/scaffold/skills/vitest/references/advanced-vi.md +249 -0
- package/scaffold/skills/vitest/references/core-cli.md +166 -0
- package/scaffold/skills/vitest/references/core-config.md +174 -0
- package/scaffold/skills/vitest/references/core-describe.md +193 -0
- package/scaffold/skills/vitest/references/core-expect.md +219 -0
- package/scaffold/skills/vitest/references/core-hooks.md +244 -0
- package/scaffold/skills/vitest/references/core-test-api.md +233 -0
- package/scaffold/skills/vitest/references/features-concurrency.md +250 -0
- package/scaffold/skills/vitest/references/features-context.md +238 -0
- package/scaffold/skills/vitest/references/features-coverage.md +207 -0
- package/scaffold/skills/vitest/references/features-filtering.md +211 -0
- package/scaffold/skills/vitest/references/features-mocking.md +265 -0
- package/scaffold/skills/vitest/references/features-snapshots.md +207 -0
- package/scaffold/skills/worktree/SKILL.md +93 -0
- package/scaffold/storyboard.config.json +44 -0
- package/src/canvas/Canvas.jsx +78 -0
- package/src/canvas/Draggable.jsx +235 -0
- package/src/canvas/index.d.ts +41 -0
- package/src/canvas/index.js +6 -0
- package/src/canvas/style.css +118 -0
- package/src/canvas/useResetCanvas.js +17 -0
- package/src/canvas/utils.js +136 -0
- package/src/core/assets/fonts/IoskeleyMono-Bold.woff2 +0 -0
- package/src/core/assets/fonts/IoskeleyMono-Italic.woff2 +0 -0
- package/src/core/assets/fonts/IoskeleyMono-Medium.woff2 +0 -0
- package/src/core/assets/fonts/IoskeleyMono-Regular.woff2 +0 -0
- package/src/core/assets/fonts/IoskeleyMono-SemiBold.woff2 +0 -0
- package/src/core/autosync/server.js +714 -0
- package/src/core/autosync/server.test.js +158 -0
- package/src/core/canvas/__tests__/agent-integration.test.js +596 -0
- package/src/core/canvas/__tests__/helpers/browser.js +95 -0
- package/src/core/canvas/__tests__/helpers/canvas-api.js +129 -0
- package/src/core/canvas/__tests__/helpers/perf.js +118 -0
- package/src/core/canvas/__tests__/helpers/setup.js +176 -0
- package/src/core/canvas/__tests__/helpers/tmux.js +130 -0
- package/src/core/canvas/__tests__/helpers/transcript.js +132 -0
- package/src/core/canvas/__tests__/terminal-integration.test.js +177 -0
- package/src/core/canvas/collision.js +292 -0
- package/src/core/canvas/collision.test.js +371 -0
- package/src/core/canvas/compact.js +83 -0
- package/src/core/canvas/deriveCanvasId.test.js +40 -0
- package/src/core/canvas/githubEmbeds.js +527 -0
- package/src/core/canvas/githubEmbeds.test.js +302 -0
- package/src/core/canvas/hot-pool.js +766 -0
- package/src/core/canvas/identity.js +107 -0
- package/src/core/canvas/identity.test.js +100 -0
- package/src/core/canvas/materializer.js +259 -0
- package/src/core/canvas/materializer.test.js +356 -0
- package/src/core/canvas/selectedWidgets.js +270 -0
- package/src/core/canvas/selectedWidgets.test.js +321 -0
- package/src/core/canvas/server.js +3134 -0
- package/src/core/canvas/server.test.js +379 -0
- package/src/core/canvas/terminal-config.js +330 -0
- package/src/core/canvas/terminal-registry.js +465 -0
- package/src/core/canvas/terminal-server.js +1436 -0
- package/src/core/canvas/writeGuard.js +53 -0
- package/src/core/cli/agent.js +85 -0
- package/src/core/cli/branch.js +386 -0
- package/src/core/cli/canvasAdd.js +241 -0
- package/src/core/cli/canvasBatch.js +98 -0
- package/src/core/cli/canvasBounds.js +160 -0
- package/src/core/cli/canvasRead.js +236 -0
- package/src/core/cli/canvasUpdate.js +179 -0
- package/src/core/cli/code.js +67 -0
- package/src/core/cli/compact.js +62 -0
- package/src/core/cli/create.js +674 -0
- package/src/core/cli/dev-helpers.js +53 -0
- package/src/core/cli/dev-helpers.test.js +53 -0
- package/src/core/cli/dev.js +430 -0
- package/src/core/cli/exit.js +38 -0
- package/src/core/cli/flags.js +174 -0
- package/src/core/cli/flags.test.js +155 -0
- package/src/core/cli/index.js +233 -0
- package/src/core/cli/intro.js +37 -0
- package/src/core/cli/proxy.js +319 -0
- package/src/core/cli/proxy.test.js +63 -0
- package/src/core/cli/schemas.js +223 -0
- package/src/core/cli/server.js +192 -0
- package/src/core/cli/serverUrl.js +61 -0
- package/src/core/cli/sessions.js +459 -0
- package/src/core/cli/setup.js +404 -0
- package/src/core/cli/terminal-commands.js +287 -0
- package/src/core/cli/terminal-messaging.js +231 -0
- package/src/core/cli/terminal-welcome.js +515 -0
- package/src/core/cli/updateVersion.js +124 -0
- package/src/core/comments/api.js +284 -0
- package/src/core/comments/api.test.js +282 -0
- package/src/core/comments/auth.js +151 -0
- package/src/core/comments/auth.test.js +167 -0
- package/src/core/comments/commentCache.js +109 -0
- package/src/core/comments/commentCache.test.js +48 -0
- package/src/core/comments/commentDrafts.js +68 -0
- package/src/core/comments/commentMode.js +63 -0
- package/src/core/comments/commentMode.test.js +90 -0
- package/src/core/comments/config.js +47 -0
- package/src/core/comments/config.test.js +77 -0
- package/src/core/comments/graphql.js +65 -0
- package/src/core/comments/graphql.test.js +95 -0
- package/src/core/comments/index.js +42 -0
- package/src/core/comments/metadata.js +52 -0
- package/src/core/comments/metadata.test.js +110 -0
- package/src/core/comments/queries.js +245 -0
- package/src/core/comments/ui/AuthModal.jsx +114 -0
- package/src/core/comments/ui/CommentOverlay.js +52 -0
- package/src/core/comments/ui/CommentWindow.jsx +329 -0
- package/src/core/comments/ui/CommentsDrawer.jsx +102 -0
- package/src/core/comments/ui/Composer.jsx +64 -0
- package/src/core/comments/ui/authModal.js +66 -0
- package/src/core/comments/ui/authModal.test.js +76 -0
- package/src/core/comments/ui/comment-cursor-dark.svg +1 -0
- package/src/core/comments/ui/comment-cursor.svg +1 -0
- package/src/core/comments/ui/comment-layout.css +142 -0
- package/src/core/comments/ui/commentWindow.js +121 -0
- package/src/core/comments/ui/comments.css +242 -0
- package/src/core/comments/ui/commentsDrawer.js +84 -0
- package/src/core/comments/ui/composer.js +136 -0
- package/src/core/comments/ui/index.js +14 -0
- package/src/core/comments/ui/mount.js +687 -0
- package/src/core/comments/ui/mount.test.js +336 -0
- package/src/core/data/dotPath.js +53 -0
- package/src/core/data/dotPath.test.js +114 -0
- package/src/core/data/loader.js +409 -0
- package/src/core/data/loader.test.js +599 -0
- package/src/core/data/viewfinder.js +363 -0
- package/src/core/data/viewfinder.test.js +456 -0
- package/src/core/devtools/devtools-consumer.js +28 -0
- package/src/core/devtools/devtools.js +144 -0
- package/src/core/devtools/devtools.test.js +75 -0
- package/src/core/devtools/sceneDebug.js +112 -0
- package/src/core/devtools/sceneDebug.test.js +141 -0
- package/src/core/index.js +124 -0
- package/src/core/inspector/fiberWalker.js +239 -0
- package/src/core/inspector/highlighter.js +275 -0
- package/src/core/inspector/mouseMode.js +259 -0
- package/src/core/lib/components/ui/alert/alert-action.jsx +11 -0
- package/src/core/lib/components/ui/alert/alert-description.jsx +11 -0
- package/src/core/lib/components/ui/alert/alert-title.jsx +11 -0
- package/src/core/lib/components/ui/alert/alert.jsx +25 -0
- package/src/core/lib/components/ui/alert/index.js +17 -0
- package/src/core/lib/components/ui/avatar/avatar-badge.jsx +22 -0
- package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +18 -0
- package/src/core/lib/components/ui/avatar/avatar-group-count.jsx +19 -0
- package/src/core/lib/components/ui/avatar/avatar-group.jsx +19 -0
- package/src/core/lib/components/ui/avatar/avatar-image.jsx +15 -0
- package/src/core/lib/components/ui/avatar/avatar.jsx +19 -0
- package/src/core/lib/components/ui/avatar/index.js +22 -0
- package/src/core/lib/components/ui/badge/badge.jsx +31 -0
- package/src/core/lib/components/ui/badge/index.js +2 -0
- package/src/core/lib/components/ui/button/button.jsx +100 -0
- package/src/core/lib/components/ui/button/index.js +12 -0
- package/src/core/lib/components/ui/card/card-action.jsx +11 -0
- package/src/core/lib/components/ui/card/card-content.jsx +11 -0
- package/src/core/lib/components/ui/card/card-description.jsx +11 -0
- package/src/core/lib/components/ui/card/card-footer.jsx +11 -0
- package/src/core/lib/components/ui/card/card-header.jsx +19 -0
- package/src/core/lib/components/ui/card/card-title.jsx +11 -0
- package/src/core/lib/components/ui/card/card.jsx +17 -0
- package/src/core/lib/components/ui/card/index.js +25 -0
- package/src/core/lib/components/ui/checkbox/checkbox.jsx +29 -0
- package/src/core/lib/components/ui/checkbox/index.js +6 -0
- package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +7 -0
- package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +7 -0
- package/src/core/lib/components/ui/collapsible/collapsible.jsx +7 -0
- package/src/core/lib/components/ui/collapsible/index.js +13 -0
- package/src/core/lib/components/ui/dialog/dialog-close.jsx +7 -0
- package/src/core/lib/components/ui/dialog/dialog-content.jsx +34 -0
- package/src/core/lib/components/ui/dialog/dialog-description.jsx +15 -0
- package/src/core/lib/components/ui/dialog/dialog-footer.jsx +23 -0
- package/src/core/lib/components/ui/dialog/dialog-header.jsx +11 -0
- package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +15 -0
- package/src/core/lib/components/ui/dialog/dialog-portal.jsx +4 -0
- package/src/core/lib/components/ui/dialog/dialog-title.jsx +15 -0
- package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +7 -0
- package/src/core/lib/components/ui/dialog/dialog.jsx +4 -0
- package/src/core/lib/components/ui/dialog/index.js +34 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +8 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +30 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +22 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +16 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +7 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +20 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-label.jsx +17 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +4 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +7 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +29 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +15 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.jsx +16 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +15 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +23 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +4 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +7 -0
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +4 -0
- package/src/core/lib/components/ui/dropdown-menu/index.js +54 -0
- package/src/core/lib/components/ui/input/index.js +7 -0
- package/src/core/lib/components/ui/input/input.jsx +19 -0
- package/src/core/lib/components/ui/label/index.js +7 -0
- package/src/core/lib/components/ui/label/label.jsx +19 -0
- package/src/core/lib/components/ui/panel/index.js +24 -0
- package/src/core/lib/components/ui/panel/panel-body.jsx +11 -0
- package/src/core/lib/components/ui/panel/panel-close.jsx +16 -0
- package/src/core/lib/components/ui/panel/panel-content.jsx +29 -0
- package/src/core/lib/components/ui/panel/panel-footer.jsx +11 -0
- package/src/core/lib/components/ui/panel/panel-header.jsx +11 -0
- package/src/core/lib/components/ui/panel/panel-title.jsx +12 -0
- package/src/core/lib/components/ui/panel/panel.jsx +4 -0
- package/src/core/lib/components/ui/popover/index.js +28 -0
- package/src/core/lib/components/ui/popover/popover-close.jsx +7 -0
- package/src/core/lib/components/ui/popover/popover-content.jsx +22 -0
- package/src/core/lib/components/ui/popover/popover-description.jsx +11 -0
- package/src/core/lib/components/ui/popover/popover-header.jsx +11 -0
- package/src/core/lib/components/ui/popover/popover-portal.jsx +4 -0
- package/src/core/lib/components/ui/popover/popover-title.jsx +11 -0
- package/src/core/lib/components/ui/popover/popover-trigger.jsx +8 -0
- package/src/core/lib/components/ui/popover/popover.jsx +4 -0
- package/src/core/lib/components/ui/searchable-list.jsx +160 -0
- package/src/core/lib/components/ui/select/index.js +37 -0
- package/src/core/lib/components/ui/select/select-content.jsx +30 -0
- package/src/core/lib/components/ui/select/select-group-heading.jsx +17 -0
- package/src/core/lib/components/ui/select/select-group.jsx +15 -0
- package/src/core/lib/components/ui/select/select-item.jsx +26 -0
- package/src/core/lib/components/ui/select/select-label.jsx +11 -0
- package/src/core/lib/components/ui/select/select-portal.jsx +4 -0
- package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +18 -0
- package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +18 -0
- package/src/core/lib/components/ui/select/select-separator.jsx +15 -0
- package/src/core/lib/components/ui/select/select-trigger.jsx +25 -0
- package/src/core/lib/components/ui/select/select.jsx +4 -0
- package/src/core/lib/components/ui/separator/index.js +7 -0
- package/src/core/lib/components/ui/separator/separator.jsx +22 -0
- package/src/core/lib/components/ui/sheet/index.js +34 -0
- package/src/core/lib/components/ui/sheet/sheet-close.jsx +7 -0
- package/src/core/lib/components/ui/sheet/sheet-content.jsx +35 -0
- package/src/core/lib/components/ui/sheet/sheet-description.jsx +15 -0
- package/src/core/lib/components/ui/sheet/sheet-footer.jsx +11 -0
- package/src/core/lib/components/ui/sheet/sheet-header.jsx +11 -0
- package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +15 -0
- package/src/core/lib/components/ui/sheet/sheet-portal.jsx +4 -0
- package/src/core/lib/components/ui/sheet/sheet-title.jsx +15 -0
- package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +7 -0
- package/src/core/lib/components/ui/sheet/sheet.jsx +4 -0
- package/src/core/lib/components/ui/textarea/index.js +7 -0
- package/src/core/lib/components/ui/textarea/textarea.jsx +18 -0
- package/src/core/lib/components/ui/toggle/index.js +8 -0
- package/src/core/lib/components/ui/toggle/toggle.jsx +36 -0
- package/src/core/lib/components/ui/toggle-group/index.js +10 -0
- package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +29 -0
- package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +43 -0
- package/src/core/lib/components/ui/tooltip/index.js +3 -0
- package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +21 -0
- package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +23 -0
- package/src/core/lib/components/ui/tooltip/tooltip.jsx +11 -0
- package/src/core/lib/components/ui/trigger-button/index.js +6 -0
- package/src/core/lib/components/ui/trigger-button/trigger-button.css +38 -0
- package/src/core/lib/components/ui/trigger-button/trigger-button.jsx +63 -0
- package/src/core/lib/utils/index.js +6 -0
- package/src/core/logger/devLogger.js +238 -0
- package/src/core/logger/devLogger.test.js +193 -0
- package/src/core/modes/modes.css +98 -0
- package/src/core/modes/modes.js +492 -0
- package/src/core/modes/modes.test.js +562 -0
- package/src/core/mountStoryboardCore.js +478 -0
- package/src/core/rename-watcher/config.json +23 -0
- package/src/core/rename-watcher/watcher.js +531 -0
- package/src/core/scaffold.js +100 -0
- package/src/core/server/index.js +391 -0
- package/src/core/session/bodyClasses.js +128 -0
- package/src/core/session/bodyClasses.test.js +192 -0
- package/src/core/session/hashSubscribe.js +19 -0
- package/src/core/session/hashSubscribe.test.js +62 -0
- package/src/core/session/hideMode.js +424 -0
- package/src/core/session/hideMode.test.js +268 -0
- package/src/core/session/interceptHideParams.js +35 -0
- package/src/core/session/interceptHideParams.test.js +90 -0
- package/src/core/session/localStorage.js +134 -0
- package/src/core/session/localStorage.test.js +148 -0
- package/src/core/session/session.js +76 -0
- package/src/core/session/session.test.js +91 -0
- package/src/core/stores/canvasConfig.js +134 -0
- package/src/core/stores/canvasConfig.test.js +120 -0
- package/src/core/stores/commandActions.js +284 -0
- package/src/core/stores/commandPaletteConfig.js +31 -0
- package/src/core/stores/configSchema.js +232 -0
- package/src/core/stores/configSchema.test.js +72 -0
- package/src/core/stores/configStore.js +161 -0
- package/src/core/stores/customerModeConfig.js +30 -0
- package/src/core/stores/featureFlags.js +127 -0
- package/src/core/stores/paletteProviders.js +360 -0
- package/src/core/stores/paletteProviders.test.js +186 -0
- package/src/core/stores/plugins.js +40 -0
- package/src/core/stores/plugins.test.js +68 -0
- package/src/core/stores/recentArtifacts.js +68 -0
- package/src/core/stores/recentArtifacts.test.js +71 -0
- package/src/core/stores/sidePanelStore.ts +143 -0
- package/src/core/stores/themeStore.ts +291 -0
- package/src/core/stores/toolRegistry.js +227 -0
- package/src/core/stores/toolStateStore.js +183 -0
- package/src/core/stores/toolStateStore.test.js +220 -0
- package/src/core/stores/toolbarConfigStore.js +165 -0
- package/src/core/stores/uiConfig.js +64 -0
- package/src/core/stores/uiConfig.test.js +63 -0
- package/src/core/styles/tailwind.css +204 -0
- package/src/core/tools/handlers/autosync.js +12 -0
- package/src/core/tools/handlers/canvasAddWidget.js +11 -0
- package/src/core/tools/handlers/canvasAgents.js +20 -0
- package/src/core/tools/handlers/canvasToolbar.js +56 -0
- package/src/core/tools/handlers/commandPalette.js +9 -0
- package/src/core/tools/handlers/comments.js +16 -0
- package/src/core/tools/handlers/create.js +39 -0
- package/src/core/tools/handlers/devtools.js +122 -0
- package/src/core/tools/handlers/devtools.test.js +87 -0
- package/src/core/tools/handlers/featureFlags.js +21 -0
- package/src/core/tools/handlers/flows.js +68 -0
- package/src/core/tools/handlers/hideChrome.js +9 -0
- package/src/core/tools/handlers/hideToolbars.js +25 -0
- package/src/core/tools/handlers/inspector.js +19 -0
- package/src/core/tools/handlers/paletteTheme.js +35 -0
- package/src/core/tools/handlers/theme.js +9 -0
- package/src/core/tools/registry.js +26 -0
- package/src/core/tools/surfaces/canvasToolbar.js +10 -0
- package/src/core/tools/surfaces/commandList.js +10 -0
- package/src/core/tools/surfaces/mainToolbar.js +11 -0
- package/src/core/tools/surfaces/registry.js +19 -0
- package/src/core/ui/ActionMenuButton.jsx +114 -0
- package/src/core/ui/AutosyncMenuButton.css +67 -0
- package/src/core/ui/AutosyncMenuButton.jsx +242 -0
- package/src/core/ui/BranchSelect.jsx +29 -0
- package/src/core/ui/BranchSelect.module.css +30 -0
- package/src/core/ui/CanvasAgentsMenu.jsx +89 -0
- package/src/core/ui/CanvasCreateMenu.jsx +611 -0
- package/src/core/ui/CanvasSnap.css +27 -0
- package/src/core/ui/CanvasSnap.jsx +51 -0
- package/src/core/ui/CanvasUndoRedo.css +36 -0
- package/src/core/ui/CanvasUndoRedo.jsx +62 -0
- package/src/core/ui/CanvasZoomControl.css +53 -0
- package/src/core/ui/CanvasZoomControl.jsx +49 -0
- package/src/core/ui/CanvasZoomToFit.css +18 -0
- package/src/core/ui/CanvasZoomToFit.jsx +26 -0
- package/src/core/ui/CommandMenu.css +8 -0
- package/src/core/ui/CommandMenu.jsx +287 -0
- package/src/core/ui/CommandPalette.jsx +35 -0
- package/src/core/ui/CommandPaletteTrigger.jsx +25 -0
- package/src/core/ui/CommentsMenuButton.jsx +40 -0
- package/src/core/ui/CoreUIBar.css +47 -0
- package/src/core/ui/CoreUIBar.jsx +905 -0
- package/src/core/ui/CreateMenuButton.jsx +117 -0
- package/src/core/ui/HideChromeTrigger.jsx +48 -0
- package/src/core/ui/Icon.jsx +279 -0
- package/src/core/ui/InspectorPanel.css +109 -0
- package/src/core/ui/InspectorPanel.jsx +632 -0
- package/src/core/ui/PwaInstallBanner.css +42 -0
- package/src/core/ui/PwaInstallBanner.jsx +124 -0
- package/src/core/ui/SidePanel.jsx +261 -0
- package/src/core/ui/ThemeMenuButton.jsx +139 -0
- package/src/core/ui/core-ui-colors.css +129 -0
- package/src/core/ui/design-modes.ts +7 -0
- package/src/core/ui/sidepanel.css +301 -0
- package/src/core/ui/viewfinder.ts +7 -0
- package/src/core/ui-entry.js +30 -0
- package/src/core/utils/fuzzySearch.js +117 -0
- package/src/core/utils/fuzzySearch.test.js +119 -0
- package/src/core/utils/mobileViewport.js +57 -0
- package/src/core/utils/mobileViewport.test.js +68 -0
- package/src/core/utils/prodMode.js +38 -0
- package/src/core/utils/smoothCorners.js +20 -0
- package/src/core/vite/docs-handler.js +155 -0
- package/src/core/vite/server-plugin.js +797 -0
- package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +260 -0
- package/src/core/workshop/features/createCanvas/index.js +14 -0
- package/src/core/workshop/features/createFlow/CreateFlowForm.jsx +334 -0
- package/src/core/workshop/features/createFlow/index.js +19 -0
- package/src/core/workshop/features/createFlow/server.js +663 -0
- package/src/core/workshop/features/createPage/CreatePageForm.jsx +304 -0
- package/src/core/workshop/features/createPage/index.js +11 -0
- package/src/core/workshop/features/createPrototype/CreatePrototypeForm.jsx +289 -0
- package/src/core/workshop/features/createPrototype/index.js +19 -0
- package/src/core/workshop/features/createPrototype/server.js +433 -0
- package/src/core/workshop/features/createStory/CreateStoryForm.jsx +208 -0
- package/src/core/workshop/features/createStory/index.js +14 -0
- package/src/core/workshop/features/registry-server.js +22 -0
- package/src/core/workshop/features/registry.js +28 -0
- package/src/core/workshop/features/templateIndex.js +155 -0
- package/src/core/workshop/ui/WorkshopPanel.jsx +98 -0
- package/src/core/workshop/ui/mount.ts +6 -0
- package/src/core/worktree/port.js +268 -0
- package/src/core/worktree/port.test.js +222 -0
- package/src/core/worktree/serverRegistry.js +120 -0
- package/src/internals/AuthModal/AuthModal.jsx +132 -0
- package/src/internals/AuthModal/AuthModal.module.css +221 -0
- package/src/internals/BranchBar/BranchBar.jsx +87 -0
- package/src/internals/BranchBar/BranchBar.module.css +247 -0
- package/src/internals/BranchBar/useBranches.js +93 -0
- package/src/internals/BranchBar/useBranches.test.js +68 -0
- package/src/internals/CommandPalette/CommandPalette.jsx +1361 -0
- package/src/internals/CommandPalette/CreateDialog.jsx +219 -0
- package/src/internals/CommandPalette/command-palette.css +180 -0
- package/src/internals/FlowError.module.css +30 -0
- package/src/internals/Icon.jsx +279 -0
- package/src/internals/StoryboardContext.js +3 -0
- package/src/internals/Viewfinder.jsx +1479 -0
- package/src/internals/Viewfinder.module.css +1540 -0
- package/src/internals/Workspace.jsx +7 -0
- package/src/internals/__mocks__/virtual-storyboard-data-index.js +4 -0
- package/src/internals/canvas/CanvasControls.jsx +112 -0
- package/src/internals/canvas/CanvasControls.module.css +135 -0
- package/src/internals/canvas/CanvasPage.bridge.test.jsx +387 -0
- package/src/internals/canvas/CanvasPage.dragdrop.test.jsx +350 -0
- package/src/internals/canvas/CanvasPage.jsx +3092 -0
- package/src/internals/canvas/CanvasPage.module.css +187 -0
- package/src/internals/canvas/CanvasPage.multiselect.test.jsx +358 -0
- package/src/internals/canvas/CanvasToolbar.jsx +73 -0
- package/src/internals/canvas/CanvasToolbar.module.css +92 -0
- package/src/internals/canvas/ComponentErrorBoundary.jsx +50 -0
- package/src/internals/canvas/ConnectorLayer.jsx +208 -0
- package/src/internals/canvas/ConnectorLayer.module.css +129 -0
- package/src/internals/canvas/MarqueeOverlay.jsx +20 -0
- package/src/internals/canvas/PageSelector.jsx +587 -0
- package/src/internals/canvas/PageSelector.module.css +261 -0
- package/src/internals/canvas/PageSelector.test.jsx +113 -0
- package/src/internals/canvas/WebGLContextPool.jsx +292 -0
- package/src/internals/canvas/WebGLContextPool.test.jsx +165 -0
- package/src/internals/canvas/canvasApi.js +164 -0
- package/src/internals/canvas/canvasReloadGuard.js +37 -0
- package/src/internals/canvas/canvasReloadGuard.test.js +27 -0
- package/src/internals/canvas/canvasTheme.js +118 -0
- package/src/internals/canvas/componentIsolate.jsx +165 -0
- package/src/internals/canvas/componentSetIsolate.jsx +257 -0
- package/src/internals/canvas/computeCanvasBounds.test.js +121 -0
- package/src/internals/canvas/connectorGeometry.js +132 -0
- package/src/internals/canvas/hotPoolDevLogs.js +25 -0
- package/src/internals/canvas/textSelection.js +10 -0
- package/src/internals/canvas/textSelection.test.js +26 -0
- package/src/internals/canvas/useCanvas.js +126 -0
- package/src/internals/canvas/useCanvas.test.js +26 -0
- package/src/internals/canvas/useMarqueeSelect.js +213 -0
- package/src/internals/canvas/useMarqueeSelect.test.js +78 -0
- package/src/internals/canvas/useUndoRedo.js +86 -0
- package/src/internals/canvas/useUndoRedo.test.js +231 -0
- package/src/internals/canvas/widgets/CodePenEmbed.jsx +293 -0
- package/src/internals/canvas/widgets/CodePenEmbed.module.css +161 -0
- package/src/internals/canvas/widgets/ComponentSetWidget.jsx +2 -0
- package/src/internals/canvas/widgets/ComponentSetWidget.module.css +89 -0
- package/src/internals/canvas/widgets/ComponentWidget.jsx +14 -0
- package/src/internals/canvas/widgets/ComponentWidget.module.css +0 -0
- package/src/internals/canvas/widgets/CropOverlay.jsx +179 -0
- package/src/internals/canvas/widgets/CropOverlay.module.css +154 -0
- package/src/internals/canvas/widgets/ExpandedPane.jsx +474 -0
- package/src/internals/canvas/widgets/ExpandedPane.module.css +179 -0
- package/src/internals/canvas/widgets/ExpandedPane.test.jsx +240 -0
- package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
- package/src/internals/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
- package/src/internals/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
- package/src/internals/canvas/widgets/FigmaEmbed.jsx +296 -0
- package/src/internals/canvas/widgets/FigmaEmbed.module.css +222 -0
- package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +151 -0
- package/src/internals/canvas/widgets/FrozenTerminalOverlay.module.css +83 -0
- package/src/internals/canvas/widgets/ImageWidget.jsx +287 -0
- package/src/internals/canvas/widgets/ImageWidget.module.css +81 -0
- package/src/internals/canvas/widgets/LinkPreview.jsx +439 -0
- package/src/internals/canvas/widgets/LinkPreview.module.css +585 -0
- package/src/internals/canvas/widgets/LinkPreview.test.jsx +193 -0
- package/src/internals/canvas/widgets/MarkdownBlock.jsx +354 -0
- package/src/internals/canvas/widgets/MarkdownBlock.module.css +377 -0
- package/src/internals/canvas/widgets/MarkdownBlock.test.jsx +92 -0
- package/src/internals/canvas/widgets/PromptWidget.jsx +428 -0
- package/src/internals/canvas/widgets/PromptWidget.module.css +273 -0
- package/src/internals/canvas/widgets/PrototypeEmbed.jsx +463 -0
- package/src/internals/canvas/widgets/PrototypeEmbed.module.css +579 -0
- package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +10 -0
- package/src/internals/canvas/widgets/ResizeHandle.jsx +67 -0
- package/src/internals/canvas/widgets/ResizeHandle.module.css +29 -0
- package/src/internals/canvas/widgets/StickyNote.jsx +92 -0
- package/src/internals/canvas/widgets/StickyNote.module.css +70 -0
- package/src/internals/canvas/widgets/StickyNote.test.jsx +116 -0
- package/src/internals/canvas/widgets/StorySetWidget.jsx +208 -0
- package/src/internals/canvas/widgets/StorySetWidget.module.css +89 -0
- package/src/internals/canvas/widgets/StoryWidget.jsx +334 -0
- package/src/internals/canvas/widgets/StoryWidget.module.css +211 -0
- package/src/internals/canvas/widgets/TerminalReadWidget.jsx +146 -0
- package/src/internals/canvas/widgets/TerminalReadWidget.module.css +94 -0
- package/src/internals/canvas/widgets/TerminalWidget.jsx +704 -0
- package/src/internals/canvas/widgets/TerminalWidget.module.css +444 -0
- package/src/internals/canvas/widgets/TilesWidget.jsx +300 -0
- package/src/internals/canvas/widgets/TilesWidget.module.css +133 -0
- package/src/internals/canvas/widgets/WidgetChrome.jsx +580 -0
- package/src/internals/canvas/widgets/WidgetChrome.module.css +421 -0
- package/src/internals/canvas/widgets/WidgetWrapper.jsx +15 -0
- package/src/internals/canvas/widgets/WidgetWrapper.module.css +25 -0
- package/src/internals/canvas/widgets/codepenUrl.js +75 -0
- package/src/internals/canvas/widgets/codepenUrl.test.js +76 -0
- package/src/internals/canvas/widgets/embedInteraction.test.jsx +173 -0
- package/src/internals/canvas/widgets/embedOverlay.module.css +35 -0
- package/src/internals/canvas/widgets/embedTheme.js +148 -0
- package/src/internals/canvas/widgets/expandUtils.js +559 -0
- package/src/internals/canvas/widgets/expandUtils.test.js +155 -0
- package/src/internals/canvas/widgets/figmaUrl.js +118 -0
- package/src/internals/canvas/widgets/figmaUrl.test.js +139 -0
- package/src/internals/canvas/widgets/githubUrl.js +82 -0
- package/src/internals/canvas/widgets/githubUrl.test.js +74 -0
- package/src/internals/canvas/widgets/iframeDevLogs.js +49 -0
- package/src/internals/canvas/widgets/iframeDevLogs.test.jsx +81 -0
- package/src/internals/canvas/widgets/index.js +42 -0
- package/src/internals/canvas/widgets/pasteRules.js +295 -0
- package/src/internals/canvas/widgets/pasteRules.test.js +474 -0
- package/src/internals/canvas/widgets/snapshotDisplay.test.jsx +211 -0
- package/src/internals/canvas/widgets/tilePool.js +23 -0
- package/src/internals/canvas/widgets/tiles/diagonal-bl.png +0 -0
- package/src/internals/canvas/widgets/tiles/diagonal-br.png +0 -0
- package/src/internals/canvas/widgets/tiles/diagonal-tl.png +0 -0
- package/src/internals/canvas/widgets/tiles/leaf.png +0 -0
- package/src/internals/canvas/widgets/tiles/quarter-tl.png +0 -0
- package/src/internals/canvas/widgets/tiles/quarter-tr.png +0 -0
- package/src/internals/canvas/widgets/tiles/solid-a.png +0 -0
- package/src/internals/canvas/widgets/tiles/solid-b.png +0 -0
- package/src/internals/canvas/widgets/widgetConfig.js +291 -0
- package/src/internals/canvas/widgets/widgetConfig.test.js +68 -0
- package/src/internals/canvas/widgets/widgetIcons.jsx +190 -0
- package/src/internals/canvas/widgets/widgetProps.js +133 -0
- package/src/internals/context/FormContext.js +13 -0
- package/src/internals/context/FormContext.test.js +48 -0
- package/src/internals/context.jsx +481 -0
- package/src/internals/context.test.jsx +296 -0
- package/src/internals/hashPreserver.js +73 -0
- package/src/internals/hashPreserver.test.js +107 -0
- package/src/internals/hooks/useConfig.js +14 -0
- package/src/internals/hooks/useFeatureFlag.js +14 -0
- package/src/internals/hooks/useFlows.js +50 -0
- package/src/internals/hooks/useFlows.test.js +134 -0
- package/src/internals/hooks/useHideMode.js +31 -0
- package/src/internals/hooks/useHideMode.test.js +43 -0
- package/src/internals/hooks/useLocalStorage.js +57 -0
- package/src/internals/hooks/useLocalStorage.test.js +75 -0
- package/src/internals/hooks/useMode.js +43 -0
- package/src/internals/hooks/useObject.js +101 -0
- package/src/internals/hooks/useObject.test.js +74 -0
- package/src/internals/hooks/useOverride.js +84 -0
- package/src/internals/hooks/useOverride.test.js +71 -0
- package/src/internals/hooks/usePrototypeReloadGuard.js +64 -0
- package/src/internals/hooks/useRecord.js +158 -0
- package/src/internals/hooks/useRecord.test.js +221 -0
- package/src/internals/hooks/useScene.js +38 -0
- package/src/internals/hooks/useScene.test.js +66 -0
- package/src/internals/hooks/useSceneData.js +108 -0
- package/src/internals/hooks/useSceneData.test.js +136 -0
- package/src/internals/hooks/useSession.js +4 -0
- package/src/internals/hooks/useSession.test.js +8 -0
- package/src/internals/hooks/useThemeState.js +61 -0
- package/src/internals/hooks/useThemeState.test.js +66 -0
- package/src/internals/hooks/useUndoRedo.js +28 -0
- package/src/internals/hooks/useUndoRedo.test.js +64 -0
- package/src/internals/index.js +58 -0
- package/src/internals/story/ComponentSetPage.jsx +198 -0
- package/src/internals/story/ComponentSetPage.module.css +129 -0
- package/src/internals/story/StoryPage.jsx +147 -0
- package/src/internals/story/StoryPage.module.css +18 -0
- package/src/internals/test-utils.js +45 -0
- package/src/internals/vite/data-plugin.js +1508 -0
- package/src/internals/vite/data-plugin.test.js +1223 -0
- package/src/test-utils.js +44 -0
- package/toolbar.config.json +271 -0
- package/widgets.config.json +1537 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAT authentication for comments.
|
|
3
|
+
*
|
|
4
|
+
* Stores and retrieves the GitHub PAT from localStorage.
|
|
5
|
+
* Provides validation by fetching the authenticated user and
|
|
6
|
+
* verifying the token can access repository discussions.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { getCommentsConfig } from './config.js'
|
|
10
|
+
|
|
11
|
+
const STORAGE_KEY = 'sb-comments-token'
|
|
12
|
+
const USER_KEY = 'sb-comments-user'
|
|
13
|
+
const GITHUB_GRAPHQL_URL = 'https://api.github.com/graphql'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the stored PAT token.
|
|
17
|
+
* @returns {string|null}
|
|
18
|
+
*/
|
|
19
|
+
export function getToken() {
|
|
20
|
+
try {
|
|
21
|
+
return localStorage.getItem(STORAGE_KEY)
|
|
22
|
+
} catch {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Store a PAT token.
|
|
29
|
+
* @param {string} token
|
|
30
|
+
*/
|
|
31
|
+
export function setToken(token) {
|
|
32
|
+
localStorage.setItem(STORAGE_KEY, token)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Remove the stored PAT token and user.
|
|
37
|
+
*/
|
|
38
|
+
export function clearToken() {
|
|
39
|
+
localStorage.removeItem(STORAGE_KEY)
|
|
40
|
+
localStorage.removeItem(USER_KEY)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get the cached authenticated user info.
|
|
45
|
+
* @returns {{ login: string, avatarUrl: string }|null}
|
|
46
|
+
*/
|
|
47
|
+
export function getCachedUser() {
|
|
48
|
+
try {
|
|
49
|
+
const raw = localStorage.getItem(USER_KEY)
|
|
50
|
+
return raw ? JSON.parse(raw) : null
|
|
51
|
+
} catch {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate a PAT by fetching the authenticated user from GitHub,
|
|
58
|
+
* then probing the GraphQL API to verify the token can access
|
|
59
|
+
* the configured repository's discussions.
|
|
60
|
+
*
|
|
61
|
+
* Caches the user in localStorage on success.
|
|
62
|
+
* @param {string} token - GitHub PAT to validate
|
|
63
|
+
* @returns {Promise<{ login: string, avatarUrl: string }>}
|
|
64
|
+
*/
|
|
65
|
+
export async function validateToken(token) {
|
|
66
|
+
// 1. Verify token is a valid GitHub token
|
|
67
|
+
const res = await fetch('https://api.github.com/user', {
|
|
68
|
+
headers: { Authorization: `bearer ${token}` },
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
throw new Error('Invalid token — GitHub returned ' + res.status)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const user = await res.json()
|
|
76
|
+
const scopes = (res.headers.get('x-oauth-scopes') || '').split(',').map(s => s.trim()).filter(Boolean)
|
|
77
|
+
const userInfo = { login: user.login, avatarUrl: user.avatar_url, scopes }
|
|
78
|
+
|
|
79
|
+
// 2. Verify the token can access repository discussions
|
|
80
|
+
await validateTokenPermissions(token)
|
|
81
|
+
|
|
82
|
+
localStorage.setItem(USER_KEY, JSON.stringify(userInfo))
|
|
83
|
+
return userInfo
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Probe the GraphQL API to verify the token has access to the
|
|
88
|
+
* configured repository's discussions. Throws a descriptive error
|
|
89
|
+
* if the token lacks the required scopes.
|
|
90
|
+
* @param {string} token - GitHub PAT to test
|
|
91
|
+
*/
|
|
92
|
+
async function validateTokenPermissions(token) {
|
|
93
|
+
const config = getCommentsConfig()
|
|
94
|
+
if (!config) return // no config = nothing to probe
|
|
95
|
+
|
|
96
|
+
const { owner, name } = config.repo
|
|
97
|
+
if (!owner || !name) return
|
|
98
|
+
|
|
99
|
+
const query = `query { repository(owner: "${owner}", name: "${name}") { id discussionCategories(first: 1) { nodes { id } } } }`
|
|
100
|
+
|
|
101
|
+
const res = await fetch(GITHUB_GRAPHQL_URL, {
|
|
102
|
+
method: 'POST',
|
|
103
|
+
headers: {
|
|
104
|
+
Authorization: `bearer ${token}`,
|
|
105
|
+
'Content-Type': 'application/json',
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({ query }),
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
if (res.status === 401) {
|
|
111
|
+
throw new Error('Token is invalid or expired.')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
throw new Error(`GitHub API error: ${res.status}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const json = await res.json()
|
|
119
|
+
|
|
120
|
+
if (json.errors?.length) {
|
|
121
|
+
const msg = json.errors.map((e) => e.message).join(', ')
|
|
122
|
+
if (msg.includes('not accessible') || msg.includes('insufficient')) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
`Token doesn't have access to ${owner}/${name} discussions. ` +
|
|
125
|
+
'Fine-grained tokens need "Discussions: Read and write". ' +
|
|
126
|
+
'Classic tokens need the "repo" scope.'
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
throw new Error(`GitHub API error: ${msg}`)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (!json.data?.repository) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Repository ${owner}/${name} not found. Check that the token has access to this repository.`
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!json.data.repository.discussionCategories?.nodes?.length) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`No discussion categories found in ${owner}/${name}. Enable Discussions in the repository settings.`
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check whether the user is currently authenticated.
|
|
147
|
+
* @returns {boolean}
|
|
148
|
+
*/
|
|
149
|
+
export function isAuthenticated() {
|
|
150
|
+
return getToken() !== null
|
|
151
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { getToken, setToken, clearToken, getCachedUser, isAuthenticated, validateToken } from './auth.js'
|
|
2
|
+
import { initCommentsConfig } from './config.js'
|
|
3
|
+
|
|
4
|
+
describe('auth token management', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
localStorage.clear()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('returns null when no token is stored', () => {
|
|
10
|
+
expect(getToken()).toBeNull()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('stores and retrieves a token', () => {
|
|
14
|
+
setToken('ghp_test123')
|
|
15
|
+
expect(getToken()).toBe('ghp_test123')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('clears token and user', () => {
|
|
19
|
+
setToken('ghp_test123')
|
|
20
|
+
localStorage.setItem('sb-comments-user', JSON.stringify({ login: 'test' }))
|
|
21
|
+
clearToken()
|
|
22
|
+
expect(getToken()).toBeNull()
|
|
23
|
+
expect(getCachedUser()).toBeNull()
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('getCachedUser', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
localStorage.clear()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('returns null when no user is cached', () => {
|
|
33
|
+
expect(getCachedUser()).toBeNull()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('returns cached user info', () => {
|
|
37
|
+
const user = { login: 'dfosco', avatarUrl: 'https://example.com/avatar.png' }
|
|
38
|
+
localStorage.setItem('sb-comments-user', JSON.stringify(user))
|
|
39
|
+
expect(getCachedUser()).toEqual(user)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('returns null on invalid JSON', () => {
|
|
43
|
+
localStorage.setItem('sb-comments-user', 'not json')
|
|
44
|
+
expect(getCachedUser()).toBeNull()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('isAuthenticated', () => {
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
localStorage.clear()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('returns false when no token', () => {
|
|
54
|
+
expect(isAuthenticated()).toBe(false)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('returns true when token exists', () => {
|
|
58
|
+
setToken('ghp_abc')
|
|
59
|
+
expect(isAuthenticated()).toBe(true)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('validateToken', () => {
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
localStorage.clear()
|
|
66
|
+
globalThis.fetch = vi.fn()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
afterEach(() => {
|
|
70
|
+
vi.restoreAllMocks()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('throws on invalid token (REST 401)', async () => {
|
|
74
|
+
globalThis.fetch.mockResolvedValueOnce({ ok: false, status: 401 })
|
|
75
|
+
await expect(validateToken('bad_token')).rejects.toThrow('Invalid token')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('validates user and permissions when config is set', async () => {
|
|
79
|
+
initCommentsConfig({
|
|
80
|
+
comments: { discussions: { category: 'Comments' } },
|
|
81
|
+
repository: { owner: 'testorg', name: 'testrepo' },
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
// REST /user succeeds
|
|
85
|
+
globalThis.fetch.mockResolvedValueOnce({
|
|
86
|
+
ok: true,
|
|
87
|
+
json: () => Promise.resolve({ login: 'testuser', avatar_url: 'https://img/avatar' }),
|
|
88
|
+
})
|
|
89
|
+
// GraphQL permissions probe succeeds
|
|
90
|
+
globalThis.fetch.mockResolvedValueOnce({
|
|
91
|
+
ok: true,
|
|
92
|
+
status: 200,
|
|
93
|
+
json: () => Promise.resolve({
|
|
94
|
+
data: {
|
|
95
|
+
repository: {
|
|
96
|
+
id: 'R_123',
|
|
97
|
+
discussionCategories: { nodes: [{ id: 'DC_1' }] },
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
}),
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const user = await validateToken('ghp_valid')
|
|
104
|
+
expect(user).toEqual({ login: 'testuser', avatarUrl: 'https://img/avatar' })
|
|
105
|
+
expect(getCachedUser()).toEqual(user)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('throws descriptive error when token lacks discussion access', async () => {
|
|
109
|
+
initCommentsConfig({
|
|
110
|
+
comments: { discussions: { category: 'Comments' } },
|
|
111
|
+
repository: { owner: 'testorg', name: 'testrepo' },
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// REST /user succeeds
|
|
115
|
+
globalThis.fetch.mockResolvedValueOnce({
|
|
116
|
+
ok: true,
|
|
117
|
+
json: () => Promise.resolve({ login: 'testuser', avatar_url: 'https://img/avatar' }),
|
|
118
|
+
})
|
|
119
|
+
// GraphQL probe fails with permissions error
|
|
120
|
+
globalThis.fetch.mockResolvedValueOnce({
|
|
121
|
+
ok: true,
|
|
122
|
+
status: 200,
|
|
123
|
+
json: () => Promise.resolve({
|
|
124
|
+
errors: [{ message: 'Resource not accessible by personal access token' }],
|
|
125
|
+
}),
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
await expect(validateToken('ghp_no_scope')).rejects.toThrow(
|
|
129
|
+
/doesn't have access.*discussions/i
|
|
130
|
+
)
|
|
131
|
+
// User should NOT be cached on permission failure
|
|
132
|
+
expect(getCachedUser()).toBeNull()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('throws when repository not found', async () => {
|
|
136
|
+
initCommentsConfig({
|
|
137
|
+
comments: { discussions: { category: 'Comments' } },
|
|
138
|
+
repository: { owner: 'testorg', name: 'missing' },
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
globalThis.fetch.mockResolvedValueOnce({
|
|
142
|
+
ok: true,
|
|
143
|
+
json: () => Promise.resolve({ login: 'testuser', avatar_url: 'https://img/avatar' }),
|
|
144
|
+
})
|
|
145
|
+
globalThis.fetch.mockResolvedValueOnce({
|
|
146
|
+
ok: true,
|
|
147
|
+
status: 200,
|
|
148
|
+
json: () => Promise.resolve({ data: { repository: null } }),
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
await expect(validateToken('ghp_valid')).rejects.toThrow(/not found/)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('skips permission check when no comments config', async () => {
|
|
155
|
+
initCommentsConfig({}) // no comments key
|
|
156
|
+
|
|
157
|
+
globalThis.fetch.mockResolvedValueOnce({
|
|
158
|
+
ok: true,
|
|
159
|
+
json: () => Promise.resolve({ login: 'testuser', avatar_url: 'https://img/avatar' }),
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const user = await validateToken('ghp_valid')
|
|
163
|
+
expect(user.login).toBe('testuser')
|
|
164
|
+
// Only 1 fetch call (REST), no GraphQL probe
|
|
165
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1)
|
|
166
|
+
})
|
|
167
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* localStorage cache for comment pin data.
|
|
3
|
+
*
|
|
4
|
+
* Stores lightweight comment listings per route so pins render instantly
|
|
5
|
+
* on repeat visits without hitting the GitHub API.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const CACHE_PREFIX = 'sb-comments:'
|
|
9
|
+
const TTL_MS = 2 * 60 * 1000 // 2 minutes
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get cached comment listing for a route.
|
|
13
|
+
* Returns null if cache is missing or expired.
|
|
14
|
+
* @param {string} route
|
|
15
|
+
* @returns {object|null} - Cached discussion object (with .comments array)
|
|
16
|
+
*/
|
|
17
|
+
export function getCachedComments(route) {
|
|
18
|
+
try {
|
|
19
|
+
const raw = localStorage.getItem(CACHE_PREFIX + route)
|
|
20
|
+
if (!raw) return null
|
|
21
|
+
const entry = JSON.parse(raw)
|
|
22
|
+
if (Date.now() - entry.ts > TTL_MS) {
|
|
23
|
+
localStorage.removeItem(CACHE_PREFIX + route)
|
|
24
|
+
return null
|
|
25
|
+
}
|
|
26
|
+
return entry.data
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Store comment listing in cache for a route.
|
|
34
|
+
* @param {string} route
|
|
35
|
+
* @param {object} data - Discussion object with .comments array
|
|
36
|
+
*/
|
|
37
|
+
export function setCachedComments(route, data) {
|
|
38
|
+
try {
|
|
39
|
+
localStorage.setItem(CACHE_PREFIX + route, JSON.stringify({ ts: Date.now(), data }))
|
|
40
|
+
} catch {
|
|
41
|
+
// localStorage full or unavailable — ignore
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Clear cached comments for a specific route.
|
|
47
|
+
* @param {string} route
|
|
48
|
+
*/
|
|
49
|
+
export function clearCachedComments(route) {
|
|
50
|
+
try {
|
|
51
|
+
localStorage.removeItem(CACHE_PREFIX + route)
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- Pending (failed) comments ---
|
|
58
|
+
|
|
59
|
+
const PENDING_PREFIX = 'sb-pending-comments:'
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Save a pending comment that failed to submit.
|
|
63
|
+
* @param {string} route
|
|
64
|
+
* @param {{ id: string, x: number, y: number, text: string, author: object }} comment
|
|
65
|
+
*/
|
|
66
|
+
export function savePendingComment(route, comment) {
|
|
67
|
+
try {
|
|
68
|
+
const pending = getPendingComments(route)
|
|
69
|
+
// Replace if same id already exists, else append
|
|
70
|
+
const idx = pending.findIndex(c => c.id === comment.id)
|
|
71
|
+
if (idx >= 0) pending[idx] = comment
|
|
72
|
+
else pending.push(comment)
|
|
73
|
+
localStorage.setItem(PENDING_PREFIX + route, JSON.stringify(pending))
|
|
74
|
+
} catch {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get all pending (failed) comments for a route.
|
|
81
|
+
* @param {string} route
|
|
82
|
+
* @returns {Array<{ id: string, x: number, y: number, text: string, author: object }>}
|
|
83
|
+
*/
|
|
84
|
+
export function getPendingComments(route) {
|
|
85
|
+
try {
|
|
86
|
+
const raw = localStorage.getItem(PENDING_PREFIX + route)
|
|
87
|
+
return raw ? JSON.parse(raw) : []
|
|
88
|
+
} catch {
|
|
89
|
+
return []
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Remove a pending comment (after successful retry or dismissal).
|
|
95
|
+
* @param {string} route
|
|
96
|
+
* @param {string} pendingId
|
|
97
|
+
*/
|
|
98
|
+
export function removePendingComment(route, pendingId) {
|
|
99
|
+
try {
|
|
100
|
+
const pending = getPendingComments(route).filter(c => c.id !== pendingId)
|
|
101
|
+
if (pending.length > 0) {
|
|
102
|
+
localStorage.setItem(PENDING_PREFIX + route, JSON.stringify(pending))
|
|
103
|
+
} else {
|
|
104
|
+
localStorage.removeItem(PENDING_PREFIX + route)
|
|
105
|
+
}
|
|
106
|
+
} catch {
|
|
107
|
+
// ignore
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { getCachedComments, setCachedComments, clearCachedComments } from './commentCache.js'
|
|
2
|
+
|
|
3
|
+
describe('commentCache', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
localStorage.clear()
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it('returns null for uncached route', () => {
|
|
9
|
+
expect(getCachedComments('/Overview')).toBeNull()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('stores and retrieves cached comments', () => {
|
|
13
|
+
const data = { id: 'D_1', comments: [{ id: 'C_1', meta: { x: 10, y: 20 } }] }
|
|
14
|
+
setCachedComments('/Overview', data)
|
|
15
|
+
const cached = getCachedComments('/Overview')
|
|
16
|
+
expect(cached).toEqual(data)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('returns null for expired cache', () => {
|
|
20
|
+
const data = { id: 'D_1', comments: [] }
|
|
21
|
+
// Manually write expired entry
|
|
22
|
+
localStorage.setItem('sb-comments:/Overview', JSON.stringify({
|
|
23
|
+
ts: Date.now() - 3 * 60 * 1000, // 3 min ago (exceeds 2-min TTL)
|
|
24
|
+
data,
|
|
25
|
+
}))
|
|
26
|
+
expect(getCachedComments('/Overview')).toBeNull()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('clears cached comments for a route', () => {
|
|
30
|
+
setCachedComments('/Overview', { id: 'D_1', comments: [] })
|
|
31
|
+
clearCachedComments('/Overview')
|
|
32
|
+
expect(getCachedComments('/Overview')).toBeNull()
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('does not affect other routes when clearing', () => {
|
|
36
|
+
const data1 = { id: 'D_1', comments: [] }
|
|
37
|
+
const data2 = { id: 'D_2', comments: [] }
|
|
38
|
+
setCachedComments('/Overview', data1)
|
|
39
|
+
setCachedComments('/Issues', data2)
|
|
40
|
+
clearCachedComments('/Overview')
|
|
41
|
+
expect(getCachedComments('/Issues')).toEqual(data2)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('handles corrupted localStorage gracefully', () => {
|
|
45
|
+
localStorage.setItem('sb-comments:/Overview', 'not-json')
|
|
46
|
+
expect(getCachedComments('/Overview')).toBeNull()
|
|
47
|
+
})
|
|
48
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* localStorage persistence for comment drafts.
|
|
3
|
+
*
|
|
4
|
+
* Saves in-progress comment text so it survives window close/reopen.
|
|
5
|
+
* Each draft is a structured entry with a type discriminator:
|
|
6
|
+
* - { type: 'comment', text } — top-level comment (keyed by route)
|
|
7
|
+
* - { type: 'reply', text } — reply to an existing thread (keyed by comment ID)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const STORAGE_KEY = 'sb-comment-drafts'
|
|
11
|
+
|
|
12
|
+
/** @returns {Record<string, { type: string, text: string }>} */
|
|
13
|
+
function readDrafts() {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}')
|
|
16
|
+
} catch {
|
|
17
|
+
return {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @param {Record<string, { type: string, text: string }>} drafts */
|
|
22
|
+
function writeDrafts(drafts) {
|
|
23
|
+
try {
|
|
24
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(drafts))
|
|
25
|
+
} catch {
|
|
26
|
+
// localStorage full or unavailable
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Save a draft entry.
|
|
32
|
+
* @param {string} key - Storage key (use composerDraftKey / replyDraftKey)
|
|
33
|
+
* @param {{ type: string, text: string }} draft
|
|
34
|
+
*/
|
|
35
|
+
export function saveDraft(key, draft) {
|
|
36
|
+
const drafts = readDrafts()
|
|
37
|
+
drafts[key] = draft
|
|
38
|
+
writeDrafts(drafts)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get a draft entry by key. Returns null if not found.
|
|
43
|
+
* @param {string} key
|
|
44
|
+
* @returns {{ type: string, text: string } | null}
|
|
45
|
+
*/
|
|
46
|
+
export function getDraft(key) {
|
|
47
|
+
return readDrafts()[key] ?? null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Clear a draft entry.
|
|
52
|
+
* @param {string} key
|
|
53
|
+
*/
|
|
54
|
+
export function clearDraft(key) {
|
|
55
|
+
const drafts = readDrafts()
|
|
56
|
+
delete drafts[key]
|
|
57
|
+
writeDrafts(drafts)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Key for a top-level comment draft on a given route. */
|
|
61
|
+
export function composerDraftKey(route) {
|
|
62
|
+
return `comment:${route}`
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Key for a reply draft on a given comment thread. */
|
|
66
|
+
export function replyDraftKey(commentId) {
|
|
67
|
+
return `reply:${commentId}`
|
|
68
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comment mode state — manages the toggle between normal and comment mode.
|
|
3
|
+
*
|
|
4
|
+
* When active: cursor changes to crosshair, comment pins are visible,
|
|
5
|
+
* clicking places a new comment.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { isCommentsEnabled } from './config.js'
|
|
9
|
+
import { isAuthenticated } from './auth.js'
|
|
10
|
+
|
|
11
|
+
let _active = false
|
|
12
|
+
const _listeners = new Set()
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check whether comment mode is currently active.
|
|
16
|
+
*/
|
|
17
|
+
export function isCommentModeActive() {
|
|
18
|
+
return _active
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Toggle comment mode on/off.
|
|
23
|
+
* Only activates if comments are enabled and user is authenticated.
|
|
24
|
+
* @returns {boolean} The new state
|
|
25
|
+
*/
|
|
26
|
+
export function toggleCommentMode() {
|
|
27
|
+
if (!isCommentsEnabled()) {
|
|
28
|
+
console.warn('[storyboard] Comments not enabled — check storyboard.config.json')
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!_active && !isAuthenticated()) {
|
|
33
|
+
console.warn('[storyboard] Sign in first to use comments')
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_active = !_active
|
|
38
|
+
_notify()
|
|
39
|
+
return _active
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Explicitly set comment mode.
|
|
44
|
+
* @param {boolean} active
|
|
45
|
+
*/
|
|
46
|
+
export function setCommentMode(active) {
|
|
47
|
+
_active = active
|
|
48
|
+
_notify()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Subscribe to comment mode changes.
|
|
53
|
+
* @param {(active: boolean) => void} callback
|
|
54
|
+
* @returns {() => void} Unsubscribe function
|
|
55
|
+
*/
|
|
56
|
+
export function subscribeToCommentMode(callback) {
|
|
57
|
+
_listeners.add(callback)
|
|
58
|
+
return () => _listeners.delete(callback)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _notify() {
|
|
62
|
+
for (const cb of _listeners) cb(_active)
|
|
63
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isCommentModeActive,
|
|
3
|
+
toggleCommentMode,
|
|
4
|
+
setCommentMode,
|
|
5
|
+
subscribeToCommentMode,
|
|
6
|
+
} from './commentMode.js'
|
|
7
|
+
import { initCommentsConfig } from './config.js'
|
|
8
|
+
import { setToken, clearToken } from './auth.js'
|
|
9
|
+
|
|
10
|
+
describe('commentMode', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Reset state
|
|
13
|
+
setCommentMode(false)
|
|
14
|
+
clearToken()
|
|
15
|
+
initCommentsConfig(null)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('starts inactive', () => {
|
|
19
|
+
setCommentMode(false)
|
|
20
|
+
expect(isCommentModeActive()).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('setCommentMode activates and deactivates', () => {
|
|
24
|
+
setCommentMode(true)
|
|
25
|
+
expect(isCommentModeActive()).toBe(true)
|
|
26
|
+
setCommentMode(false)
|
|
27
|
+
expect(isCommentModeActive()).toBe(false)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('toggleCommentMode returns false when comments not enabled', () => {
|
|
31
|
+
const result = toggleCommentMode()
|
|
32
|
+
expect(result).toBe(false)
|
|
33
|
+
expect(isCommentModeActive()).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('toggleCommentMode returns false when not authenticated', () => {
|
|
37
|
+
initCommentsConfig({
|
|
38
|
+
comments: { discussions: { category: 'Test' } },
|
|
39
|
+
repository: { owner: 'o', name: 'r' },
|
|
40
|
+
})
|
|
41
|
+
const result = toggleCommentMode()
|
|
42
|
+
expect(result).toBe(false)
|
|
43
|
+
expect(isCommentModeActive()).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('toggleCommentMode activates when enabled and authenticated', () => {
|
|
47
|
+
initCommentsConfig({
|
|
48
|
+
comments: { discussions: { category: 'Test' } },
|
|
49
|
+
repository: { owner: 'o', name: 'r' },
|
|
50
|
+
})
|
|
51
|
+
setToken('ghp_test')
|
|
52
|
+
const result = toggleCommentMode()
|
|
53
|
+
expect(result).toBe(true)
|
|
54
|
+
expect(isCommentModeActive()).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('toggleCommentMode toggles off when active', () => {
|
|
58
|
+
initCommentsConfig({
|
|
59
|
+
comments: { discussions: { category: 'Test' } },
|
|
60
|
+
repository: { owner: 'o', name: 'r' },
|
|
61
|
+
})
|
|
62
|
+
setToken('ghp_test')
|
|
63
|
+
toggleCommentMode() // on
|
|
64
|
+
const result = toggleCommentMode() // off
|
|
65
|
+
expect(result).toBe(false)
|
|
66
|
+
expect(isCommentModeActive()).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('subscribeToCommentMode calls callback on changes', () => {
|
|
70
|
+
const calls = []
|
|
71
|
+
subscribeToCommentMode((active) => calls.push(active))
|
|
72
|
+
|
|
73
|
+
setCommentMode(true)
|
|
74
|
+
setCommentMode(false)
|
|
75
|
+
setCommentMode(true)
|
|
76
|
+
|
|
77
|
+
expect(calls).toEqual([true, false, true])
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('subscribeToCommentMode returns unsubscribe function', () => {
|
|
81
|
+
const calls = []
|
|
82
|
+
const unsub = subscribeToCommentMode((active) => calls.push(active))
|
|
83
|
+
|
|
84
|
+
setCommentMode(true)
|
|
85
|
+
unsub()
|
|
86
|
+
setCommentMode(false)
|
|
87
|
+
|
|
88
|
+
expect(calls).toEqual([true])
|
|
89
|
+
})
|
|
90
|
+
})
|