@grackle-ai/web-components 0.107.2

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 (279) hide show
  1. package/.rush/temp/3ae72563f781afd72723475938136f113846603e.untar.log +10 -0
  2. package/.rush/temp/bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log +10 -0
  3. package/.rush/temp/operation/_phase_build/all.log +18 -0
  4. package/.rush/temp/operation/_phase_build/log-chunks.jsonl +18 -0
  5. package/.rush/temp/operation/_phase_build/state.json +3 -0
  6. package/.rush/temp/operation/_phase_test/all.log +121 -0
  7. package/.rush/temp/operation/_phase_test/log-chunks.jsonl +121 -0
  8. package/.rush/temp/operation/_phase_test/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +938 -0
  10. package/.storybook/main.ts +22 -0
  11. package/.storybook/preview.tsx +30 -0
  12. package/config/rig.json +4 -0
  13. package/config/rush-project.json +12 -0
  14. package/dist/index.css +1 -0
  15. package/dist/index.js +39221 -0
  16. package/eslint.config.cjs +5 -0
  17. package/package.json +83 -0
  18. package/rush-logs/web-components._phase_build.cache.log +4 -0
  19. package/rush-logs/web-components._phase_test.cache.log +4 -0
  20. package/src/components/chat/ChatInput.module.scss +81 -0
  21. package/src/components/chat/ChatInput.stories.tsx +91 -0
  22. package/src/components/chat/ChatInput.tsx +168 -0
  23. package/src/components/chat/index.ts +6 -0
  24. package/src/components/dag/DagView.module.scss +149 -0
  25. package/src/components/dag/DagView.stories.tsx +125 -0
  26. package/src/components/dag/DagView.tsx +109 -0
  27. package/src/components/dag/TaskNode.stories.tsx +133 -0
  28. package/src/components/dag/TaskNode.tsx +40 -0
  29. package/src/components/dag/useDagLayout.ts +139 -0
  30. package/src/components/display/Breadcrumbs.module.scss +71 -0
  31. package/src/components/display/Breadcrumbs.stories.tsx +80 -0
  32. package/src/components/display/Breadcrumbs.tsx +46 -0
  33. package/src/components/display/Button.module.scss +110 -0
  34. package/src/components/display/Button.stories.tsx +88 -0
  35. package/src/components/display/Button.tsx +40 -0
  36. package/src/components/display/ConfirmDialog.module.scss +67 -0
  37. package/src/components/display/ConfirmDialog.stories.tsx +81 -0
  38. package/src/components/display/ConfirmDialog.tsx +88 -0
  39. package/src/components/display/CopyButton.module.scss +41 -0
  40. package/src/components/display/CopyButton.stories.tsx +78 -0
  41. package/src/components/display/CopyButton.tsx +64 -0
  42. package/src/components/display/DemoBanner.module.scss +37 -0
  43. package/src/components/display/DemoBanner.stories.tsx +40 -0
  44. package/src/components/display/DemoBanner.tsx +23 -0
  45. package/src/components/display/EventHoverRow.module.scss +102 -0
  46. package/src/components/display/EventHoverRow.stories.tsx +99 -0
  47. package/src/components/display/EventHoverRow.tsx +154 -0
  48. package/src/components/display/EventRenderer.module.scss +272 -0
  49. package/src/components/display/EventRenderer.stories.tsx +186 -0
  50. package/src/components/display/EventRenderer.tsx +271 -0
  51. package/src/components/display/EventStream.module.scss +93 -0
  52. package/src/components/display/EventStream.stories.tsx +249 -0
  53. package/src/components/display/EventStream.tsx +369 -0
  54. package/src/components/display/FloatingActionBar.module.scss +107 -0
  55. package/src/components/display/FloatingActionBar.stories.tsx +122 -0
  56. package/src/components/display/FloatingActionBar.tsx +119 -0
  57. package/src/components/display/SessionAttemptSelector.module.scss +50 -0
  58. package/src/components/display/SessionAttemptSelector.stories.tsx +78 -0
  59. package/src/components/display/SessionAttemptSelector.tsx +49 -0
  60. package/src/components/display/SessionPicker.module.scss +200 -0
  61. package/src/components/display/SessionPicker.stories.tsx +169 -0
  62. package/src/components/display/SessionPicker.tsx +214 -0
  63. package/src/components/display/Skeleton.module.scss +58 -0
  64. package/src/components/display/Skeleton.stories.tsx +94 -0
  65. package/src/components/display/Skeleton.tsx +127 -0
  66. package/src/components/display/Spinner.module.scss +41 -0
  67. package/src/components/display/Spinner.stories.tsx +66 -0
  68. package/src/components/display/Spinner.tsx +32 -0
  69. package/src/components/display/SplashScreen.module.scss +20 -0
  70. package/src/components/display/SplashScreen.stories.tsx +26 -0
  71. package/src/components/display/SplashScreen.tsx +16 -0
  72. package/src/components/display/SplitButton.module.scss +166 -0
  73. package/src/components/display/SplitButton.stories.tsx +95 -0
  74. package/src/components/display/SplitButton.tsx +128 -0
  75. package/src/components/display/Tooltip.module.scss +84 -0
  76. package/src/components/display/Tooltip.stories.tsx +240 -0
  77. package/src/components/display/Tooltip.tsx +184 -0
  78. package/src/components/display/extractText.test.tsx +48 -0
  79. package/src/components/display/index.ts +20 -0
  80. package/src/components/editable/EditableCheckbox.stories.tsx +54 -0
  81. package/src/components/editable/EditableCheckbox.tsx +39 -0
  82. package/src/components/editable/EditableField.module.scss +135 -0
  83. package/src/components/editable/EditableSelect.tsx +164 -0
  84. package/src/components/editable/EditableTextArea.stories.tsx +50 -0
  85. package/src/components/editable/EditableTextArea.tsx +148 -0
  86. package/src/components/editable/EditableTextField.stories.tsx +62 -0
  87. package/src/components/editable/EditableTextField.tsx +153 -0
  88. package/src/components/editable/EnvironmentSelect.module.scss +17 -0
  89. package/src/components/editable/EnvironmentSelect.stories.tsx +61 -0
  90. package/src/components/editable/EnvironmentSelect.tsx +87 -0
  91. package/src/components/editable/index.ts +13 -0
  92. package/src/components/editable/useEditableField.test.tsx +233 -0
  93. package/src/components/editable/useEditableField.ts +173 -0
  94. package/src/components/index.ts +20 -0
  95. package/src/components/knowledge/KnowledgeDetailPanel.module.scss +162 -0
  96. package/src/components/knowledge/KnowledgeDetailPanel.stories.tsx +208 -0
  97. package/src/components/knowledge/KnowledgeDetailPanel.tsx +122 -0
  98. package/src/components/knowledge/KnowledgeGraph.module.scss +110 -0
  99. package/src/components/knowledge/KnowledgeGraph.stories.tsx +180 -0
  100. package/src/components/knowledge/KnowledgeGraph.tsx +455 -0
  101. package/src/components/knowledge/KnowledgeNav.module.scss +130 -0
  102. package/src/components/knowledge/KnowledgeNav.stories.tsx +108 -0
  103. package/src/components/knowledge/KnowledgeNav.tsx +138 -0
  104. package/src/components/knowledge/index.ts +3 -0
  105. package/src/components/layout/AppNav.module.scss +82 -0
  106. package/src/components/layout/AppNav.stories.tsx +115 -0
  107. package/src/components/layout/AppNav.tsx +133 -0
  108. package/src/components/layout/BottomStatusBar.module.scss +58 -0
  109. package/src/components/layout/BottomStatusBar.stories.tsx +35 -0
  110. package/src/components/layout/BottomStatusBar.tsx +206 -0
  111. package/src/components/layout/Sidebar.module.scss +60 -0
  112. package/src/components/layout/Sidebar.stories.tsx +46 -0
  113. package/src/components/layout/Sidebar.tsx +84 -0
  114. package/src/components/layout/StatusBar.module.scss +108 -0
  115. package/src/components/layout/StatusBar.stories.tsx +119 -0
  116. package/src/components/layout/StatusBar.tsx +70 -0
  117. package/src/components/layout/index.ts +9 -0
  118. package/src/components/lists/EnvironmentNav.module.scss +118 -0
  119. package/src/components/lists/EnvironmentNav.stories.tsx +121 -0
  120. package/src/components/lists/EnvironmentNav.tsx +133 -0
  121. package/src/components/lists/FindingsNav.module.scss +126 -0
  122. package/src/components/lists/FindingsNav.tsx +146 -0
  123. package/src/components/lists/TaskList.module.scss +206 -0
  124. package/src/components/lists/TaskList.stories.tsx +401 -0
  125. package/src/components/lists/TaskList.tsx +509 -0
  126. package/src/components/lists/index.ts +6 -0
  127. package/src/components/lists/listHelpers.tsx +130 -0
  128. package/src/components/notifications/Callout.module.scss +83 -0
  129. package/src/components/notifications/Callout.stories.tsx +81 -0
  130. package/src/components/notifications/Callout.tsx +64 -0
  131. package/src/components/notifications/Toast.module.scss +86 -0
  132. package/src/components/notifications/Toast.stories.tsx +71 -0
  133. package/src/components/notifications/Toast.tsx +51 -0
  134. package/src/components/notifications/ToastContainer.module.scss +23 -0
  135. package/src/components/notifications/ToastContainer.stories.tsx +66 -0
  136. package/src/components/notifications/ToastContainer.tsx +29 -0
  137. package/src/components/notifications/UpdateBanner.stories.tsx +77 -0
  138. package/src/components/notifications/UpdateBanner.test.tsx +64 -0
  139. package/src/components/notifications/UpdateBanner.tsx +44 -0
  140. package/src/components/notifications/index.ts +8 -0
  141. package/src/components/panels/AboutPanel.stories.tsx +70 -0
  142. package/src/components/panels/AboutPanel.tsx +66 -0
  143. package/src/components/panels/AppearancePanel.stories.tsx +45 -0
  144. package/src/components/panels/AppearancePanel.tsx +97 -0
  145. package/src/components/panels/CredentialProvidersPanel.stories.tsx +62 -0
  146. package/src/components/panels/CredentialProvidersPanel.tsx +111 -0
  147. package/src/components/panels/EnvironmentEditPanel.module.scss +170 -0
  148. package/src/components/panels/EnvironmentEditPanel.stories.tsx +206 -0
  149. package/src/components/panels/EnvironmentEditPanel.tsx +785 -0
  150. package/src/components/panels/FindingsPanel.module.scss +94 -0
  151. package/src/components/panels/FindingsPanel.stories.tsx +109 -0
  152. package/src/components/panels/FindingsPanel.tsx +76 -0
  153. package/src/components/panels/KeyboardShortcutsPanel.module.scss +65 -0
  154. package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +40 -0
  155. package/src/components/panels/KeyboardShortcutsPanel.tsx +104 -0
  156. package/src/components/panels/PluginsPanel.tsx +77 -0
  157. package/src/components/panels/SettingsPanel.module.scss +336 -0
  158. package/src/components/panels/TaskActionButtons.module.scss +22 -0
  159. package/src/components/panels/TaskActionButtons.stories.tsx +125 -0
  160. package/src/components/panels/TaskActionButtons.tsx +87 -0
  161. package/src/components/panels/TaskEditPanel.module.scss +202 -0
  162. package/src/components/panels/TaskEditPanel.stories.tsx +75 -0
  163. package/src/components/panels/TaskEditPanel.tsx +328 -0
  164. package/src/components/panels/TaskOverviewPanel.module.scss +236 -0
  165. package/src/components/panels/TaskOverviewPanel.stories.tsx +219 -0
  166. package/src/components/panels/TaskOverviewPanel.tsx +270 -0
  167. package/src/components/panels/TokensPanel.stories.tsx +131 -0
  168. package/src/components/panels/TokensPanel.tsx +143 -0
  169. package/src/components/panels/WorkpadPanel.module.scss +39 -0
  170. package/src/components/panels/WorkpadPanel.stories.tsx +56 -0
  171. package/src/components/panels/WorkpadPanel.tsx +63 -0
  172. package/src/components/panels/index.ts +13 -0
  173. package/src/components/personas/McpToolSelector.module.scss +109 -0
  174. package/src/components/personas/McpToolSelector.stories.tsx +129 -0
  175. package/src/components/personas/McpToolSelector.tsx +180 -0
  176. package/src/components/personas/PersonaManager.module.scss +233 -0
  177. package/src/components/personas/PersonaManager.stories.tsx +139 -0
  178. package/src/components/personas/PersonaManager.tsx +122 -0
  179. package/src/components/schedules/ScheduleManager.module.scss +98 -0
  180. package/src/components/schedules/ScheduleManager.stories.tsx +78 -0
  181. package/src/components/schedules/ScheduleManager.tsx +160 -0
  182. package/src/components/settings/SettingsNav.module.scss +82 -0
  183. package/src/components/settings/SettingsNav.stories.tsx +83 -0
  184. package/src/components/settings/SettingsNav.tsx +104 -0
  185. package/src/components/streams/StreamDetailPanel.module.scss +206 -0
  186. package/src/components/streams/StreamDetailPanel.stories.tsx +132 -0
  187. package/src/components/streams/StreamDetailPanel.tsx +119 -0
  188. package/src/components/streams/StreamList.module.scss +92 -0
  189. package/src/components/streams/StreamList.stories.tsx +99 -0
  190. package/src/components/streams/StreamList.tsx +114 -0
  191. package/src/components/streams/index.ts +10 -0
  192. package/src/components/tools/AgentToolCard.module.scss +118 -0
  193. package/src/components/tools/AgentToolCard.stories.tsx +304 -0
  194. package/src/components/tools/AgentToolCard.tsx +247 -0
  195. package/src/components/tools/FileEditCard.stories.tsx +138 -0
  196. package/src/components/tools/FileEditCard.tsx +160 -0
  197. package/src/components/tools/FileReadCard.stories.tsx +120 -0
  198. package/src/components/tools/FileReadCard.tsx +106 -0
  199. package/src/components/tools/FindingCard.stories.tsx +124 -0
  200. package/src/components/tools/FindingCard.tsx +178 -0
  201. package/src/components/tools/GenericToolCard.stories.tsx +80 -0
  202. package/src/components/tools/GenericToolCard.tsx +111 -0
  203. package/src/components/tools/IpcCard.stories.tsx +129 -0
  204. package/src/components/tools/IpcCard.tsx +178 -0
  205. package/src/components/tools/KnowledgeCard.stories.tsx +112 -0
  206. package/src/components/tools/KnowledgeCard.tsx +165 -0
  207. package/src/components/tools/MetadataCard.stories.tsx +32 -0
  208. package/src/components/tools/MetadataCard.tsx +39 -0
  209. package/src/components/tools/SearchCard.stories.tsx +74 -0
  210. package/src/components/tools/SearchCard.tsx +86 -0
  211. package/src/components/tools/ShellCard.stories.tsx +112 -0
  212. package/src/components/tools/ShellCard.tsx +106 -0
  213. package/src/components/tools/TaskCard.stories.tsx +123 -0
  214. package/src/components/tools/TaskCard.tsx +203 -0
  215. package/src/components/tools/TodoCard.module.scss +131 -0
  216. package/src/components/tools/TodoCard.stories.tsx +202 -0
  217. package/src/components/tools/TodoCard.tsx +200 -0
  218. package/src/components/tools/ToolCard.stories.tsx +177 -0
  219. package/src/components/tools/ToolCard.tsx +60 -0
  220. package/src/components/tools/ToolCardProps.ts +20 -0
  221. package/src/components/tools/ToolSearchCard.stories.tsx +81 -0
  222. package/src/components/tools/ToolSearchCard.tsx +86 -0
  223. package/src/components/tools/WorkpadCard.stories.tsx +106 -0
  224. package/src/components/tools/WorkpadCard.tsx +125 -0
  225. package/src/components/tools/classifyTool.test.ts +44 -0
  226. package/src/components/tools/classifyTool.ts +134 -0
  227. package/src/components/tools/parseDiff.ts +95 -0
  228. package/src/components/tools/parseShellOutput.ts +28 -0
  229. package/src/components/tools/toolCardHelpers.test.ts +53 -0
  230. package/src/components/tools/toolCards.module.scss +234 -0
  231. package/src/components/workspace/WorkspaceBoard.module.scss +238 -0
  232. package/src/components/workspace/WorkspaceBoard.stories.tsx +240 -0
  233. package/src/components/workspace/WorkspaceBoard.tsx +232 -0
  234. package/src/components/workspace/WorkspaceFormFields.module.scss +79 -0
  235. package/src/components/workspace/WorkspaceFormFields.stories.tsx +133 -0
  236. package/src/components/workspace/WorkspaceFormFields.tsx +185 -0
  237. package/src/context/GrackleContext.ts +28 -0
  238. package/src/context/GrackleContextTypes.ts +64 -0
  239. package/src/context/SidebarContext.tsx +53 -0
  240. package/src/context/ThemeContext.tsx +21 -0
  241. package/src/context/ToastContext.tsx +56 -0
  242. package/src/hooks/types.ts +864 -0
  243. package/src/hooks/useEventSelection.test.ts +204 -0
  244. package/src/hooks/useEventSelection.ts +158 -0
  245. package/src/hooks/useSmartScroll.ts +151 -0
  246. package/src/hooks/useTheme.ts +228 -0
  247. package/src/index.ts +210 -0
  248. package/src/mocks/MockGrackleProvider.tsx +1397 -0
  249. package/src/mocks/mockData.ts +1966 -0
  250. package/src/mocks/mockKnowledgeData.ts +294 -0
  251. package/src/scss.d.ts +12 -0
  252. package/src/styles/global.scss +244 -0
  253. package/src/styles/mixins.scss +278 -0
  254. package/src/styles/prism-theme.scss +148 -0
  255. package/src/styles/theme.scss +1102 -0
  256. package/src/test-utils/storybook-decorators.tsx +50 -0
  257. package/src/test-utils/storybook-helpers.ts +262 -0
  258. package/src/themes.ts +142 -0
  259. package/src/utils/boardColumns.ts +141 -0
  260. package/src/utils/breadcrumbs.test.ts +285 -0
  261. package/src/utils/breadcrumbs.ts +222 -0
  262. package/src/utils/dashboard.test.ts +156 -0
  263. package/src/utils/dashboard.ts +195 -0
  264. package/src/utils/eventContent.test.ts +353 -0
  265. package/src/utils/eventContent.ts +209 -0
  266. package/src/utils/findingCategory.ts +33 -0
  267. package/src/utils/format.ts +27 -0
  268. package/src/utils/iconSize.ts +18 -0
  269. package/src/utils/navigation.ts +205 -0
  270. package/src/utils/route-config.test.ts +128 -0
  271. package/src/utils/scrollUtils.test.ts +65 -0
  272. package/src/utils/scrollUtils.ts +49 -0
  273. package/src/utils/sessionEvents.test.ts +302 -0
  274. package/src/utils/sessionEvents.ts +233 -0
  275. package/src/utils/taskStatus.tsx +137 -0
  276. package/src/utils/time.ts +92 -0
  277. package/tsconfig.json +8 -0
  278. package/vite.config.ts +20 -0
  279. package/vitest.config.ts +10 -0
@@ -0,0 +1,46 @@
1
+ import type { JSX } from "react";
2
+ import { ChevronRight } from "lucide-react";
3
+ import { Link } from "react-router";
4
+ import type { BreadcrumbSegment } from "../../utils/breadcrumbs.js";
5
+ import { ICON_SM } from "../../utils/iconSize.js";
6
+ import styles from "./Breadcrumbs.module.scss";
7
+
8
+ /** Props for the Breadcrumbs component. */
9
+ interface BreadcrumbsProps {
10
+ segments: BreadcrumbSegment[];
11
+ }
12
+
13
+ /** Renders a clickable breadcrumb trail from a list of segments. */
14
+ export function Breadcrumbs({ segments }: BreadcrumbsProps): JSX.Element {
15
+ return (
16
+ <nav className={styles.breadcrumbs} aria-label="Breadcrumb" data-testid="breadcrumbs">
17
+ <ol className={styles.list}>
18
+ {segments.map((segment, index) => {
19
+ const isLast = index === segments.length - 1;
20
+ return (
21
+ <li key={index} className={styles.item}>
22
+ {index > 0 && (
23
+ <span className={styles.separator} aria-hidden="true">
24
+ <ChevronRight size={ICON_SM} />
25
+ </span>
26
+ )}
27
+ {segment.url && !isLast ? (
28
+ <Link
29
+ className={styles.link}
30
+ to={segment.url}
31
+ title={segment.label}
32
+ >
33
+ {segment.label}
34
+ </Link>
35
+ ) : (
36
+ <span className={styles.current} aria-current="page" title={segment.label}>
37
+ {segment.label}
38
+ </span>
39
+ )}
40
+ </li>
41
+ );
42
+ })}
43
+ </ol>
44
+ </nav>
45
+ );
46
+ }
@@ -0,0 +1,110 @@
1
+ @use '../../styles/mixins' as *;
2
+
3
+ // =============================================================================
4
+ // Button — shared component with variant × size matrix
5
+ // =============================================================================
6
+
7
+ // --- Base -------------------------------------------------------------------
8
+
9
+ .btn {
10
+ display: inline-flex;
11
+ align-items: center;
12
+ justify-content: center;
13
+ gap: var(--space-xs);
14
+ white-space: nowrap;
15
+ cursor: pointer;
16
+ font-family: var(--font-ui);
17
+ border-radius: var(--radius-md);
18
+ transition: background var(--transition-fast),
19
+ box-shadow var(--transition-fast),
20
+ transform var(--transition-fast),
21
+ border-color var(--transition-fast);
22
+
23
+ &:focus-visible {
24
+ outline: 2px solid var(--ring);
25
+ outline-offset: 2px;
26
+ }
27
+
28
+ &:disabled {
29
+ opacity: 0.64;
30
+ cursor: not-allowed;
31
+ pointer-events: none;
32
+ }
33
+ }
34
+
35
+ // --- Sizes ------------------------------------------------------------------
36
+
37
+ .sm {
38
+ font-size: var(--font-size-xs);
39
+ padding: 2px var(--space-sm);
40
+ font-weight: var(--font-weight-medium);
41
+ }
42
+
43
+ .md {
44
+ font-size: var(--font-size-sm);
45
+ padding: var(--space-xs) var(--space-md);
46
+ font-weight: var(--font-weight-bold);
47
+ }
48
+
49
+ .lg {
50
+ font-size: var(--font-size-sm);
51
+ padding: var(--space-sm) var(--space-lg);
52
+ font-weight: var(--font-weight-bold);
53
+ }
54
+
55
+ // --- Variants ---------------------------------------------------------------
56
+
57
+ .primary {
58
+ background: var(--accent-green);
59
+ border: 1px solid var(--accent-green);
60
+ color: #ffffff;
61
+ box-shadow: var(--shadow-xs);
62
+
63
+ &:hover {
64
+ box-shadow: var(--shadow-glow-green);
65
+ transform: translateY(-1px);
66
+ }
67
+
68
+ &:active {
69
+ transform: translateY(0);
70
+ box-shadow: none;
71
+ }
72
+ }
73
+
74
+ .danger {
75
+ background: var(--accent-red);
76
+ border: 1px solid var(--accent-red);
77
+ color: #ffffff;
78
+ box-shadow: var(--shadow-xs);
79
+
80
+ &:hover {
81
+ box-shadow: var(--shadow-glow-red);
82
+ transform: translateY(-1px);
83
+ }
84
+
85
+ &:active {
86
+ transform: translateY(0);
87
+ box-shadow: none;
88
+ }
89
+ }
90
+
91
+ .outline {
92
+ background: var(--bg-surface);
93
+ border: 1px solid var(--border-input);
94
+ color: var(--accent-green);
95
+ box-shadow: var(--shadow-xs);
96
+
97
+ &:hover {
98
+ background: var(--bg-overlay);
99
+ }
100
+ }
101
+
102
+ .ghost {
103
+ background: none;
104
+ border: 1px solid transparent;
105
+ color: var(--text-secondary);
106
+
107
+ &:hover {
108
+ background: var(--bg-overlay);
109
+ }
110
+ }
@@ -0,0 +1,88 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect, fn } from "@storybook/test";
3
+ import { Button } from "./Button.js";
4
+
5
+ const meta: Meta<typeof Button> = {
6
+ component: Button,
7
+ title: "Primitives/Display/Button",
8
+ tags: ["autodocs"],
9
+ args: {
10
+ children: "Click me",
11
+ onClick: fn(),
12
+ },
13
+ };
14
+ export default meta;
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ /** Default primary button at medium size. */
18
+ export const Default: Story = {
19
+ play: async ({ canvas }) => {
20
+ const button = canvas.getByRole("button", { name: "Click me" });
21
+ await expect(button).toBeInTheDocument();
22
+ await expect(button).toBeEnabled();
23
+ },
24
+ };
25
+
26
+ /** Danger variant for destructive actions. */
27
+ export const Danger: Story = {
28
+ args: {
29
+ variant: "danger",
30
+ children: "Delete",
31
+ },
32
+ play: async ({ canvas }) => {
33
+ const button = canvas.getByRole("button", { name: "Delete" });
34
+ await expect(button).toBeInTheDocument();
35
+ await expect(button.className).toContain("danger");
36
+ },
37
+ };
38
+
39
+ /** Ghost variant for minimal visual weight. */
40
+ export const Ghost: Story = {
41
+ args: {
42
+ variant: "ghost",
43
+ children: "Cancel",
44
+ },
45
+ play: async ({ canvas }) => {
46
+ const button = canvas.getByRole("button", { name: "Cancel" });
47
+ await expect(button).toBeInTheDocument();
48
+ await expect(button.className).toContain("ghost");
49
+ },
50
+ };
51
+
52
+ /** Small size variant. */
53
+ export const Small: Story = {
54
+ args: {
55
+ size: "sm",
56
+ children: "Small",
57
+ },
58
+ play: async ({ canvas }) => {
59
+ const button = canvas.getByRole("button", { name: "Small" });
60
+ await expect(button).toBeInTheDocument();
61
+ await expect(button.className).toContain("sm");
62
+ },
63
+ };
64
+
65
+ /** Large size variant. */
66
+ export const Large: Story = {
67
+ args: {
68
+ size: "lg",
69
+ children: "Large",
70
+ },
71
+ play: async ({ canvas }) => {
72
+ const button = canvas.getByRole("button", { name: "Large" });
73
+ await expect(button).toBeInTheDocument();
74
+ await expect(button.className).toContain("lg");
75
+ },
76
+ };
77
+
78
+ /** Disabled button prevents interaction. */
79
+ export const Disabled: Story = {
80
+ args: {
81
+ disabled: true,
82
+ children: "Disabled",
83
+ },
84
+ play: async ({ canvas }) => {
85
+ const button = canvas.getByRole("button", { name: "Disabled" });
86
+ await expect(button).toBeDisabled();
87
+ },
88
+ };
@@ -0,0 +1,40 @@
1
+ import { forwardRef, type ButtonHTMLAttributes, type JSX } from "react";
2
+ import type React from "react";
3
+ import styles from "./Button.module.scss";
4
+
5
+ /** Visual variant of the button. */
6
+ export type ButtonVariant = "primary" | "danger" | "outline" | "ghost";
7
+
8
+ /** Size of the button. */
9
+ export type ButtonSize = "sm" | "md" | "lg";
10
+
11
+ /** Props for the {@link Button} component. */
12
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
13
+ /** Visual variant. Defaults to `"primary"`. */
14
+ variant?: ButtonVariant;
15
+ /** Size. Defaults to `"md"`. */
16
+ size?: ButtonSize;
17
+ }
18
+
19
+ /**
20
+ * Standardized button with consistent sizing and styling across the app.
21
+ *
22
+ * Uses the existing mixin-based design tokens so colours/radii stay in sync
23
+ * with the rest of the UI.
24
+ */
25
+ export const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>> = forwardRef<HTMLButtonElement, ButtonProps>(
26
+ function Button({ variant = "primary", size = "md", className, children, ...rest }: ButtonProps, ref: React.ForwardedRef<HTMLButtonElement>): JSX.Element {
27
+ const cls = [
28
+ styles.btn,
29
+ styles[variant],
30
+ styles[size],
31
+ className,
32
+ ].filter(Boolean).join(" ");
33
+
34
+ return (
35
+ <button ref={ref} className={cls} {...rest}>
36
+ {children}
37
+ </button>
38
+ );
39
+ },
40
+ );
@@ -0,0 +1,67 @@
1
+ @use '../../styles/mixins' as *;
2
+
3
+ // =============================================================================
4
+ // ConfirmDialog — in-app modal replacing window.confirm()
5
+ // =============================================================================
6
+
7
+ /// Full-screen overlay that dims the background and blocks interaction.
8
+ .overlay {
9
+ position: fixed;
10
+ inset: 0;
11
+ z-index: 1000;
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ background: rgba(0, 0, 0, 0.5);
16
+ }
17
+
18
+ /// Card dialog panel.
19
+ .dialog {
20
+ @include surface-card;
21
+ padding: var(--space-xl);
22
+ min-width: 300px;
23
+ max-width: 440px;
24
+ width: 90%;
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--space-md);
28
+ box-shadow: var(--shadow-lg);
29
+ font-family: var(--font-ui);
30
+ }
31
+
32
+ /// Dialog title.
33
+ .title {
34
+ margin: 0;
35
+ font-size: var(--font-size-lg);
36
+ font-weight: var(--font-weight-bold);
37
+ color: var(--text-primary);
38
+ line-height: 1.3;
39
+ }
40
+
41
+ /// Consequence description below the title.
42
+ .description {
43
+ margin: 0;
44
+ font-size: var(--font-size-sm);
45
+ color: var(--text-secondary);
46
+ line-height: var(--line-height);
47
+ }
48
+
49
+ /// Button row — cancel on left, danger confirm on right.
50
+ .actions {
51
+ display: flex;
52
+ gap: var(--space-sm);
53
+ justify-content: flex-end;
54
+ margin-top: var(--space-xs);
55
+ }
56
+
57
+ .cancelButton {
58
+ @include btn-ghost(var(--text-secondary));
59
+ padding: var(--space-sm) var(--space-lg);
60
+ font-size: var(--font-size-sm);
61
+ font-family: var(--font-ui);
62
+ }
63
+
64
+ .confirmButton {
65
+ @include btn-danger;
66
+ font-size: var(--font-size-sm);
67
+ }
@@ -0,0 +1,81 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect, fn, userEvent } from "@storybook/test";
3
+ import { ConfirmDialog } from "./ConfirmDialog.js";
4
+
5
+ const meta: Meta<typeof ConfirmDialog> = {
6
+ title: "Primitives/Display/ConfirmDialog",
7
+ tags: ["autodocs"],
8
+ component: ConfirmDialog,
9
+ args: {
10
+ isOpen: true,
11
+ title: "Delete Task?",
12
+ description: '"tdel-dismiss-task" will be permanently removed.',
13
+ confirmLabel: "Delete",
14
+ onConfirm: fn(),
15
+ onCancel: fn(),
16
+ },
17
+ };
18
+
19
+ export default meta;
20
+
21
+ type Story = StoryObj<typeof ConfirmDialog>;
22
+
23
+ /** The confirm dialog can be dismissed by clicking Cancel, which calls onCancel. */
24
+ export const DismissViaCancelButton: Story = {
25
+ play: async ({ canvas, args }) => {
26
+ // Dialog is visible with the correct title
27
+ await expect(canvas.getByText("Delete Task?")).toBeInTheDocument();
28
+
29
+ // Description text is shown
30
+ await expect(canvas.getByText(/tdel-dismiss-task/)).toBeInTheDocument();
31
+
32
+ // Cancel button is present and clickable
33
+ const cancelButton = canvas.getByRole("button", { name: "Cancel" });
34
+ await expect(cancelButton).toBeInTheDocument();
35
+ await userEvent.click(cancelButton);
36
+
37
+ // onCancel should have been called
38
+ await expect(args.onCancel).toHaveBeenCalled();
39
+
40
+ // onConfirm should NOT have been called
41
+ await expect(args.onConfirm).not.toHaveBeenCalled();
42
+ },
43
+ };
44
+
45
+ /** The confirm dialog calls onConfirm when the Delete button is clicked. */
46
+ export const ConfirmAction: Story = {
47
+ play: async ({ canvas, args }) => {
48
+ await expect(canvas.getByText("Delete Task?")).toBeInTheDocument();
49
+
50
+ const confirmButton = canvas.getByRole("button", { name: "Delete" });
51
+ await userEvent.click(confirmButton);
52
+
53
+ await expect(args.onConfirm).toHaveBeenCalled();
54
+ await expect(args.onCancel).not.toHaveBeenCalled();
55
+ },
56
+ };
57
+
58
+ /** Pressing Escape calls onCancel to dismiss the dialog. */
59
+ export const EscapeKeyCloses: Story = {
60
+ play: async ({ canvas, args }) => {
61
+ await expect(canvas.getByText("Delete Task?")).toBeInTheDocument();
62
+
63
+ // Press Escape to dismiss
64
+ await userEvent.keyboard("{Escape}");
65
+
66
+ await expect(args.onCancel).toHaveBeenCalled();
67
+ await expect(args.onConfirm).not.toHaveBeenCalled();
68
+ },
69
+ };
70
+
71
+ /** When isOpen is false the dialog renders nothing visible. */
72
+ export const ClosedDialog: Story = {
73
+ args: {
74
+ isOpen: false,
75
+ },
76
+ play: async ({ canvas }) => {
77
+ // The title text should not be in the document when the dialog is closed
78
+ const title = canvas.queryByText("Delete Task?");
79
+ await expect(title).not.toBeInTheDocument();
80
+ },
81
+ };
@@ -0,0 +1,88 @@
1
+ import { useId, type JSX } from "react";
2
+ import { AnimatePresence, motion } from "motion/react";
3
+ import styles from "./ConfirmDialog.module.scss";
4
+
5
+ /** Props for the ConfirmDialog component. */
6
+ interface ConfirmDialogProps {
7
+ /** Whether the dialog is currently visible. */
8
+ isOpen: boolean;
9
+ /** Short, action-oriented title (e.g. "Delete Task?"). */
10
+ title: string;
11
+ /** Consequence description shown below the title. */
12
+ description?: string;
13
+ /** Label for the danger confirm button. Defaults to "Delete". */
14
+ confirmLabel?: string;
15
+ /** Called when the user confirms the destructive action. */
16
+ onConfirm: () => void;
17
+ /** Called when the user cancels or clicks the overlay backdrop. */
18
+ onCancel: () => void;
19
+ }
20
+
21
+ /**
22
+ * Modal confirmation dialog with glass card aesthetic and motion animation.
23
+ *
24
+ * Replaces native `window.confirm()` for destructive actions, providing a
25
+ * styled in-app dialog that matches the dark glass UI.
26
+ */
27
+ export function ConfirmDialog({
28
+ isOpen,
29
+ title,
30
+ description,
31
+ confirmLabel = "Delete",
32
+ onConfirm,
33
+ onCancel,
34
+ }: ConfirmDialogProps): JSX.Element {
35
+ const titleId = useId();
36
+ const descriptionId = useId();
37
+
38
+ return (
39
+ <AnimatePresence>
40
+ {isOpen && (
41
+ <motion.div
42
+ className={styles.overlay}
43
+ initial={{ opacity: 0 }}
44
+ animate={{ opacity: 1 }}
45
+ exit={{ opacity: 0 }}
46
+ transition={{ duration: 0.15 }}
47
+ onClick={onCancel}
48
+ onKeyDown={(e) => { if (e.key === "Escape") onCancel(); }}
49
+ role="dialog"
50
+ aria-modal="true"
51
+ aria-labelledby={titleId}
52
+ aria-describedby={description ? descriptionId : undefined}
53
+ >
54
+ <motion.div
55
+ className={styles.dialog}
56
+ initial={{ opacity: 0, scale: 0.93, y: -10 }}
57
+ animate={{ opacity: 1, scale: 1, y: 0 }}
58
+ exit={{ opacity: 0, scale: 0.93, y: -10 }}
59
+ transition={{ duration: 0.15, ease: [0.16, 1, 0.3, 1] }}
60
+ onClick={(e) => e.stopPropagation()}
61
+ >
62
+ <h3 id={titleId} className={styles.title}>{title}</h3>
63
+ {description && (
64
+ <p id={descriptionId} className={styles.description}>{description}</p>
65
+ )}
66
+ <div className={styles.actions}>
67
+ <button
68
+ type="button"
69
+ className={styles.cancelButton}
70
+ onClick={onCancel}
71
+ autoFocus
72
+ >
73
+ Cancel
74
+ </button>
75
+ <button
76
+ type="button"
77
+ className={styles.confirmButton}
78
+ onClick={onConfirm}
79
+ >
80
+ {confirmLabel}
81
+ </button>
82
+ </div>
83
+ </motion.div>
84
+ </motion.div>
85
+ )}
86
+ </AnimatePresence>
87
+ );
88
+ }
@@ -0,0 +1,41 @@
1
+ @use '../../styles/mixins' as *;
2
+
3
+ // =============================================================================
4
+ // CopyButton — clipboard copy with visual feedback
5
+ // =============================================================================
6
+
7
+ .copyButton {
8
+ position: absolute;
9
+ top: 4px;
10
+ right: 4px;
11
+ z-index: 1;
12
+
13
+ background: none;
14
+ border: none;
15
+ padding: 2px 4px;
16
+ margin: 0;
17
+
18
+ font-size: 14px;
19
+ line-height: 1;
20
+ cursor: pointer;
21
+ border-radius: var(--radius-sm);
22
+ opacity: 0;
23
+ transition: opacity var(--transition-fast),
24
+ background var(--transition-fast);
25
+
26
+ &:hover {
27
+ opacity: 1 !important;
28
+ background: var(--bg-overlay);
29
+ }
30
+
31
+ &:focus-visible {
32
+ opacity: 1 !important;
33
+ outline: 2px solid var(--accent-blue);
34
+ outline-offset: 2px;
35
+ background: var(--bg-overlay);
36
+ }
37
+
38
+ @include mobile {
39
+ opacity: 1;
40
+ }
41
+ }
@@ -0,0 +1,78 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect, userEvent, fn, waitFor } from "@storybook/test";
3
+ import { CopyButton } from "./CopyButton.js";
4
+
5
+ const meta: Meta<typeof CopyButton> = {
6
+ component: CopyButton,
7
+ title: "Primitives/Display/CopyButton",
8
+ tags: ["autodocs"],
9
+ decorators: [
10
+ (Story) => {
11
+ // Mock the clipboard API for Storybook/test environments.
12
+ // navigator.clipboard is a read-only getter in Chromium, so
13
+ // Object.assign fails; use defineProperty to override it.
14
+ Object.defineProperty(navigator, "clipboard", {
15
+ value: { writeText: fn().mockResolvedValue(undefined) },
16
+ writable: true,
17
+ configurable: true,
18
+ });
19
+ return <Story />;
20
+ },
21
+ ],
22
+ };
23
+ export default meta;
24
+ type Story = StoryObj<typeof meta>;
25
+
26
+ export const Default: Story = {
27
+ args: {
28
+ text: "Hello, world!",
29
+ },
30
+ play: async ({ canvas }) => {
31
+ const button = canvas.getByTestId("copy-button");
32
+ await expect(button).toBeInTheDocument();
33
+ await expect(button).toHaveAttribute("aria-label", "Copy to clipboard");
34
+ },
35
+ };
36
+
37
+ export const CopiesCorrectText: Story = {
38
+ name: "Click copies text and shows checkmark",
39
+ args: {
40
+ text: "# Hello\n\nSome **bold** markdown",
41
+ },
42
+ play: async ({ canvas }) => {
43
+ const button = canvas.getByTestId("copy-button");
44
+ await userEvent.click(button);
45
+ // Wait for async clipboard write and state update
46
+ // eslint-disable-next-line @typescript-eslint/unbound-method -- mock fn assigned by decorator
47
+ const writeTextMock = navigator.clipboard.writeText;
48
+ await waitFor(async () => {
49
+ await expect(button).toHaveAttribute("aria-label", "Copied");
50
+ await expect(writeTextMock).toHaveBeenCalledWith("# Hello\n\nSome **bold** markdown");
51
+ });
52
+ },
53
+ };
54
+
55
+ export const CheckmarkReverts: Story = {
56
+ name: "Checkmark reverts after 2 seconds",
57
+ args: {
58
+ text: "revert test",
59
+ },
60
+ play: async ({ canvas }) => {
61
+ const button = canvas.getByTestId("copy-button");
62
+ await userEvent.click(button);
63
+ await waitFor(() => expect(button).toHaveAttribute("aria-label", "Copied"));
64
+ // Wait for the checkmark to revert after COPIED_FEEDBACK_DURATION (2s)
65
+ await waitFor(() => expect(button).toHaveAttribute("aria-label", "Copy to clipboard"), { timeout: 3000 });
66
+ },
67
+ };
68
+
69
+ export const CustomTestId: Story = {
70
+ name: "Custom data-testid",
71
+ args: {
72
+ text: "test",
73
+ "data-testid": "my-custom-copy",
74
+ },
75
+ play: async ({ canvas }) => {
76
+ await expect(canvas.getByTestId("my-custom-copy")).toBeInTheDocument();
77
+ },
78
+ };