@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.
- package/.rush/temp/3ae72563f781afd72723475938136f113846603e.untar.log +10 -0
- package/.rush/temp/bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log +10 -0
- package/.rush/temp/operation/_phase_build/all.log +18 -0
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +18 -0
- package/.rush/temp/operation/_phase_build/state.json +3 -0
- package/.rush/temp/operation/_phase_test/all.log +121 -0
- package/.rush/temp/operation/_phase_test/log-chunks.jsonl +121 -0
- package/.rush/temp/operation/_phase_test/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +938 -0
- package/.storybook/main.ts +22 -0
- package/.storybook/preview.tsx +30 -0
- package/config/rig.json +4 -0
- package/config/rush-project.json +12 -0
- package/dist/index.css +1 -0
- package/dist/index.js +39221 -0
- package/eslint.config.cjs +5 -0
- package/package.json +83 -0
- package/rush-logs/web-components._phase_build.cache.log +4 -0
- package/rush-logs/web-components._phase_test.cache.log +4 -0
- package/src/components/chat/ChatInput.module.scss +81 -0
- package/src/components/chat/ChatInput.stories.tsx +91 -0
- package/src/components/chat/ChatInput.tsx +168 -0
- package/src/components/chat/index.ts +6 -0
- package/src/components/dag/DagView.module.scss +149 -0
- package/src/components/dag/DagView.stories.tsx +125 -0
- package/src/components/dag/DagView.tsx +109 -0
- package/src/components/dag/TaskNode.stories.tsx +133 -0
- package/src/components/dag/TaskNode.tsx +40 -0
- package/src/components/dag/useDagLayout.ts +139 -0
- package/src/components/display/Breadcrumbs.module.scss +71 -0
- package/src/components/display/Breadcrumbs.stories.tsx +80 -0
- package/src/components/display/Breadcrumbs.tsx +46 -0
- package/src/components/display/Button.module.scss +110 -0
- package/src/components/display/Button.stories.tsx +88 -0
- package/src/components/display/Button.tsx +40 -0
- package/src/components/display/ConfirmDialog.module.scss +67 -0
- package/src/components/display/ConfirmDialog.stories.tsx +81 -0
- package/src/components/display/ConfirmDialog.tsx +88 -0
- package/src/components/display/CopyButton.module.scss +41 -0
- package/src/components/display/CopyButton.stories.tsx +78 -0
- package/src/components/display/CopyButton.tsx +64 -0
- package/src/components/display/DemoBanner.module.scss +37 -0
- package/src/components/display/DemoBanner.stories.tsx +40 -0
- package/src/components/display/DemoBanner.tsx +23 -0
- package/src/components/display/EventHoverRow.module.scss +102 -0
- package/src/components/display/EventHoverRow.stories.tsx +99 -0
- package/src/components/display/EventHoverRow.tsx +154 -0
- package/src/components/display/EventRenderer.module.scss +272 -0
- package/src/components/display/EventRenderer.stories.tsx +186 -0
- package/src/components/display/EventRenderer.tsx +271 -0
- package/src/components/display/EventStream.module.scss +93 -0
- package/src/components/display/EventStream.stories.tsx +249 -0
- package/src/components/display/EventStream.tsx +369 -0
- package/src/components/display/FloatingActionBar.module.scss +107 -0
- package/src/components/display/FloatingActionBar.stories.tsx +122 -0
- package/src/components/display/FloatingActionBar.tsx +119 -0
- package/src/components/display/SessionAttemptSelector.module.scss +50 -0
- package/src/components/display/SessionAttemptSelector.stories.tsx +78 -0
- package/src/components/display/SessionAttemptSelector.tsx +49 -0
- package/src/components/display/SessionPicker.module.scss +200 -0
- package/src/components/display/SessionPicker.stories.tsx +169 -0
- package/src/components/display/SessionPicker.tsx +214 -0
- package/src/components/display/Skeleton.module.scss +58 -0
- package/src/components/display/Skeleton.stories.tsx +94 -0
- package/src/components/display/Skeleton.tsx +127 -0
- package/src/components/display/Spinner.module.scss +41 -0
- package/src/components/display/Spinner.stories.tsx +66 -0
- package/src/components/display/Spinner.tsx +32 -0
- package/src/components/display/SplashScreen.module.scss +20 -0
- package/src/components/display/SplashScreen.stories.tsx +26 -0
- package/src/components/display/SplashScreen.tsx +16 -0
- package/src/components/display/SplitButton.module.scss +166 -0
- package/src/components/display/SplitButton.stories.tsx +95 -0
- package/src/components/display/SplitButton.tsx +128 -0
- package/src/components/display/Tooltip.module.scss +84 -0
- package/src/components/display/Tooltip.stories.tsx +240 -0
- package/src/components/display/Tooltip.tsx +184 -0
- package/src/components/display/extractText.test.tsx +48 -0
- package/src/components/display/index.ts +20 -0
- package/src/components/editable/EditableCheckbox.stories.tsx +54 -0
- package/src/components/editable/EditableCheckbox.tsx +39 -0
- package/src/components/editable/EditableField.module.scss +135 -0
- package/src/components/editable/EditableSelect.tsx +164 -0
- package/src/components/editable/EditableTextArea.stories.tsx +50 -0
- package/src/components/editable/EditableTextArea.tsx +148 -0
- package/src/components/editable/EditableTextField.stories.tsx +62 -0
- package/src/components/editable/EditableTextField.tsx +153 -0
- package/src/components/editable/EnvironmentSelect.module.scss +17 -0
- package/src/components/editable/EnvironmentSelect.stories.tsx +61 -0
- package/src/components/editable/EnvironmentSelect.tsx +87 -0
- package/src/components/editable/index.ts +13 -0
- package/src/components/editable/useEditableField.test.tsx +233 -0
- package/src/components/editable/useEditableField.ts +173 -0
- package/src/components/index.ts +20 -0
- package/src/components/knowledge/KnowledgeDetailPanel.module.scss +162 -0
- package/src/components/knowledge/KnowledgeDetailPanel.stories.tsx +208 -0
- package/src/components/knowledge/KnowledgeDetailPanel.tsx +122 -0
- package/src/components/knowledge/KnowledgeGraph.module.scss +110 -0
- package/src/components/knowledge/KnowledgeGraph.stories.tsx +180 -0
- package/src/components/knowledge/KnowledgeGraph.tsx +455 -0
- package/src/components/knowledge/KnowledgeNav.module.scss +130 -0
- package/src/components/knowledge/KnowledgeNav.stories.tsx +108 -0
- package/src/components/knowledge/KnowledgeNav.tsx +138 -0
- package/src/components/knowledge/index.ts +3 -0
- package/src/components/layout/AppNav.module.scss +82 -0
- package/src/components/layout/AppNav.stories.tsx +115 -0
- package/src/components/layout/AppNav.tsx +133 -0
- package/src/components/layout/BottomStatusBar.module.scss +58 -0
- package/src/components/layout/BottomStatusBar.stories.tsx +35 -0
- package/src/components/layout/BottomStatusBar.tsx +206 -0
- package/src/components/layout/Sidebar.module.scss +60 -0
- package/src/components/layout/Sidebar.stories.tsx +46 -0
- package/src/components/layout/Sidebar.tsx +84 -0
- package/src/components/layout/StatusBar.module.scss +108 -0
- package/src/components/layout/StatusBar.stories.tsx +119 -0
- package/src/components/layout/StatusBar.tsx +70 -0
- package/src/components/layout/index.ts +9 -0
- package/src/components/lists/EnvironmentNav.module.scss +118 -0
- package/src/components/lists/EnvironmentNav.stories.tsx +121 -0
- package/src/components/lists/EnvironmentNav.tsx +133 -0
- package/src/components/lists/FindingsNav.module.scss +126 -0
- package/src/components/lists/FindingsNav.tsx +146 -0
- package/src/components/lists/TaskList.module.scss +206 -0
- package/src/components/lists/TaskList.stories.tsx +401 -0
- package/src/components/lists/TaskList.tsx +509 -0
- package/src/components/lists/index.ts +6 -0
- package/src/components/lists/listHelpers.tsx +130 -0
- package/src/components/notifications/Callout.module.scss +83 -0
- package/src/components/notifications/Callout.stories.tsx +81 -0
- package/src/components/notifications/Callout.tsx +64 -0
- package/src/components/notifications/Toast.module.scss +86 -0
- package/src/components/notifications/Toast.stories.tsx +71 -0
- package/src/components/notifications/Toast.tsx +51 -0
- package/src/components/notifications/ToastContainer.module.scss +23 -0
- package/src/components/notifications/ToastContainer.stories.tsx +66 -0
- package/src/components/notifications/ToastContainer.tsx +29 -0
- package/src/components/notifications/UpdateBanner.stories.tsx +77 -0
- package/src/components/notifications/UpdateBanner.test.tsx +64 -0
- package/src/components/notifications/UpdateBanner.tsx +44 -0
- package/src/components/notifications/index.ts +8 -0
- package/src/components/panels/AboutPanel.stories.tsx +70 -0
- package/src/components/panels/AboutPanel.tsx +66 -0
- package/src/components/panels/AppearancePanel.stories.tsx +45 -0
- package/src/components/panels/AppearancePanel.tsx +97 -0
- package/src/components/panels/CredentialProvidersPanel.stories.tsx +62 -0
- package/src/components/panels/CredentialProvidersPanel.tsx +111 -0
- package/src/components/panels/EnvironmentEditPanel.module.scss +170 -0
- package/src/components/panels/EnvironmentEditPanel.stories.tsx +206 -0
- package/src/components/panels/EnvironmentEditPanel.tsx +785 -0
- package/src/components/panels/FindingsPanel.module.scss +94 -0
- package/src/components/panels/FindingsPanel.stories.tsx +109 -0
- package/src/components/panels/FindingsPanel.tsx +76 -0
- package/src/components/panels/KeyboardShortcutsPanel.module.scss +65 -0
- package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +40 -0
- package/src/components/panels/KeyboardShortcutsPanel.tsx +104 -0
- package/src/components/panels/PluginsPanel.tsx +77 -0
- package/src/components/panels/SettingsPanel.module.scss +336 -0
- package/src/components/panels/TaskActionButtons.module.scss +22 -0
- package/src/components/panels/TaskActionButtons.stories.tsx +125 -0
- package/src/components/panels/TaskActionButtons.tsx +87 -0
- package/src/components/panels/TaskEditPanel.module.scss +202 -0
- package/src/components/panels/TaskEditPanel.stories.tsx +75 -0
- package/src/components/panels/TaskEditPanel.tsx +328 -0
- package/src/components/panels/TaskOverviewPanel.module.scss +236 -0
- package/src/components/panels/TaskOverviewPanel.stories.tsx +219 -0
- package/src/components/panels/TaskOverviewPanel.tsx +270 -0
- package/src/components/panels/TokensPanel.stories.tsx +131 -0
- package/src/components/panels/TokensPanel.tsx +143 -0
- package/src/components/panels/WorkpadPanel.module.scss +39 -0
- package/src/components/panels/WorkpadPanel.stories.tsx +56 -0
- package/src/components/panels/WorkpadPanel.tsx +63 -0
- package/src/components/panels/index.ts +13 -0
- package/src/components/personas/McpToolSelector.module.scss +109 -0
- package/src/components/personas/McpToolSelector.stories.tsx +129 -0
- package/src/components/personas/McpToolSelector.tsx +180 -0
- package/src/components/personas/PersonaManager.module.scss +233 -0
- package/src/components/personas/PersonaManager.stories.tsx +139 -0
- package/src/components/personas/PersonaManager.tsx +122 -0
- package/src/components/schedules/ScheduleManager.module.scss +98 -0
- package/src/components/schedules/ScheduleManager.stories.tsx +78 -0
- package/src/components/schedules/ScheduleManager.tsx +160 -0
- package/src/components/settings/SettingsNav.module.scss +82 -0
- package/src/components/settings/SettingsNav.stories.tsx +83 -0
- package/src/components/settings/SettingsNav.tsx +104 -0
- package/src/components/streams/StreamDetailPanel.module.scss +206 -0
- package/src/components/streams/StreamDetailPanel.stories.tsx +132 -0
- package/src/components/streams/StreamDetailPanel.tsx +119 -0
- package/src/components/streams/StreamList.module.scss +92 -0
- package/src/components/streams/StreamList.stories.tsx +99 -0
- package/src/components/streams/StreamList.tsx +114 -0
- package/src/components/streams/index.ts +10 -0
- package/src/components/tools/AgentToolCard.module.scss +118 -0
- package/src/components/tools/AgentToolCard.stories.tsx +304 -0
- package/src/components/tools/AgentToolCard.tsx +247 -0
- package/src/components/tools/FileEditCard.stories.tsx +138 -0
- package/src/components/tools/FileEditCard.tsx +160 -0
- package/src/components/tools/FileReadCard.stories.tsx +120 -0
- package/src/components/tools/FileReadCard.tsx +106 -0
- package/src/components/tools/FindingCard.stories.tsx +124 -0
- package/src/components/tools/FindingCard.tsx +178 -0
- package/src/components/tools/GenericToolCard.stories.tsx +80 -0
- package/src/components/tools/GenericToolCard.tsx +111 -0
- package/src/components/tools/IpcCard.stories.tsx +129 -0
- package/src/components/tools/IpcCard.tsx +178 -0
- package/src/components/tools/KnowledgeCard.stories.tsx +112 -0
- package/src/components/tools/KnowledgeCard.tsx +165 -0
- package/src/components/tools/MetadataCard.stories.tsx +32 -0
- package/src/components/tools/MetadataCard.tsx +39 -0
- package/src/components/tools/SearchCard.stories.tsx +74 -0
- package/src/components/tools/SearchCard.tsx +86 -0
- package/src/components/tools/ShellCard.stories.tsx +112 -0
- package/src/components/tools/ShellCard.tsx +106 -0
- package/src/components/tools/TaskCard.stories.tsx +123 -0
- package/src/components/tools/TaskCard.tsx +203 -0
- package/src/components/tools/TodoCard.module.scss +131 -0
- package/src/components/tools/TodoCard.stories.tsx +202 -0
- package/src/components/tools/TodoCard.tsx +200 -0
- package/src/components/tools/ToolCard.stories.tsx +177 -0
- package/src/components/tools/ToolCard.tsx +60 -0
- package/src/components/tools/ToolCardProps.ts +20 -0
- package/src/components/tools/ToolSearchCard.stories.tsx +81 -0
- package/src/components/tools/ToolSearchCard.tsx +86 -0
- package/src/components/tools/WorkpadCard.stories.tsx +106 -0
- package/src/components/tools/WorkpadCard.tsx +125 -0
- package/src/components/tools/classifyTool.test.ts +44 -0
- package/src/components/tools/classifyTool.ts +134 -0
- package/src/components/tools/parseDiff.ts +95 -0
- package/src/components/tools/parseShellOutput.ts +28 -0
- package/src/components/tools/toolCardHelpers.test.ts +53 -0
- package/src/components/tools/toolCards.module.scss +234 -0
- package/src/components/workspace/WorkspaceBoard.module.scss +238 -0
- package/src/components/workspace/WorkspaceBoard.stories.tsx +240 -0
- package/src/components/workspace/WorkspaceBoard.tsx +232 -0
- package/src/components/workspace/WorkspaceFormFields.module.scss +79 -0
- package/src/components/workspace/WorkspaceFormFields.stories.tsx +133 -0
- package/src/components/workspace/WorkspaceFormFields.tsx +185 -0
- package/src/context/GrackleContext.ts +28 -0
- package/src/context/GrackleContextTypes.ts +64 -0
- package/src/context/SidebarContext.tsx +53 -0
- package/src/context/ThemeContext.tsx +21 -0
- package/src/context/ToastContext.tsx +56 -0
- package/src/hooks/types.ts +864 -0
- package/src/hooks/useEventSelection.test.ts +204 -0
- package/src/hooks/useEventSelection.ts +158 -0
- package/src/hooks/useSmartScroll.ts +151 -0
- package/src/hooks/useTheme.ts +228 -0
- package/src/index.ts +210 -0
- package/src/mocks/MockGrackleProvider.tsx +1397 -0
- package/src/mocks/mockData.ts +1966 -0
- package/src/mocks/mockKnowledgeData.ts +294 -0
- package/src/scss.d.ts +12 -0
- package/src/styles/global.scss +244 -0
- package/src/styles/mixins.scss +278 -0
- package/src/styles/prism-theme.scss +148 -0
- package/src/styles/theme.scss +1102 -0
- package/src/test-utils/storybook-decorators.tsx +50 -0
- package/src/test-utils/storybook-helpers.ts +262 -0
- package/src/themes.ts +142 -0
- package/src/utils/boardColumns.ts +141 -0
- package/src/utils/breadcrumbs.test.ts +285 -0
- package/src/utils/breadcrumbs.ts +222 -0
- package/src/utils/dashboard.test.ts +156 -0
- package/src/utils/dashboard.ts +195 -0
- package/src/utils/eventContent.test.ts +353 -0
- package/src/utils/eventContent.ts +209 -0
- package/src/utils/findingCategory.ts +33 -0
- package/src/utils/format.ts +27 -0
- package/src/utils/iconSize.ts +18 -0
- package/src/utils/navigation.ts +205 -0
- package/src/utils/route-config.test.ts +128 -0
- package/src/utils/scrollUtils.test.ts +65 -0
- package/src/utils/scrollUtils.ts +49 -0
- package/src/utils/sessionEvents.test.ts +302 -0
- package/src/utils/sessionEvents.ts +233 -0
- package/src/utils/taskStatus.tsx +137 -0
- package/src/utils/time.ts +92 -0
- package/tsconfig.json +8 -0
- package/vite.config.ts +20 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
@use '../../styles/mixins' as *;
|
|
2
|
+
|
|
3
|
+
// Edit wrapper that supports wrapping hint/error below the input
|
|
4
|
+
.editFieldWrapper {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-wrap: wrap;
|
|
7
|
+
flex: 1;
|
|
8
|
+
min-width: 0;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: var(--space-xs);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.editInput {
|
|
14
|
+
@include input-field;
|
|
15
|
+
font-size: var(--font-size-sm);
|
|
16
|
+
padding: 2px var(--space-xs);
|
|
17
|
+
flex: 1;
|
|
18
|
+
min-width: 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.editTextarea {
|
|
22
|
+
@include input-field;
|
|
23
|
+
font-size: var(--font-size-sm);
|
|
24
|
+
padding: var(--space-xs) var(--space-sm);
|
|
25
|
+
flex: 1;
|
|
26
|
+
min-width: 0;
|
|
27
|
+
resize: vertical;
|
|
28
|
+
min-height: 60px;
|
|
29
|
+
font-family: inherit;
|
|
30
|
+
line-height: 1.5;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.editSelect {
|
|
34
|
+
@include input-field;
|
|
35
|
+
font-size: var(--font-size-sm);
|
|
36
|
+
padding: 2px var(--space-xs);
|
|
37
|
+
flex: 1;
|
|
38
|
+
min-width: 0;
|
|
39
|
+
cursor: pointer;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Validation error message
|
|
43
|
+
.editError {
|
|
44
|
+
font-size: 10px;
|
|
45
|
+
color: var(--accent-red);
|
|
46
|
+
margin-top: 2px;
|
|
47
|
+
flex-basis: 100%;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Red border for invalid inputs
|
|
51
|
+
.editInputInvalid {
|
|
52
|
+
border-color: var(--accent-red) !important;
|
|
53
|
+
box-shadow: 0 0 0 1px var(--accent-red-dim);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Keyboard hint shown below editing inputs
|
|
57
|
+
.editHint {
|
|
58
|
+
font-size: 10px;
|
|
59
|
+
color: var(--text-tertiary);
|
|
60
|
+
opacity: 0.7;
|
|
61
|
+
margin-top: 2px;
|
|
62
|
+
font-family: var(--font-mono);
|
|
63
|
+
flex-basis: 100%;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Unsaved dot indicator
|
|
67
|
+
.unsavedDot {
|
|
68
|
+
width: 6px;
|
|
69
|
+
height: 6px;
|
|
70
|
+
border-radius: 50%;
|
|
71
|
+
background: var(--accent-yellow);
|
|
72
|
+
flex-shrink: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Click-to-edit wrapper — makes the whole value area clickable
|
|
76
|
+
.metaValueClickable {
|
|
77
|
+
display: inline-flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: var(--space-xs);
|
|
80
|
+
cursor: pointer;
|
|
81
|
+
border-radius: var(--radius-sm);
|
|
82
|
+
padding: 1px var(--space-xs);
|
|
83
|
+
margin: -1px calc(-1 * var(--space-xs));
|
|
84
|
+
transition: background var(--transition-fast);
|
|
85
|
+
background: none;
|
|
86
|
+
border: none;
|
|
87
|
+
text-align: left;
|
|
88
|
+
color: inherit;
|
|
89
|
+
font: inherit;
|
|
90
|
+
|
|
91
|
+
&:hover {
|
|
92
|
+
background: var(--bg-overlay);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Pencil icon with opacity transition
|
|
97
|
+
.editButton {
|
|
98
|
+
background: none;
|
|
99
|
+
border: none;
|
|
100
|
+
color: var(--text-tertiary);
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
padding: 2px;
|
|
103
|
+
font-size: var(--font-size-sm);
|
|
104
|
+
opacity: 0.4;
|
|
105
|
+
transition: opacity var(--transition-fast);
|
|
106
|
+
flex-shrink: 0;
|
|
107
|
+
|
|
108
|
+
&:hover {
|
|
109
|
+
opacity: 1;
|
|
110
|
+
color: var(--accent-green);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Italic gray placeholder for empty values
|
|
115
|
+
.metaPlaceholder {
|
|
116
|
+
color: var(--text-tertiary);
|
|
117
|
+
font-style: italic;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Checkbox + label styling
|
|
121
|
+
.worktreeToggle {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
gap: var(--space-sm);
|
|
125
|
+
font-size: var(--font-size-sm);
|
|
126
|
+
color: var(--text-secondary);
|
|
127
|
+
cursor: pointer;
|
|
128
|
+
|
|
129
|
+
input[type="checkbox"] {
|
|
130
|
+
accent-color: var(--accent-green);
|
|
131
|
+
width: 16px;
|
|
132
|
+
height: 16px;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, type JSX, type ReactNode } from "react";
|
|
2
|
+
import { useEditableField } from "./useEditableField.js";
|
|
3
|
+
import styles from "./EditableField.module.scss";
|
|
4
|
+
|
|
5
|
+
/** A single option in the select dropdown. */
|
|
6
|
+
export interface SelectOption {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Props for EditableSelect. */
|
|
12
|
+
export interface EditableSelectProps {
|
|
13
|
+
/** Current persisted value. */
|
|
14
|
+
value: string;
|
|
15
|
+
/** Called when the user selects a new value. Required in edit mode. */
|
|
16
|
+
onSave: (value: string) => void;
|
|
17
|
+
/** "edit" (default) for click-to-edit, "create" for always-visible. */
|
|
18
|
+
mode?: "edit" | "create";
|
|
19
|
+
/** Available options for the dropdown. */
|
|
20
|
+
options: SelectOption[];
|
|
21
|
+
/** Unique field identifier for coordination. */
|
|
22
|
+
fieldId?: string;
|
|
23
|
+
/** Which field is currently being edited (parent coordination). */
|
|
24
|
+
activeFieldId?: string | null; // eslint-disable-line @rushstack/no-new-null
|
|
25
|
+
/** Callback to tell the parent which field is active. */
|
|
26
|
+
onActivate?: (fieldId: string | null) => void; // eslint-disable-line @rushstack/no-new-null
|
|
27
|
+
/** Called on change in create mode. */
|
|
28
|
+
onChange?: (value: string) => void;
|
|
29
|
+
/** Custom display renderer for the selected value. */
|
|
30
|
+
renderDisplay?: (value: string) => ReactNode | undefined;
|
|
31
|
+
/** Placeholder text when no value is selected. */
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
/** Accessible label for the select. */
|
|
34
|
+
ariaLabel?: string;
|
|
35
|
+
/** Base test ID — gets `-select` / `-button` suffixes appended. */
|
|
36
|
+
"data-testid"?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Reusable click-to-edit select dropdown. */
|
|
40
|
+
export function EditableSelect(props: EditableSelectProps): JSX.Element {
|
|
41
|
+
const {
|
|
42
|
+
value,
|
|
43
|
+
onSave,
|
|
44
|
+
mode = "edit",
|
|
45
|
+
options,
|
|
46
|
+
fieldId = "select",
|
|
47
|
+
activeFieldId,
|
|
48
|
+
onActivate,
|
|
49
|
+
onChange,
|
|
50
|
+
renderDisplay,
|
|
51
|
+
placeholder,
|
|
52
|
+
ariaLabel,
|
|
53
|
+
"data-testid": testId,
|
|
54
|
+
} = props;
|
|
55
|
+
|
|
56
|
+
const selectRef = useRef<HTMLSelectElement>(null);
|
|
57
|
+
|
|
58
|
+
const field = useEditableField({
|
|
59
|
+
value,
|
|
60
|
+
onSave,
|
|
61
|
+
fieldId,
|
|
62
|
+
activeFieldId,
|
|
63
|
+
onActivate,
|
|
64
|
+
enterToSave: false,
|
|
65
|
+
trimOnSave: false,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Auto-focus when entering edit mode
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (field.isEditing) {
|
|
71
|
+
const timer = window.setTimeout(() => {
|
|
72
|
+
selectRef.current?.focus();
|
|
73
|
+
}, 0);
|
|
74
|
+
return () => window.clearTimeout(timer);
|
|
75
|
+
}
|
|
76
|
+
}, [field.isEditing]);
|
|
77
|
+
|
|
78
|
+
/** Select saves immediately on change and exits edit mode. */
|
|
79
|
+
const handleSelectChange = useCallback((e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
80
|
+
const newValue = e.target.value;
|
|
81
|
+
field.ignoreInitialBlurRef.current = false;
|
|
82
|
+
if (newValue !== value) {
|
|
83
|
+
onSave(newValue);
|
|
84
|
+
}
|
|
85
|
+
field.cancelEdit();
|
|
86
|
+
}, [value, onSave, field]);
|
|
87
|
+
|
|
88
|
+
/** Blur just cancels (no auto-save for selects). */
|
|
89
|
+
const handleSelectBlur = useCallback((event: React.FocusEvent) => {
|
|
90
|
+
if (field.ignoreInitialBlurRef.current) {
|
|
91
|
+
field.ignoreInitialBlurRef.current = false;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (
|
|
95
|
+
event.relatedTarget instanceof HTMLElement &&
|
|
96
|
+
event.relatedTarget.dataset.editAction === fieldId
|
|
97
|
+
) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
field.cancelEdit();
|
|
101
|
+
}, [fieldId, field]);
|
|
102
|
+
|
|
103
|
+
// Create mode: always show select
|
|
104
|
+
if (mode === "create") {
|
|
105
|
+
return (
|
|
106
|
+
<select
|
|
107
|
+
className={styles.editSelect}
|
|
108
|
+
value={value}
|
|
109
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
110
|
+
aria-label={ariaLabel}
|
|
111
|
+
data-testid={testId ? `${testId}-select` : undefined}
|
|
112
|
+
>
|
|
113
|
+
{options.map((opt) => (
|
|
114
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
115
|
+
))}
|
|
116
|
+
</select>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Edit mode: show select dropdown
|
|
121
|
+
if (field.isEditing) {
|
|
122
|
+
return (
|
|
123
|
+
<select
|
|
124
|
+
ref={selectRef}
|
|
125
|
+
className={styles.editSelect}
|
|
126
|
+
value={field.draft}
|
|
127
|
+
onChange={handleSelectChange}
|
|
128
|
+
onBlur={handleSelectBlur}
|
|
129
|
+
title={ariaLabel}
|
|
130
|
+
aria-label={ariaLabel}
|
|
131
|
+
data-testid={testId ? `${testId}-select` : undefined}
|
|
132
|
+
>
|
|
133
|
+
{options.map((opt) => (
|
|
134
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
135
|
+
))}
|
|
136
|
+
</select>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Display mode
|
|
141
|
+
const displayContent = renderDisplay?.(value);
|
|
142
|
+
const selectedLabel = options.find((o) => o.value === value)?.label;
|
|
143
|
+
return (
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
className={styles.metaValueClickable}
|
|
147
|
+
onClick={() => field.startEdit()}
|
|
148
|
+
title="Click to change"
|
|
149
|
+
aria-label={ariaLabel}
|
|
150
|
+
data-testid={testId ? `${testId}-button` : undefined}
|
|
151
|
+
>
|
|
152
|
+
{displayContent !== undefined ? displayContent : (
|
|
153
|
+
selectedLabel ? (
|
|
154
|
+
<span>{selectedLabel}</span>
|
|
155
|
+
) : (
|
|
156
|
+
<span className={styles.metaPlaceholder}>{placeholder || "None"}</span>
|
|
157
|
+
)
|
|
158
|
+
)}
|
|
159
|
+
<span className={styles.editButton} aria-hidden="true">
|
|
160
|
+
✏️
|
|
161
|
+
</span>
|
|
162
|
+
</button>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect, fn } from "@storybook/test";
|
|
3
|
+
import { EditableTextArea } from "./EditableTextArea.js";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof EditableTextArea> = {
|
|
6
|
+
title: "Primitives/Editable/EditableTextArea",
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
component: EditableTextArea,
|
|
9
|
+
args: {
|
|
10
|
+
value: "Multi-line\ntext content",
|
|
11
|
+
onSave: fn(),
|
|
12
|
+
"data-testid": "test-area",
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
/** Display mode shows the value with an edit button. */
|
|
19
|
+
export const DisplayMode: Story = {
|
|
20
|
+
play: async ({ canvas }) => {
|
|
21
|
+
const button = canvas.getByTestId("test-area-button");
|
|
22
|
+
await expect(button).toBeInTheDocument();
|
|
23
|
+
await expect(button).toHaveAttribute("role", "button");
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Display button is keyboard-accessible (tabIndex, role, focusable). */
|
|
28
|
+
export const KeyboardAccessible: Story = {
|
|
29
|
+
play: async ({ canvas }) => {
|
|
30
|
+
const button = canvas.getByTestId("test-area-button");
|
|
31
|
+
await expect(button).toHaveAttribute("role", "button");
|
|
32
|
+
await expect(button).toHaveAttribute("tabindex", "0");
|
|
33
|
+
|
|
34
|
+
button.focus();
|
|
35
|
+
await expect(button).toHaveFocus();
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Create mode renders as an always-visible textarea. */
|
|
40
|
+
export const CreateMode: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
mode: "create",
|
|
43
|
+
value: "",
|
|
44
|
+
placeholder: "Enter description",
|
|
45
|
+
},
|
|
46
|
+
play: async ({ canvas }) => {
|
|
47
|
+
const textarea = canvas.getByTestId("test-area-input");
|
|
48
|
+
await expect(textarea).toBeInTheDocument();
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { useEffect, useRef, type JSX, type ReactNode } from "react";
|
|
2
|
+
import { useEditableField } from "./useEditableField.js";
|
|
3
|
+
import styles from "./EditableField.module.scss";
|
|
4
|
+
|
|
5
|
+
/** Props for EditableTextArea. */
|
|
6
|
+
export interface EditableTextAreaProps {
|
|
7
|
+
/** Current persisted value. */
|
|
8
|
+
value: string;
|
|
9
|
+
/** Called when the user saves. Required in edit mode. */
|
|
10
|
+
onSave: (value: string) => void;
|
|
11
|
+
/** Optional validation — return an error string, or undefined if valid. */
|
|
12
|
+
validate?: (value: string) => string | undefined;
|
|
13
|
+
/** "edit" (default) for click-to-edit, "create" for always-editable. */
|
|
14
|
+
mode?: "edit" | "create";
|
|
15
|
+
/** Unique field identifier for coordination. */
|
|
16
|
+
fieldId?: string;
|
|
17
|
+
/** Which field is currently being edited (parent coordination). */
|
|
18
|
+
activeFieldId?: string | null; // eslint-disable-line @rushstack/no-new-null
|
|
19
|
+
/** Callback to tell the parent which field is active. */
|
|
20
|
+
onActivate?: (fieldId: string | null) => void; // eslint-disable-line @rushstack/no-new-null
|
|
21
|
+
/** Called on every keystroke in create mode. */
|
|
22
|
+
onChange?: (value: string) => void;
|
|
23
|
+
/** Custom display renderer (e.g., Markdown). */
|
|
24
|
+
renderDisplay?: (value: string) => ReactNode | undefined;
|
|
25
|
+
/** Placeholder text shown when empty. */
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
/** Accessible label for the textarea. */
|
|
28
|
+
ariaLabel?: string;
|
|
29
|
+
/** Base test ID — gets `-input` / `-button` suffixes appended. */
|
|
30
|
+
"data-testid"?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Reusable click-to-edit textarea field. */
|
|
34
|
+
export function EditableTextArea(props: EditableTextAreaProps): JSX.Element {
|
|
35
|
+
const {
|
|
36
|
+
value,
|
|
37
|
+
onSave,
|
|
38
|
+
validate,
|
|
39
|
+
mode = "edit",
|
|
40
|
+
fieldId = "textarea",
|
|
41
|
+
activeFieldId,
|
|
42
|
+
onActivate,
|
|
43
|
+
onChange,
|
|
44
|
+
renderDisplay,
|
|
45
|
+
placeholder,
|
|
46
|
+
ariaLabel,
|
|
47
|
+
"data-testid": testId,
|
|
48
|
+
} = props;
|
|
49
|
+
|
|
50
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
51
|
+
|
|
52
|
+
const field = useEditableField({
|
|
53
|
+
value,
|
|
54
|
+
onSave,
|
|
55
|
+
validate,
|
|
56
|
+
fieldId,
|
|
57
|
+
activeFieldId,
|
|
58
|
+
onActivate,
|
|
59
|
+
enterToSave: false,
|
|
60
|
+
trimOnSave: false,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Auto-focus when entering edit mode
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (field.isEditing) {
|
|
66
|
+
const timer = window.setTimeout(() => {
|
|
67
|
+
textareaRef.current?.focus();
|
|
68
|
+
}, 0);
|
|
69
|
+
return () => window.clearTimeout(timer);
|
|
70
|
+
}
|
|
71
|
+
}, [field.isEditing]);
|
|
72
|
+
|
|
73
|
+
// Create mode: always show textarea, no blur-to-save
|
|
74
|
+
if (mode === "create") {
|
|
75
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
|
76
|
+
onChange?.(e.target.value);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const validationError = validate?.(value);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className={styles.editFieldWrapper}>
|
|
83
|
+
<textarea
|
|
84
|
+
className={`${styles.editTextarea} ${validationError ? styles.editInputInvalid : ""}`}
|
|
85
|
+
value={value}
|
|
86
|
+
onChange={handleChange}
|
|
87
|
+
placeholder={placeholder}
|
|
88
|
+
aria-label={ariaLabel}
|
|
89
|
+
data-testid={testId ? `${testId}-input` : undefined}
|
|
90
|
+
/>
|
|
91
|
+
{validationError && (
|
|
92
|
+
<span className={styles.editError} data-testid="edit-error">{validationError}</span>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Edit mode: toggle between display and textarea
|
|
99
|
+
if (field.isEditing) {
|
|
100
|
+
return (
|
|
101
|
+
<div className={styles.editFieldWrapper}>
|
|
102
|
+
<textarea
|
|
103
|
+
ref={textareaRef}
|
|
104
|
+
className={`${styles.editTextarea} ${field.error ? styles.editInputInvalid : ""}`}
|
|
105
|
+
value={field.draft}
|
|
106
|
+
onChange={(e) => field.setDraft(e.target.value)}
|
|
107
|
+
onBlur={field.handleBlur}
|
|
108
|
+
onKeyDown={field.handleKeyDown}
|
|
109
|
+
title={ariaLabel}
|
|
110
|
+
aria-label={ariaLabel}
|
|
111
|
+
data-testid={testId ? `${testId}-input` : undefined}
|
|
112
|
+
/>
|
|
113
|
+
{field.isDirty && <span className={styles.unsavedDot} title="Unsaved changes" />}
|
|
114
|
+
{field.error && (
|
|
115
|
+
<span className={styles.editError} data-testid="edit-error">{field.error}</span>
|
|
116
|
+
)}
|
|
117
|
+
<span className={styles.editHint}>Tab to save · Esc to cancel</span>
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Display mode — uses <span role="button"> to avoid nested interactive elements
|
|
123
|
+
// when renderDisplay returns links or block-level content (e.g., Markdown)
|
|
124
|
+
const displayContent = renderDisplay?.(value);
|
|
125
|
+
return (
|
|
126
|
+
<span
|
|
127
|
+
role="button"
|
|
128
|
+
tabIndex={0}
|
|
129
|
+
className={styles.metaValueClickable}
|
|
130
|
+
onClick={() => field.startEdit()}
|
|
131
|
+
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); field.startEdit(); } }}
|
|
132
|
+
title="Click to edit"
|
|
133
|
+
aria-label={ariaLabel}
|
|
134
|
+
data-testid={testId ? `${testId}-button` : undefined}
|
|
135
|
+
>
|
|
136
|
+
{displayContent !== undefined ? displayContent : (
|
|
137
|
+
value ? (
|
|
138
|
+
<span>{value}</span>
|
|
139
|
+
) : (
|
|
140
|
+
<span className={styles.metaPlaceholder}>{placeholder || "None"}</span>
|
|
141
|
+
)
|
|
142
|
+
)}
|
|
143
|
+
<span className={styles.editButton} aria-hidden="true">
|
|
144
|
+
✏️
|
|
145
|
+
</span>
|
|
146
|
+
</span>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect, fn } from "@storybook/test";
|
|
3
|
+
import { EditableTextField } from "./EditableTextField.js";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof EditableTextField> = {
|
|
6
|
+
title: "Primitives/Editable/EditableTextField",
|
|
7
|
+
tags: ["autodocs"],
|
|
8
|
+
component: EditableTextField,
|
|
9
|
+
args: {
|
|
10
|
+
value: "Hello World",
|
|
11
|
+
onSave: fn(),
|
|
12
|
+
"data-testid": "test-field",
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
export default meta;
|
|
16
|
+
type Story = StoryObj<typeof meta>;
|
|
17
|
+
|
|
18
|
+
/** Display mode shows the value with an edit button. */
|
|
19
|
+
export const DisplayMode: Story = {
|
|
20
|
+
play: async ({ canvas }) => {
|
|
21
|
+
await expect(canvas.getByText("Hello World")).toBeInTheDocument();
|
|
22
|
+
const button = canvas.getByTestId("test-field-button");
|
|
23
|
+
await expect(button).toBeInTheDocument();
|
|
24
|
+
await expect(button).toHaveAttribute("role", "button");
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Display button is keyboard-accessible (tabIndex, role, focusable). */
|
|
29
|
+
export const KeyboardAccessible: Story = {
|
|
30
|
+
play: async ({ canvas }) => {
|
|
31
|
+
const button = canvas.getByTestId("test-field-button");
|
|
32
|
+
await expect(button).toHaveAttribute("role", "button");
|
|
33
|
+
await expect(button).toHaveAttribute("tabindex", "0");
|
|
34
|
+
|
|
35
|
+
button.focus();
|
|
36
|
+
await expect(button).toHaveFocus();
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Empty value shows placeholder text. */
|
|
41
|
+
export const EmptyShowsPlaceholder: Story = {
|
|
42
|
+
args: {
|
|
43
|
+
value: "",
|
|
44
|
+
placeholder: "Enter a value",
|
|
45
|
+
},
|
|
46
|
+
play: async ({ canvas }) => {
|
|
47
|
+
await expect(canvas.getByText("Enter a value")).toBeInTheDocument();
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Create mode renders as an always-visible input. */
|
|
52
|
+
export const CreateMode: Story = {
|
|
53
|
+
args: {
|
|
54
|
+
mode: "create",
|
|
55
|
+
value: "",
|
|
56
|
+
placeholder: "Enter title",
|
|
57
|
+
},
|
|
58
|
+
play: async ({ canvas }) => {
|
|
59
|
+
const input = canvas.getByTestId("test-field-input");
|
|
60
|
+
await expect(input).toBeInTheDocument();
|
|
61
|
+
},
|
|
62
|
+
};
|