@principal-ai/file-city-react 0.5.40 → 0.5.41

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 (55) hide show
  1. package/dist/components/FileCity3D/FileCity3D.d.ts +8 -2
  2. package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
  3. package/dist/components/FileCity3D/FileCity3D.js +129 -40
  4. package/dist/components/FileCityExplorer/AddToAreaModal.d.ts +14 -0
  5. package/dist/components/FileCityExplorer/AddToAreaModal.d.ts.map +1 -0
  6. package/dist/components/FileCityExplorer/AddToAreaModal.js +140 -0
  7. package/dist/components/FileCityExplorer/AddToScopeModal.d.ts +14 -0
  8. package/dist/components/FileCityExplorer/AddToScopeModal.d.ts.map +1 -0
  9. package/dist/components/FileCityExplorer/AddToScopeModal.js +176 -0
  10. package/dist/components/FileCityExplorer/FileCityExplorer.d.ts +30 -0
  11. package/dist/components/FileCityExplorer/FileCityExplorer.d.ts.map +1 -0
  12. package/dist/components/FileCityExplorer/FileCityExplorer.js +1045 -0
  13. package/dist/components/FileCityExplorer/ScopeInfoOverlay.d.ts +10 -0
  14. package/dist/components/FileCityExplorer/ScopeInfoOverlay.d.ts.map +1 -0
  15. package/dist/components/FileCityExplorer/ScopeInfoOverlay.js +73 -0
  16. package/dist/components/FileCityExplorer/index.d.ts +3 -0
  17. package/dist/components/FileCityExplorer/index.d.ts.map +1 -0
  18. package/dist/components/FileCityExplorer/index.js +1 -0
  19. package/dist/components/FileCityExplorer/layers.d.ts +16 -0
  20. package/dist/components/FileCityExplorer/layers.d.ts.map +1 -0
  21. package/dist/components/FileCityExplorer/layers.js +61 -0
  22. package/dist/components/FileCityExplorer/model.d.ts +32 -0
  23. package/dist/components/FileCityExplorer/model.d.ts.map +1 -0
  24. package/dist/components/FileCityExplorer/model.js +14 -0
  25. package/dist/components/FileCityExplorer/pathConversion.d.ts +19 -0
  26. package/dist/components/FileCityExplorer/pathConversion.d.ts.map +1 -0
  27. package/dist/components/FileCityExplorer/pathConversion.js +26 -0
  28. package/dist/components/FileCityExplorer/scopeTreePaths.d.ts +21 -0
  29. package/dist/components/FileCityExplorer/scopeTreePaths.d.ts.map +1 -0
  30. package/dist/components/FileCityExplorer/scopeTreePaths.js +42 -0
  31. package/dist/components/FileCityExplorer/styles.d.ts +9 -0
  32. package/dist/components/FileCityExplorer/styles.d.ts.map +1 -0
  33. package/dist/components/FileCityExplorer/styles.js +28 -0
  34. package/dist/utils/folderElevatedPanels.d.ts +3 -1
  35. package/dist/utils/folderElevatedPanels.d.ts.map +1 -1
  36. package/dist/utils/folderElevatedPanels.js +5 -2
  37. package/package.json +2 -1
  38. package/src/components/FileCity3D/FileCity3D.tsx +200 -52
  39. package/src/components/FileCityExplorer/AddToAreaModal.tsx +273 -0
  40. package/src/components/FileCityExplorer/AddToScopeModal.tsx +320 -0
  41. package/src/components/FileCityExplorer/FileCityExplorer.tsx +1457 -0
  42. package/src/components/FileCityExplorer/ScopeInfoOverlay.tsx +229 -0
  43. package/src/components/FileCityExplorer/index.ts +2 -0
  44. package/src/components/FileCityExplorer/layers.ts +72 -0
  45. package/src/components/FileCityExplorer/model.ts +35 -0
  46. package/src/components/FileCityExplorer/pathConversion.ts +32 -0
  47. package/src/components/FileCityExplorer/scopeTreePaths.ts +52 -0
  48. package/src/components/FileCityExplorer/styles.ts +34 -0
  49. package/src/stories/2D3DComparison.stories.tsx +13 -2
  50. package/src/stories/ElevatedScopePanels.stories.tsx +295 -0
  51. package/src/stories/FileCity3D.stories.tsx +24 -3
  52. package/src/stories/FileCityExplorer.stories.tsx +2474 -0
  53. package/src/stories/FileCityExplorerComponent.stories.tsx +59 -0
  54. package/src/utils/folderElevatedPanels.ts +8 -2
  55. package/src/stories/ScopeOverlay.stories.tsx +0 -1610
@@ -0,0 +1,273 @@
1
+ import React from 'react';
2
+ import { useTheme } from '@principal-ade/industry-theme';
3
+ import type { ProjectArea } from './model';
4
+ import { AREA_PANEL_COLOR } from './layers';
5
+ import { makeSectionLabelStyle } from './styles';
6
+
7
+ export const AddToAreaModal: React.FC<{
8
+ path: string;
9
+ areas: readonly ProjectArea[];
10
+ areaName: string;
11
+ description: string;
12
+ onAreaNameChange: (value: string) => void;
13
+ onDescriptionChange: (value: string) => void;
14
+ onPickExisting: (areaName: string) => void;
15
+ onSubmit: () => void;
16
+ onClose: () => void;
17
+ }> = ({
18
+ path,
19
+ areas,
20
+ areaName,
21
+ description,
22
+ onAreaNameChange,
23
+ onDescriptionChange,
24
+ onPickExisting,
25
+ onSubmit,
26
+ onClose,
27
+ }) => {
28
+ const { theme } = useTheme();
29
+ const sectionLabelStyle = makeSectionLabelStyle(theme);
30
+
31
+ React.useEffect(() => {
32
+ const onKey = (e: KeyboardEvent) => {
33
+ if (e.key === 'Escape') onClose();
34
+ };
35
+ window.addEventListener('keydown', onKey);
36
+ return () => window.removeEventListener('keydown', onKey);
37
+ }, [onClose]);
38
+
39
+ const trimmedName = areaName.trim();
40
+ const canSubmit = trimmedName.length > 0;
41
+ const targetArea = areas.find(a => a.name === trimmedName);
42
+ const alreadyClaimed = targetArea?.paths.includes(path) ?? false;
43
+
44
+ let actionLabel = 'Add';
45
+ if (alreadyClaimed) actionLabel = 'Already added';
46
+ else if (!targetArea) actionLabel = 'Create area';
47
+ else actionLabel = 'Add path';
48
+
49
+ const sectionDivider = `1px solid ${theme.colors.backgroundSecondary}`;
50
+ const inputStyle: React.CSSProperties = {
51
+ padding: '8px 10px',
52
+ background: theme.colors.backgroundDark ?? theme.colors.background,
53
+ color: theme.colors.text,
54
+ border: `1px solid ${theme.colors.border}`,
55
+ borderRadius: theme.radii[2],
56
+ fontFamily: theme.fonts.monospace,
57
+ fontSize: theme.fontSizes[1],
58
+ };
59
+
60
+ return (
61
+ <div
62
+ onClick={onClose}
63
+ style={{
64
+ position: 'fixed',
65
+ inset: 0,
66
+ background: 'rgba(0, 0, 0, 0.55)',
67
+ display: 'flex',
68
+ alignItems: 'center',
69
+ justifyContent: 'center',
70
+ zIndex: 1000,
71
+ fontFamily: theme.fonts.body,
72
+ }}
73
+ >
74
+ <div
75
+ onClick={e => e.stopPropagation()}
76
+ style={{
77
+ width: 520,
78
+ maxHeight: 'min(80vh, 700px)',
79
+ display: 'flex',
80
+ flexDirection: 'column',
81
+ background: theme.colors.background,
82
+ color: theme.colors.text,
83
+ borderRadius: theme.radii[4],
84
+ border: `1px solid ${theme.colors.border}`,
85
+ boxShadow: theme.shadows[4],
86
+ overflow: 'hidden',
87
+ }}
88
+ >
89
+ <div
90
+ style={{
91
+ padding: '14px 18px',
92
+ borderBottom: sectionDivider,
93
+ display: 'flex',
94
+ justifyContent: 'space-between',
95
+ alignItems: 'flex-start',
96
+ gap: theme.space[3],
97
+ }}
98
+ >
99
+ <div>
100
+ <div style={sectionLabelStyle}>Add to area</div>
101
+ <div
102
+ style={{
103
+ fontFamily: theme.fonts.monospace,
104
+ fontSize: theme.fontSizes[0],
105
+ color: theme.colors.textMuted,
106
+ marginTop: 6,
107
+ wordBreak: 'break-all',
108
+ }}
109
+ >
110
+ {path}
111
+ </div>
112
+ </div>
113
+ <button
114
+ onClick={onClose}
115
+ style={{
116
+ background: 'transparent',
117
+ border: 'none',
118
+ color: theme.colors.textTertiary,
119
+ fontSize: theme.fontSizes[3],
120
+ cursor: 'pointer',
121
+ lineHeight: 1,
122
+ padding: 0,
123
+ }}
124
+ aria-label="Close"
125
+ >
126
+ ×
127
+ </button>
128
+ </div>
129
+
130
+ <div
131
+ style={{
132
+ padding: '14px 18px',
133
+ borderBottom: sectionDivider,
134
+ display: 'flex',
135
+ flexDirection: 'column',
136
+ gap: theme.space[3],
137
+ }}
138
+ >
139
+ <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
140
+ <span style={sectionLabelStyle}>Area</span>
141
+ <input
142
+ type="text"
143
+ value={areaName}
144
+ list="area-name-options"
145
+ autoFocus
146
+ placeholder="e.g. Documentation"
147
+ onChange={e => onAreaNameChange(e.target.value)}
148
+ onKeyDown={e => {
149
+ if (e.key === 'Enter' && canSubmit && !alreadyClaimed) onSubmit();
150
+ }}
151
+ style={inputStyle}
152
+ />
153
+ <datalist id="area-name-options">
154
+ {areas.map(a => (
155
+ <option key={a.name} value={a.name} />
156
+ ))}
157
+ </datalist>
158
+ </label>
159
+
160
+ {!targetArea && trimmedName.length > 0 && (
161
+ <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
162
+ <span style={sectionLabelStyle}>Description (optional)</span>
163
+ <input
164
+ type="text"
165
+ value={description}
166
+ placeholder="Why this area exists, what it covers"
167
+ onChange={e => onDescriptionChange(e.target.value)}
168
+ onKeyDown={e => {
169
+ if (e.key === 'Enter' && canSubmit) onSubmit();
170
+ }}
171
+ style={{ ...inputStyle, fontFamily: theme.fonts.body }}
172
+ />
173
+ </label>
174
+ )}
175
+
176
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: theme.space[2] }}>
177
+ <button
178
+ onClick={onClose}
179
+ style={{
180
+ padding: '8px 14px',
181
+ background: 'transparent',
182
+ color: theme.colors.textSecondary,
183
+ border: `1px solid ${theme.colors.border}`,
184
+ borderRadius: theme.radii[2],
185
+ cursor: 'pointer',
186
+ fontSize: theme.fontSizes[1],
187
+ }}
188
+ >
189
+ Cancel
190
+ </button>
191
+ <button
192
+ onClick={onSubmit}
193
+ disabled={!canSubmit || alreadyClaimed}
194
+ style={{
195
+ padding: '8px 14px',
196
+ background: !canSubmit || alreadyClaimed
197
+ ? theme.colors.backgroundSecondary
198
+ : theme.colors.textMuted,
199
+ color: !canSubmit || alreadyClaimed
200
+ ? theme.colors.muted
201
+ : theme.colors.background,
202
+ border: `1px solid ${theme.colors.border}`,
203
+ borderRadius: theme.radii[2],
204
+ cursor: !canSubmit || alreadyClaimed ? 'not-allowed' : 'pointer',
205
+ fontSize: theme.fontSizes[1],
206
+ fontWeight: theme.fontWeights.medium,
207
+ }}
208
+ >
209
+ {actionLabel}
210
+ </button>
211
+ </div>
212
+ </div>
213
+
214
+ <div style={{ padding: '14px 18px', overflowY: 'auto', flex: 1 }}>
215
+ <div style={sectionLabelStyle}>Existing areas (click to prefill)</div>
216
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: theme.space[2] }}>
217
+ {areas.length === 0 && (
218
+ <div style={{ fontSize: theme.fontSizes[0], color: theme.colors.textTertiary, fontStyle: 'italic' }}>
219
+ No areas yet. Type a name above to create the first one.
220
+ </div>
221
+ )}
222
+ {areas.map(area => {
223
+ const claims = area.paths.includes(path);
224
+ return (
225
+ <button
226
+ key={area.name}
227
+ onClick={() => onPickExisting(area.name)}
228
+ title={claims ? 'Area already claims this path' : 'Prefill the area name'}
229
+ style={{
230
+ fontSize: theme.fontSizes[0],
231
+ padding: '6px 10px',
232
+ background: claims ? theme.colors.background : theme.colors.backgroundSecondary,
233
+ color: claims ? theme.colors.muted : theme.colors.text,
234
+ border: `1px solid ${theme.colors.border}`,
235
+ borderRadius: theme.radii[2],
236
+ cursor: 'pointer',
237
+ textAlign: 'left',
238
+ display: 'flex',
239
+ alignItems: 'center',
240
+ gap: theme.space[2],
241
+ opacity: claims ? 0.6 : 1,
242
+ }}
243
+ >
244
+ <span
245
+ style={{
246
+ width: 10,
247
+ height: 10,
248
+ borderRadius: theme.radii[1],
249
+ background: AREA_PANEL_COLOR,
250
+ border: `1px dashed ${theme.colors.textMuted}`,
251
+ flexShrink: 0,
252
+ }}
253
+ />
254
+ <span style={{ fontFamily: theme.fonts.monospace }}>{area.name}</span>
255
+ <span
256
+ style={{
257
+ marginLeft: 'auto',
258
+ fontSize: theme.fontSizes[0],
259
+ color: theme.colors.textTertiary,
260
+ }}
261
+ >
262
+ {area.paths.length} path{area.paths.length === 1 ? '' : 's'}
263
+ </span>
264
+ {claims && <span style={{ marginLeft: theme.space[1], fontSize: theme.fontSizes[0] }}>✓</span>}
265
+ </button>
266
+ );
267
+ })}
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ );
273
+ };
@@ -0,0 +1,320 @@
1
+ import React from 'react';
2
+ import { useTheme } from '@principal-ade/industry-theme';
3
+ import type { Scope } from './model';
4
+ import { makeSectionLabelStyle } from './styles';
5
+
6
+ export const AddToScopeModal: React.FC<{
7
+ path: string;
8
+ scopes: readonly Scope[];
9
+ scopeId: string;
10
+ namespaceName: string;
11
+ onScopeIdChange: (value: string) => void;
12
+ onNamespaceNameChange: (value: string) => void;
13
+ onPickExisting: (scopeId: string, namespaceName: string) => void;
14
+ onSubmit: () => void;
15
+ onClose: () => void;
16
+ }> = ({
17
+ path,
18
+ scopes,
19
+ scopeId,
20
+ namespaceName,
21
+ onScopeIdChange,
22
+ onNamespaceNameChange,
23
+ onPickExisting,
24
+ onSubmit,
25
+ onClose,
26
+ }) => {
27
+ const { theme } = useTheme();
28
+ const sectionLabelStyle = makeSectionLabelStyle(theme);
29
+
30
+ React.useEffect(() => {
31
+ const onKey = (e: KeyboardEvent) => {
32
+ if (e.key === 'Escape') onClose();
33
+ };
34
+ window.addEventListener('keydown', onKey);
35
+ return () => window.removeEventListener('keydown', onKey);
36
+ }, [onClose]);
37
+
38
+ const trimmedScope = scopeId.trim();
39
+ const trimmedNs = namespaceName.trim();
40
+ const canSubmit = trimmedScope.length > 0;
41
+
42
+ // Determine what the submit will do, for the action label.
43
+ const targetScope = scopes.find(s => s.id === trimmedScope);
44
+ const targetNs = trimmedNs
45
+ ? targetScope?.namespaces.find(n => n.name === trimmedNs) ?? null
46
+ : null;
47
+ const alreadyClaimed = trimmedNs
48
+ ? targetNs?.paths.includes(path) ?? false
49
+ : targetScope?.paths.includes(path) ?? false;
50
+
51
+ let actionLabel = 'Add';
52
+ if (alreadyClaimed) actionLabel = 'Already added';
53
+ else if (!targetScope && !trimmedNs) actionLabel = 'Create scope';
54
+ else if (!targetScope) actionLabel = 'Create scope + namespace';
55
+ else if (!trimmedNs) actionLabel = 'Add to scope';
56
+ else if (!targetNs) actionLabel = 'Create namespace';
57
+ else actionLabel = 'Add path';
58
+
59
+ const sectionDivider = `1px solid ${theme.colors.backgroundSecondary}`;
60
+ const inputStyle: React.CSSProperties = {
61
+ padding: '8px 10px',
62
+ background: theme.colors.backgroundDark ?? theme.colors.background,
63
+ color: theme.colors.text,
64
+ border: `1px solid ${theme.colors.border}`,
65
+ borderRadius: theme.radii[2],
66
+ fontFamily: theme.fonts.monospace,
67
+ fontSize: theme.fontSizes[1],
68
+ };
69
+
70
+ return (
71
+ <div
72
+ onClick={onClose}
73
+ style={{
74
+ position: 'fixed',
75
+ inset: 0,
76
+ background: 'rgba(0, 0, 0, 0.55)',
77
+ display: 'flex',
78
+ alignItems: 'center',
79
+ justifyContent: 'center',
80
+ zIndex: 1000,
81
+ fontFamily: theme.fonts.body,
82
+ }}
83
+ >
84
+ <div
85
+ onClick={e => e.stopPropagation()}
86
+ style={{
87
+ width: 520,
88
+ maxHeight: 'min(80vh, 700px)',
89
+ display: 'flex',
90
+ flexDirection: 'column',
91
+ background: theme.colors.background,
92
+ color: theme.colors.text,
93
+ borderRadius: theme.radii[4],
94
+ border: `1px solid ${theme.colors.border}`,
95
+ boxShadow: theme.shadows[4],
96
+ overflow: 'hidden',
97
+ }}
98
+ >
99
+ <div
100
+ style={{
101
+ padding: '14px 18px',
102
+ borderBottom: sectionDivider,
103
+ display: 'flex',
104
+ justifyContent: 'space-between',
105
+ alignItems: 'flex-start',
106
+ gap: theme.space[3],
107
+ }}
108
+ >
109
+ <div>
110
+ <div style={sectionLabelStyle}>Add to scope</div>
111
+ <div
112
+ style={{
113
+ fontFamily: theme.fonts.monospace,
114
+ fontSize: theme.fontSizes[0],
115
+ color: theme.colors.textMuted,
116
+ marginTop: 6,
117
+ wordBreak: 'break-all',
118
+ }}
119
+ >
120
+ {path}
121
+ </div>
122
+ </div>
123
+ <button
124
+ onClick={onClose}
125
+ style={{
126
+ background: 'transparent',
127
+ border: 'none',
128
+ color: theme.colors.textTertiary,
129
+ fontSize: theme.fontSizes[3],
130
+ cursor: 'pointer',
131
+ lineHeight: 1,
132
+ padding: 0,
133
+ }}
134
+ aria-label="Close"
135
+ >
136
+ ×
137
+ </button>
138
+ </div>
139
+
140
+ <div
141
+ style={{
142
+ padding: '14px 18px',
143
+ borderBottom: sectionDivider,
144
+ display: 'flex',
145
+ flexDirection: 'column',
146
+ gap: theme.space[3],
147
+ }}
148
+ >
149
+ <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
150
+ <span style={sectionLabelStyle}>Scope</span>
151
+ <input
152
+ type="text"
153
+ value={scopeId}
154
+ list="scope-id-options"
155
+ autoFocus
156
+ placeholder="e.g. principal-view.cli"
157
+ onChange={e => onScopeIdChange(e.target.value)}
158
+ onKeyDown={e => {
159
+ if (e.key === 'Enter' && canSubmit && !alreadyClaimed) onSubmit();
160
+ }}
161
+ style={inputStyle}
162
+ />
163
+ <datalist id="scope-id-options">
164
+ {scopes.map(s => (
165
+ <option key={s.id} value={s.id} />
166
+ ))}
167
+ </datalist>
168
+ </label>
169
+
170
+ <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
171
+ <span style={sectionLabelStyle}>Namespace (optional)</span>
172
+ <input
173
+ type="text"
174
+ value={namespaceName}
175
+ placeholder="leave blank to add to scope itself"
176
+ onChange={e => onNamespaceNameChange(e.target.value)}
177
+ onKeyDown={e => {
178
+ if (e.key === 'Enter' && canSubmit && !alreadyClaimed) onSubmit();
179
+ }}
180
+ style={inputStyle}
181
+ />
182
+ </label>
183
+
184
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: theme.space[2] }}>
185
+ <button
186
+ onClick={onClose}
187
+ style={{
188
+ padding: '8px 14px',
189
+ background: 'transparent',
190
+ color: theme.colors.textSecondary,
191
+ border: `1px solid ${theme.colors.border}`,
192
+ borderRadius: theme.radii[2],
193
+ cursor: 'pointer',
194
+ fontSize: theme.fontSizes[1],
195
+ }}
196
+ >
197
+ Cancel
198
+ </button>
199
+ <button
200
+ onClick={onSubmit}
201
+ disabled={!canSubmit || alreadyClaimed}
202
+ style={{
203
+ padding: '8px 14px',
204
+ background: !canSubmit || alreadyClaimed
205
+ ? theme.colors.backgroundSecondary
206
+ : theme.colors.primary,
207
+ color: !canSubmit || alreadyClaimed
208
+ ? theme.colors.muted
209
+ : theme.colors.textOnPrimary,
210
+ border: `1px solid ${theme.colors.border}`,
211
+ borderRadius: theme.radii[2],
212
+ cursor: !canSubmit || alreadyClaimed ? 'not-allowed' : 'pointer',
213
+ fontSize: theme.fontSizes[1],
214
+ fontWeight: theme.fontWeights.medium,
215
+ }}
216
+ >
217
+ {actionLabel}
218
+ </button>
219
+ </div>
220
+ </div>
221
+
222
+ <div
223
+ style={{
224
+ padding: '14px 18px',
225
+ overflowY: 'auto',
226
+ flex: 1,
227
+ }}
228
+ >
229
+ <div style={sectionLabelStyle}>Existing scopes (click to prefill)</div>
230
+ <div style={{ display: 'flex', flexDirection: 'column', gap: theme.space[3], marginTop: theme.space[2] }}>
231
+ {scopes.map(scope => (
232
+ <div key={scope.id}>
233
+ <div
234
+ style={{
235
+ fontFamily: theme.fonts.monospace,
236
+ fontSize: theme.fontSizes[0],
237
+ color: theme.colors.textSecondary,
238
+ marginBottom: 6,
239
+ }}
240
+ >
241
+ {scope.id}
242
+ </div>
243
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: theme.space[1] }}>
244
+ <button
245
+ onClick={() => onPickExisting(scope.id, '')}
246
+ title={
247
+ scope.paths.includes(path)
248
+ ? 'Scope already claims this path'
249
+ : 'Prefill (scope-level)'
250
+ }
251
+ style={{
252
+ fontSize: theme.fontSizes[0],
253
+ padding: '3px 7px',
254
+ background: scope.paths.includes(path)
255
+ ? theme.colors.background
256
+ : theme.colors.backgroundSecondary,
257
+ color: scope.paths.includes(path)
258
+ ? theme.colors.muted
259
+ : theme.colors.textSecondary,
260
+ border: `1px dashed ${theme.colors.muted}`,
261
+ borderRadius: theme.radii[1],
262
+ cursor: 'pointer',
263
+ display: 'flex',
264
+ alignItems: 'center',
265
+ gap: 5,
266
+ fontStyle: 'italic',
267
+ opacity: scope.paths.includes(path) ? 0.6 : 1,
268
+ }}
269
+ >
270
+ (scope-level)
271
+ {scope.paths.includes(path) && (
272
+ <span style={{ marginLeft: theme.space[1], fontSize: theme.fontSizes[0] }}>✓</span>
273
+ )}
274
+ </button>
275
+ {scope.namespaces.map(ns => {
276
+ const claims = ns.paths.includes(path);
277
+ return (
278
+ <button
279
+ key={ns.name}
280
+ onClick={() => onPickExisting(scope.id, ns.name)}
281
+ title={claims ? 'Already claims this path' : 'Prefill inputs'}
282
+ style={{
283
+ fontSize: theme.fontSizes[0],
284
+ padding: '3px 7px',
285
+ background: claims
286
+ ? theme.colors.background
287
+ : theme.colors.backgroundSecondary,
288
+ color: claims ? theme.colors.muted : theme.colors.text,
289
+ border: `1px solid ${theme.colors.border}`,
290
+ borderRadius: theme.radii[1],
291
+ cursor: 'pointer',
292
+ display: 'flex',
293
+ alignItems: 'center',
294
+ gap: 5,
295
+ opacity: claims ? 0.6 : 1,
296
+ }}
297
+ >
298
+ <span
299
+ style={{
300
+ width: 8,
301
+ height: 8,
302
+ borderRadius: theme.radii[1],
303
+ background: ns.color,
304
+ flexShrink: 0,
305
+ }}
306
+ />
307
+ {ns.name}
308
+ {claims && <span style={{ marginLeft: theme.space[1], fontSize: theme.fontSizes[0] }}>✓</span>}
309
+ </button>
310
+ );
311
+ })}
312
+ </div>
313
+ </div>
314
+ ))}
315
+ </div>
316
+ </div>
317
+ </div>
318
+ </div>
319
+ );
320
+ };