@contentful/experience-design-system-cli 2.11.4-dev-build-d47ab65.0 → 2.12.1-dev-build-589b615.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +214 -357
- package/dist/package.json +1 -1
- package/dist/src/analyze/extract/astro.js +9 -0
- package/dist/src/analyze/extract/react.js +23 -0
- package/dist/src/analyze/extract/stencil.js +3 -0
- package/dist/src/analyze/extract/vue.js +6 -0
- package/dist/src/analyze/extract/web-components.js +10 -0
- package/dist/src/analyze/pre-classify.d.ts +1 -1
- package/dist/src/analyze/pre-classify.js +3 -3
- package/dist/src/analyze/select/persistence.d.ts +13 -0
- package/dist/src/analyze/select/persistence.js +38 -0
- package/dist/src/analyze/select/preview-annotations.d.ts +26 -0
- package/dist/src/analyze/select/preview-annotations.js +62 -0
- package/dist/src/analyze/select/tui/App.js +5 -2
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +30 -0
- package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +137 -0
- package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +58 -1
- package/dist/src/analyze/select/tui/components/FieldEditor.js +637 -161
- package/dist/src/analyze/select/tui/components/FinalizeDialog.js +1 -1
- package/dist/src/analyze/select/tui/components/JsonPanel.js +5 -1
- package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +34 -0
- package/dist/src/analyze/select/tui/components/RationalePanel.js +92 -0
- package/dist/src/analyze/select/tui/components/Sidebar.d.ts +13 -1
- package/dist/src/analyze/select/tui/components/Sidebar.js +27 -3
- package/dist/src/analyze/select/tui/hooks/scroll-offset.d.ts +29 -0
- package/dist/src/analyze/select/tui/hooks/scroll-offset.js +26 -0
- package/dist/src/analyze/select-agent/command.d.ts +2 -0
- package/dist/src/analyze/select-agent/command.js +259 -45
- package/dist/src/analyze/select-agent/show-rationale.d.ts +35 -0
- package/dist/src/analyze/select-agent/show-rationale.js +92 -0
- package/dist/src/apply/command.js +7 -3
- package/dist/src/apply/tui/ServerApplyView.d.ts +3 -1
- package/dist/src/apply/tui/ServerApplyView.js +3 -2
- package/dist/src/credentials-store.d.ts +5 -0
- package/dist/src/credentials-store.js +7 -1
- package/dist/src/generate/agent-runner.d.ts +14 -0
- package/dist/src/generate/agent-runner.js +16 -0
- package/dist/src/generate/command.d.ts +6 -0
- package/dist/src/generate/command.js +45 -10
- package/dist/src/generate/progress.d.ts +9 -0
- package/dist/src/generate/progress.js +11 -0
- package/dist/src/generate/prompt-builder.d.ts +8 -0
- package/dist/src/generate/prompt-builder.js +22 -11
- package/dist/src/import/agent-model-resolve.d.ts +23 -0
- package/dist/src/import/agent-model-resolve.js +35 -0
- package/dist/src/import/auto-filter-resolve.d.ts +12 -0
- package/dist/src/import/auto-filter-resolve.js +16 -0
- package/dist/src/import/command.js +248 -8
- package/dist/src/import/orchestrator.d.ts +2 -0
- package/dist/src/import/orchestrator.js +11 -3
- package/dist/src/import/print-prompt.d.ts +35 -0
- package/dist/src/import/print-prompt.js +31 -0
- package/dist/src/import/tui/CustomPromptBanner.d.ts +11 -0
- package/dist/src/import/tui/CustomPromptBanner.js +7 -0
- package/dist/src/import/tui/WizardApp.d.ts +112 -8
- package/dist/src/import/tui/WizardApp.js +814 -333
- package/dist/src/import/tui/auto-filter-error.d.ts +2 -0
- package/dist/src/import/tui/auto-filter-error.js +29 -0
- package/dist/src/import/tui/final-review-host.d.ts +15 -0
- package/dist/src/import/tui/final-review-host.js +20 -0
- package/dist/src/import/tui/push-decision-gate-helpers.d.ts +8 -0
- package/dist/src/import/tui/push-decision-gate-helpers.js +8 -0
- package/dist/src/import/tui/push-progress.d.ts +23 -0
- package/dist/src/import/tui/push-progress.js +14 -0
- package/dist/src/import/tui/run-print-files-helpers.d.ts +26 -0
- package/dist/src/import/tui/run-print-files-helpers.js +8 -0
- package/dist/src/import/tui/run-teaser.d.ts +5 -0
- package/dist/src/import/tui/run-teaser.js +9 -0
- package/dist/src/import/tui/runLivePreview.d.ts +35 -0
- package/dist/src/import/tui/runLivePreview.js +73 -0
- package/dist/src/import/tui/runScopeGate.d.ts +22 -0
- package/dist/src/import/tui/runScopeGate.js +28 -0
- package/dist/src/import/tui/scope-gate-host.d.ts +21 -0
- package/dist/src/import/tui/scope-gate-host.js +20 -0
- package/dist/src/import/tui/spawn-generate.d.ts +33 -0
- package/dist/src/import/tui/spawn-generate.js +53 -0
- package/dist/src/import/tui/steps/CredentialsStep.d.ts +25 -1
- package/dist/src/import/tui/steps/CredentialsStep.js +31 -3
- package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
- package/dist/src/import/tui/steps/DoneStep.js +10 -4
- package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +29 -2
- package/dist/src/import/tui/steps/GenerateReviewStep.js +412 -48
- package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +19 -0
- package/dist/src/import/tui/steps/PushDecisionGateStep.js +76 -0
- package/dist/src/import/tui/steps/PushingStep.d.ts +10 -0
- package/dist/src/import/tui/steps/PushingStep.js +40 -0
- package/dist/src/import/tui/steps/ScopeGateStep.d.ts +23 -0
- package/dist/src/import/tui/steps/ScopeGateStep.js +243 -0
- package/dist/src/import/tui/steps/TokenInputStep.js +20 -3
- package/dist/src/import/tui/useLivePreview.d.ts +35 -0
- package/dist/src/import/tui/useLivePreview.js +120 -0
- package/dist/src/import/tui/wizard-generate-progress.d.ts +21 -0
- package/dist/src/import/tui/wizard-generate-progress.js +25 -0
- package/dist/src/import/tui/wizard-save-flow.d.ts +37 -0
- package/dist/src/import/tui/wizard-save-flow.js +20 -0
- package/dist/src/import/tui/wizard-state-transitions.d.ts +80 -0
- package/dist/src/import/tui/wizard-state-transitions.js +79 -0
- package/dist/src/lib/contentful-urls.d.ts +34 -0
- package/dist/src/lib/contentful-urls.js +41 -0
- package/dist/src/program.js +2 -0
- package/dist/src/runs/export-helpers.d.ts +30 -0
- package/dist/src/runs/export-helpers.js +59 -0
- package/dist/src/runs/fingerprint.d.ts +47 -0
- package/dist/src/runs/fingerprint.js +75 -0
- package/dist/src/runs/ls-command.d.ts +12 -0
- package/dist/src/runs/ls-command.js +137 -0
- package/dist/src/runs/modify-launcher.d.ts +24 -0
- package/dist/src/runs/modify-launcher.js +36 -0
- package/dist/src/runs/path-prompt.d.ts +8 -0
- package/dist/src/runs/path-prompt.js +72 -0
- package/dist/src/runs/push-creds-prompt.d.ts +25 -0
- package/dist/src/runs/push-creds-prompt.js +39 -0
- package/dist/src/runs/push-helpers.d.ts +29 -0
- package/dist/src/runs/push-helpers.js +69 -0
- package/dist/src/runs/replay-helpers.d.ts +67 -0
- package/dist/src/runs/replay-helpers.js +148 -0
- package/dist/src/runs/resolve-run-target.d.ts +12 -0
- package/dist/src/runs/resolve-run-target.js +45 -0
- package/dist/src/runs/run-picker-mount.d.ts +46 -0
- package/dist/src/runs/run-picker-mount.js +72 -0
- package/dist/src/runs/run-picker.d.ts +17 -0
- package/dist/src/runs/run-picker.js +145 -0
- package/dist/src/runs/save-conflict.d.ts +8 -0
- package/dist/src/runs/save-conflict.js +56 -0
- package/dist/src/runs/save-path-resolver.d.ts +55 -0
- package/dist/src/runs/save-path-resolver.js +71 -0
- package/dist/src/runs/staleness.d.ts +38 -0
- package/dist/src/runs/staleness.js +140 -0
- package/dist/src/runs/store.d.ts +78 -0
- package/dist/src/runs/store.js +136 -0
- package/dist/src/session/cache-keys.d.ts +15 -0
- package/dist/src/session/cache-keys.js +38 -0
- package/dist/src/session/db.d.ts +89 -3
- package/dist/src/session/db.js +447 -104
- package/dist/src/setup/auto-filter-prompt.d.ts +13 -0
- package/dist/src/setup/auto-filter-prompt.js +24 -0
- package/dist/src/setup/command.d.ts +7 -0
- package/dist/src/setup/command.js +58 -2
- package/dist/src/types.d.ts +6 -0
- package/package.json +2 -2
- package/skills/generate-components.md +54 -6
- package/skills/select-components.md +13 -4
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
3
|
import { Box, Text } from 'ink';
|
|
4
4
|
import { CDF_PROPERTY_TYPES, CDF_PROPERTY_CATEGORIES, DESIGN_TOKEN_TYPES, } from '@contentful/experience-design-system-types';
|
|
5
5
|
import { useImmediateInput } from '../hooks/useImmediateInput.js';
|
|
6
|
+
import { computeNextScrollOffset } from '../hooks/scroll-offset.js';
|
|
6
7
|
// Section indices for top-level navigation
|
|
7
8
|
// 0 = component $description row
|
|
8
9
|
// 1 = $properties header (then props[0..n-1])
|
|
@@ -39,9 +40,21 @@ function parseToState(json) {
|
|
|
39
40
|
const rawProps = (component.$properties ?? {});
|
|
40
41
|
const props = Object.entries(rawProps).map(([name, raw]) => {
|
|
41
42
|
const p = (raw ?? {});
|
|
43
|
+
const type = CDF_PROPERTY_TYPES.includes(p.$type) ? p.$type : 'string';
|
|
44
|
+
let defaultValue = null;
|
|
45
|
+
if (p.$default !== undefined && p.$default !== null) {
|
|
46
|
+
if (type === 'boolean') {
|
|
47
|
+
defaultValue = typeof p.$default === 'boolean' ? p.$default : null;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Store as string for string/number/token/enum. Numbers/JSON values
|
|
51
|
+
// are stringified — downstream serialization re-emits the string.
|
|
52
|
+
defaultValue = typeof p.$default === 'string' ? p.$default : String(p.$default);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
42
55
|
return {
|
|
43
56
|
name,
|
|
44
|
-
type
|
|
57
|
+
type,
|
|
45
58
|
category: CDF_PROPERTY_CATEGORIES.includes(p.$category)
|
|
46
59
|
? p.$category
|
|
47
60
|
: 'content',
|
|
@@ -49,6 +62,7 @@ function parseToState(json) {
|
|
|
49
62
|
description: typeof p.$description === 'string' ? p.$description : '',
|
|
50
63
|
values: Array.isArray(p.$values) ? p.$values.filter((v) => typeof v === 'string') : [],
|
|
51
64
|
tokenKind: typeof p['$token.kind'] === 'string' ? p['$token.kind'] : '',
|
|
65
|
+
default: defaultValue,
|
|
52
66
|
};
|
|
53
67
|
});
|
|
54
68
|
const rawSlots = (component.$slots ?? {});
|
|
@@ -58,6 +72,9 @@ function parseToState(json) {
|
|
|
58
72
|
name,
|
|
59
73
|
description: typeof s.$description === 'string' ? s.$description : '',
|
|
60
74
|
required: s.$required === true,
|
|
75
|
+
allowedComponents: Array.isArray(s.$allowedComponents)
|
|
76
|
+
? s.$allowedComponents.filter((v) => typeof v === 'string')
|
|
77
|
+
: [],
|
|
61
78
|
};
|
|
62
79
|
});
|
|
63
80
|
return { state: { componentDescription, props, slots }, error: null };
|
|
@@ -92,6 +109,19 @@ function serializeState(state, originalJson) {
|
|
|
92
109
|
def.$values = p.values;
|
|
93
110
|
if (p.type === 'token' && p.tokenKind)
|
|
94
111
|
def['$token.kind'] = p.tokenKind;
|
|
112
|
+
// $default — gated per type so e.g. boolean props don't get string defaults.
|
|
113
|
+
// Empty string == unset for text-typed defaults. richtext/media/link don't
|
|
114
|
+
// emit a default (defaults aren't meaningful for those types).
|
|
115
|
+
if (p.default !== null) {
|
|
116
|
+
if (p.type === 'boolean' && typeof p.default === 'boolean') {
|
|
117
|
+
def.$default = p.default;
|
|
118
|
+
}
|
|
119
|
+
else if ((p.type === 'string' || p.type === 'token' || p.type === 'enum') &&
|
|
120
|
+
typeof p.default === 'string' &&
|
|
121
|
+
p.default !== '') {
|
|
122
|
+
def.$default = p.default;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
95
125
|
$properties[p.name] = def;
|
|
96
126
|
}
|
|
97
127
|
const entry = {
|
|
@@ -108,6 +138,8 @@ function serializeState(state, originalJson) {
|
|
|
108
138
|
slotDef.$description = s.description;
|
|
109
139
|
if (s.required)
|
|
110
140
|
slotDef.$required = true;
|
|
141
|
+
if (s.allowedComponents.length > 0)
|
|
142
|
+
slotDef.$allowedComponents = s.allowedComponents;
|
|
111
143
|
entry.$slots[s.name] = slotDef;
|
|
112
144
|
}
|
|
113
145
|
}
|
|
@@ -123,53 +155,168 @@ function Picker({ value, active }) {
|
|
|
123
155
|
function Toggle({ value, active }) {
|
|
124
156
|
return (_jsx(Box, { children: _jsx(Text, { color: active ? 'cyan' : value ? 'green' : undefined, children: value ? '[✓]' : '[ ]' }) }));
|
|
125
157
|
}
|
|
126
|
-
function
|
|
158
|
+
function DefaultSubRow({ prop, active, textCursor, cursorVisible, }) {
|
|
159
|
+
const cursor = cursorVisible ? '█' : ' ';
|
|
160
|
+
// richtext/media/link don't support defaults — render an inert dim line.
|
|
161
|
+
if (prop.type === 'richtext' || prop.type === 'media' || prop.type === 'link') {
|
|
162
|
+
return (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: "default:" }), _jsx(Text, { dimColor: true, children: "(not applicable)" })] }));
|
|
163
|
+
}
|
|
164
|
+
// boolean — tri-state picker: true | false | (unset).
|
|
165
|
+
if (prop.type === 'boolean') {
|
|
166
|
+
const display = prop.default === true ? 'true' : prop.default === false ? 'false' : '(unset)';
|
|
167
|
+
return (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: "default:" }), active ? _jsx(Picker, { value: display, active: true }) : _jsx(Text, { color: "white", children: display })] }));
|
|
168
|
+
}
|
|
169
|
+
// enum — picker over prop.values plus (unset).
|
|
170
|
+
if (prop.type === 'enum') {
|
|
171
|
+
if (prop.values.length === 0) {
|
|
172
|
+
return (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: "default:" }), _jsx(Text, { dimColor: true, children: "(no values defined)" })] }));
|
|
173
|
+
}
|
|
174
|
+
const display = typeof prop.default === 'string' && prop.default !== '' ? prop.default : '(unset)';
|
|
175
|
+
return (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: "default:" }), active ? _jsx(Picker, { value: display, active: true }) : _jsx(Text, { color: "white", children: display })] }));
|
|
176
|
+
}
|
|
177
|
+
// string / number / token — text input. Active state shows bordered cyan
|
|
178
|
+
// box analogous to the description editor.
|
|
179
|
+
const value = typeof prop.default === 'string' ? prop.default : '';
|
|
180
|
+
if (active) {
|
|
181
|
+
return (_jsxs(Box, { paddingLeft: 2, flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: "default:" }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { children: value.slice(0, textCursor) }), _jsx(Text, { inverse: cursorVisible, children: value[textCursor] ?? cursor }), _jsx(Text, { children: value.slice(textCursor + 1) })] })] }));
|
|
182
|
+
}
|
|
183
|
+
return (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: "default:" }), _jsx(Text, { color: value ? 'white' : undefined, dimColor: !value, children: value || '(none)' })] }));
|
|
184
|
+
}
|
|
185
|
+
function PropRow({ prop, selected, activeField, textCursor, valueCursor, cursorVisible, editingValue, valueText, width, rationale, rowKey, }) {
|
|
127
186
|
const cursor = cursorVisible ? '█' : ' ';
|
|
128
187
|
const bg = selected ? 'blue' : undefined;
|
|
188
|
+
const descActive = activeField === 'description';
|
|
129
189
|
// Name column — fixed 14 chars
|
|
130
190
|
const nameDisplay = prop.name.length > 14 ? prop.name.slice(0, 13) + '…' : prop.name.padEnd(14);
|
|
131
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: selected ? 'white' : 'cyan', bold: selected, backgroundColor: bg, children: [' ', nameDisplay, ' '] }), _jsx(Text, { dimColor: !selected, children: "type:" }), activeField === 'type' ? (_jsx(Picker, { value: prop.type, active: true })) : (_jsx(Text, { color: selected ? 'yellow' : 'white', children: prop.type })), _jsx(Text, { dimColor: !selected, children: "cat:" }), activeField === 'category' ? (_jsx(Picker, { value: prop.category, active: true })) : (_jsx(Text, { color: selected ? 'magenta' : 'white', children: prop.category })), _jsx(Text, { dimColor: !selected, children: "req:" }), activeField === 'required' ? (_jsx(Toggle, { value: prop.required, active: true })) : (_jsx(Toggle, { value: prop.required, active: false })), prop.type === 'token' && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: !selected, children: "kind:" }), activeField === 'tokenKind' ? (_jsx(Picker, { value: prop.tokenKind || DESIGN_TOKEN_TYPES[0], active: true })) : (_jsx(Text, { color: selected ? 'green' : 'white', children: prop.tokenKind || '—' }))] }))] }), selected && (_jsxs(Box, { paddingLeft: 2,
|
|
191
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: selected ? 'white' : 'cyan', bold: selected, backgroundColor: bg, children: [' ', nameDisplay, ' '] }), _jsx(Text, { dimColor: !selected, children: "type:" }), activeField === 'type' ? (_jsx(Picker, { value: prop.type, active: true })) : (_jsx(Text, { color: selected ? 'yellow' : 'white', children: prop.type })), _jsx(Text, { dimColor: !selected, children: "cat:" }), activeField === 'category' ? (_jsx(Picker, { value: prop.category, active: true })) : (_jsx(Text, { color: selected ? 'magenta' : 'white', children: prop.category })), _jsx(Text, { dimColor: !selected, children: "req:" }), activeField === 'required' ? (_jsx(Toggle, { value: prop.required, active: true })) : (_jsx(Toggle, { value: prop.required, active: false })), prop.type === 'token' && (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: !selected, children: "kind:" }), activeField === 'tokenKind' ? (_jsx(Picker, { value: prop.tokenKind || DESIGN_TOKEN_TYPES[0], active: true })) : (_jsx(Text, { color: selected ? 'green' : 'white', children: prop.tokenKind || '—' }))] }))] }), selected && (_jsx(DefaultSubRow, { prop: prop, active: activeField === 'default', textCursor: textCursor, cursorVisible: cursorVisible })), selected && descActive && (_jsxs(Box, { paddingLeft: 2, flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: "desc:" }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { children: prop.description.slice(0, textCursor) }), _jsx(Text, { inverse: cursorVisible, children: prop.description[textCursor] ?? cursor }), _jsx(Text, { children: prop.description.slice(textCursor + 1) })] })] })), selected && !descActive && (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: "desc:" }), _jsx(Text, { color: "green", children: prop.description || '—' })] })), selected && rationale && rationale.trim().length > 0 && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: (() => {
|
|
192
|
+
const max = Math.max(8, width - 8);
|
|
193
|
+
const text = `~ ${rationale}`;
|
|
194
|
+
return text.length > max ? text.slice(0, max - 1) + '…' : text;
|
|
195
|
+
})() }) }, rowKey ? `rationale-${rowKey}` : undefined)), selected && prop.type === 'enum' && (_jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "values:" }), activeField === 'values' && (_jsx(Text, { dimColor: true, children: ' [a]dd [e]dit [r]emove [↑↓] navigate [K/J] reorder' }))] }), prop.values.length === 0 && !editingValue && _jsx(Text, { dimColor: true, children: " (none \u2014 press [a] to add)" }), prop.values.map((v, i) => {
|
|
196
|
+
const isActiveCursor = activeField === 'values' && valueCursor === i;
|
|
197
|
+
const isBeingEdited = editingValue?.mode === 'edit' && editingValue.index === i;
|
|
198
|
+
if (isBeingEdited) {
|
|
199
|
+
return (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: '✎ ' }), _jsx(Text, { children: valueText }), _jsx(Text, { inverse: cursorVisible, children: " " })] }, i));
|
|
200
|
+
}
|
|
201
|
+
return (_jsx(Box, { gap: 1, paddingLeft: 2, children: _jsx(Text, { color: isActiveCursor ? 'cyan' : 'white', children: isActiveCursor ? `▶ ${v}` : ` ${v}` }) }, i));
|
|
202
|
+
}), editingValue?.mode === 'add' && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: '+ ' }), _jsx(Text, { children: valueText }), _jsx(Text, { inverse: cursorVisible, children: " " })] }))] }))] }));
|
|
132
203
|
}
|
|
133
|
-
function SlotRow({ slot, selected, activeField, textCursor, cursorVisible, width, }) {
|
|
204
|
+
function SlotRow({ slot, selected, activeField, textCursor, valueCursor, cursorVisible, editingValue, valueText, width, }) {
|
|
134
205
|
const cursor = cursorVisible ? '█' : ' ';
|
|
135
206
|
const bg = selected ? 'blue' : undefined;
|
|
136
207
|
const nameDisplay = slot.name.length > 14 ? slot.name.slice(0, 13) + '…' : slot.name.padEnd(14);
|
|
137
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: selected ? 'white' : 'cyan', bold: selected, backgroundColor: bg, children: [' ', nameDisplay, ' '] }), _jsx(Text, { dimColor: !selected, children: "req:" }), activeField === 'required' ? (_jsx(Toggle, { value: slot.required, active: true })) : (_jsx(Toggle, { value: slot.required, active: false }))] }), selected && (_jsxs(Box, { paddingLeft: 2,
|
|
208
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, children: [_jsxs(Box, { gap: 1, children: [_jsxs(Text, { color: selected ? 'white' : 'cyan', bold: selected, backgroundColor: bg, children: [' ', nameDisplay, ' '] }), _jsx(Text, { dimColor: !selected, children: "req:" }), activeField === 'required' ? (_jsx(Toggle, { value: slot.required, active: true })) : (_jsx(Toggle, { value: slot.required, active: false }))] }), selected && (_jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: "allowed:" }), activeField === 'allowedComponents' && (_jsx(Text, { dimColor: true, children: ' [a]dd [e]dit [r]emove [↑↓] navigate [K/J] reorder' }))] }), slot.allowedComponents.length === 0 && !editingValue && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: activeField === 'allowedComponents' ? '(any — press [a] to add)' : '(any)' }) })), slot.allowedComponents.map((v, i) => {
|
|
209
|
+
const isActiveCursor = activeField === 'allowedComponents' && valueCursor === i;
|
|
210
|
+
const isBeingEdited = editingValue?.mode === 'edit' && editingValue.index === i;
|
|
211
|
+
if (isBeingEdited) {
|
|
212
|
+
return (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: '✎ ' }), _jsx(Text, { children: valueText }), _jsx(Text, { inverse: cursorVisible, children: " " })] }, i));
|
|
213
|
+
}
|
|
214
|
+
return (_jsx(Box, { gap: 1, paddingLeft: 2, children: _jsx(Text, { color: isActiveCursor ? 'cyan' : 'white', children: isActiveCursor ? `▶ ${v}` : ` ${v}` }) }, i));
|
|
215
|
+
}), editingValue?.mode === 'add' && activeField === 'allowedComponents' && (_jsxs(Box, { paddingLeft: 2, children: [_jsx(Text, { color: "cyan", children: '+ ' }), _jsx(Text, { children: valueText }), _jsx(Text, { inverse: cursorVisible, children: " " })] }))] })), selected && activeField === 'description' && (_jsxs(Box, { paddingLeft: 2, flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: "desc:" }), _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { children: slot.description.slice(0, textCursor) }), _jsx(Text, { inverse: cursorVisible, children: slot.description[textCursor] ?? cursor }), _jsx(Text, { children: slot.description.slice(textCursor + 1) })] })] })), selected && activeField !== 'description' && (_jsxs(Box, { paddingLeft: 2, gap: 1, children: [_jsx(Text, { dimColor: true, children: "desc:" }), _jsx(Text, { color: "green", children: slot.description || '—' })] }))] }));
|
|
138
216
|
}
|
|
139
|
-
|
|
217
|
+
// Field order is intentional: description is LAST so it's the "edge" of the
|
|
218
|
+
// field cycle. The user reaches description by walking through type → category
|
|
219
|
+
// → required → [tokenKind?] → [values?] → [default?] → description via j/k;
|
|
220
|
+
// once active, description swallows j/k as literal text input. Putting it last
|
|
221
|
+
// means the user has to deliberately navigate to it, avoiding accidental
|
|
222
|
+
// text-entry. `default` slots in BEFORE description for the same reason —
|
|
223
|
+
// description-as-last is invariant. richtext/media/link omit `default` because
|
|
224
|
+
// defaults aren't meaningful for those types (per spec D1).
|
|
140
225
|
function propFields(prop) {
|
|
141
|
-
const fields = [
|
|
226
|
+
const fields = ['type', 'category', 'required'];
|
|
142
227
|
if (prop.type === 'token')
|
|
143
|
-
fields.
|
|
228
|
+
fields.push('tokenKind');
|
|
144
229
|
if (prop.type === 'enum')
|
|
145
230
|
fields.push('values');
|
|
231
|
+
if (prop.type !== 'richtext' && prop.type !== 'media' && prop.type !== 'link') {
|
|
232
|
+
fields.push('default');
|
|
233
|
+
}
|
|
234
|
+
fields.push('description');
|
|
146
235
|
return fields;
|
|
147
236
|
}
|
|
148
|
-
|
|
237
|
+
// Slot fields: description stays last for the same invariant reason. Allowed-
|
|
238
|
+
// components is a list-typed field that swallows j/k similarly to enum $values
|
|
239
|
+
// — placing it before description preserves description-as-last.
|
|
240
|
+
const SLOT_FIELDS = ['required', 'allowedComponents', 'description'];
|
|
149
241
|
// ── Main component ────────────────────────────────────────────────────────────
|
|
150
|
-
export function FieldEditor({ value, width, height, onChange, onSave, onDiscard, }) {
|
|
242
|
+
export function FieldEditor({ value, width, height, active = true, onChange, onSave, onDiscard, onExit, metadata, onTogglePropRationale, onToggleComponentRationale, onToggleSourceExternal, onTextEntryActiveChange, }) {
|
|
151
243
|
const { state: initialState, error: parseError } = parseToState(value);
|
|
152
244
|
const [editorState, setEditorState] = useState(initialState);
|
|
153
245
|
const [parseErr] = useState(parseError);
|
|
154
|
-
// Navigation state
|
|
155
|
-
|
|
246
|
+
// Navigation state — initial state lands at the row level with NO field
|
|
247
|
+
// auto-active. The user presses Return to enter field-edit at the first
|
|
248
|
+
// field of the row (type), then j/k to walk through fields uniformly:
|
|
249
|
+
// type → category → required → [tokenKind?] → [values?] → description.
|
|
250
|
+
// Description is reached via navigation, not auto-focus — this avoids
|
|
251
|
+
// trapping the user in description-edit (where j/k type literals).
|
|
252
|
+
const initialFocus = (() => {
|
|
253
|
+
if (initialState.props.length > 0) {
|
|
254
|
+
return {
|
|
255
|
+
focusLevel: 'prop',
|
|
256
|
+
inSlots: false,
|
|
257
|
+
activeField: null,
|
|
258
|
+
textCursor: 0,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (initialState.slots.length > 0) {
|
|
262
|
+
return {
|
|
263
|
+
focusLevel: 'slot',
|
|
264
|
+
inSlots: true,
|
|
265
|
+
activeField: null,
|
|
266
|
+
textCursor: 0,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
focusLevel: 'prop',
|
|
271
|
+
inSlots: false,
|
|
272
|
+
activeField: null,
|
|
273
|
+
textCursor: 0,
|
|
274
|
+
};
|
|
275
|
+
})();
|
|
276
|
+
const [focusLevel, setFocusLevel] = useState(initialFocus.focusLevel);
|
|
156
277
|
const [propIdx, setPropIdx] = useState(0);
|
|
157
278
|
const [slotIdx, setSlotIdx] = useState(0);
|
|
158
|
-
// Whether we're navigating props (
|
|
159
|
-
const [inSlots, setInSlots] = useState(
|
|
279
|
+
// Whether we're navigating props (false) or slots (true) at the top level
|
|
280
|
+
const [inSlots, setInSlots] = useState(initialFocus.inSlots);
|
|
281
|
+
// True when the active focus is the component-level $description row (rather
|
|
282
|
+
// than a prop/slot row). Lives parallel to inSlots — they're mutually exclusive.
|
|
283
|
+
const [inComponentDesc, setInComponentDesc] = useState(false);
|
|
160
284
|
// Active field within a prop/slot
|
|
161
|
-
const [activeField, setActiveField] = useState(
|
|
285
|
+
const [activeField, setActiveField] = useState(initialFocus.activeField);
|
|
162
286
|
// Text cursor position for description fields
|
|
163
|
-
const [textCursor, setTextCursor] = useState(
|
|
287
|
+
const [textCursor, setTextCursor] = useState(initialFocus.textCursor);
|
|
164
288
|
// Value list cursor for $values editing
|
|
165
289
|
const [valueCursor, setValueCursor] = useState(0);
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
const [
|
|
290
|
+
// Inline text-entry mode for adding/editing a $values entry.
|
|
291
|
+
// mode='add' — append on Enter; mode='edit' — replace at index on Enter.
|
|
292
|
+
const [editingValue, setEditingValue] = useState(null);
|
|
293
|
+
const [valueText, setValueText] = useState('');
|
|
169
294
|
const [validationError, setValidationError] = useState(null);
|
|
170
295
|
const [cursorVisible] = useState(true);
|
|
296
|
+
// Feature 1: source-view panel toggle. Opens with `s`, closes with `s` or Esc.
|
|
297
|
+
// When open, Esc closes the panel only (does not bubble to onExit).
|
|
298
|
+
const [sourceOpen, setSourceOpen] = useState(false);
|
|
299
|
+
// Feature 11: rationale panel toggle (`i`). Mutually exclusive with the
|
|
300
|
+
// source panel — opening one closes the other.
|
|
301
|
+
const [rationaleOpen, setRationaleOpen] = useState(false);
|
|
302
|
+
const [rationaleScrollOffset, setRationaleScrollOffset] = useState(0);
|
|
303
|
+
// Discoverability overlay. `?` toggles a modal listing every wired
|
|
304
|
+
// keybinding grouped by context. While open, every other handler is inert
|
|
305
|
+
// — only `?` and Esc respond. Esc here closes the overlay and does NOT
|
|
306
|
+
// bubble to onExit.
|
|
307
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
171
308
|
const props = editorState.props;
|
|
172
309
|
const slots = editorState.slots;
|
|
310
|
+
// Report text-entry-active state to parent so it can gate top-level `i`/`I`/`s`
|
|
311
|
+
// keybinds against literal keystrokes inside any text-entry surface.
|
|
312
|
+
const textEntryActive = (focusLevel === 'field' && activeField === 'description') ||
|
|
313
|
+
(focusLevel === 'field' &&
|
|
314
|
+
activeField === 'default' &&
|
|
315
|
+
(editorState.props[propIdx]?.type === 'string' || editorState.props[propIdx]?.type === 'token')) ||
|
|
316
|
+
editingValue != null;
|
|
317
|
+
React.useEffect(() => {
|
|
318
|
+
onTextEntryActiveChange?.(textEntryActive);
|
|
319
|
+
}, [textEntryActive, onTextEntryActiveChange]);
|
|
173
320
|
const currentProp = props[propIdx] ?? null;
|
|
174
321
|
const currentSlot = slots[slotIdx] ?? null;
|
|
175
322
|
// Emit serialized JSON whenever state changes
|
|
@@ -178,58 +325,213 @@ export function FieldEditor({ value, width, height, onChange, onSave, onDiscard,
|
|
|
178
325
|
onChange(serializeState(next, value));
|
|
179
326
|
};
|
|
180
327
|
useImmediateInput((input, key) => {
|
|
328
|
+
if (!active)
|
|
329
|
+
return;
|
|
330
|
+
// ── Help overlay (`?`) — highest priority ───────────────────────────────
|
|
331
|
+
// When open, only `?` (toggle) and Esc (close) respond. All other input
|
|
332
|
+
// is swallowed so j/k/Enter/Ctrl+S can't move state behind the modal.
|
|
333
|
+
if (showHelp) {
|
|
334
|
+
if (input === '?' || key.escape) {
|
|
335
|
+
setShowHelp(false);
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// Open the overlay. Skip when in any inline text-entry context to keep
|
|
340
|
+
// `?` literal there: description text-entry, string-typed default
|
|
341
|
+
// text-entry, and the values/allowedComponents add/edit text-entry.
|
|
342
|
+
const inDescriptionTextEntryForHelp = focusLevel === 'field' && activeField === 'description';
|
|
343
|
+
const inStringDefaultTextEntryForHelp = focusLevel === 'field' &&
|
|
344
|
+
activeField === 'default' &&
|
|
345
|
+
currentProp != null &&
|
|
346
|
+
(currentProp.type === 'string' || currentProp.type === 'token');
|
|
347
|
+
const inValueListTextEntry = editingValue != null;
|
|
348
|
+
if (input === '?' &&
|
|
349
|
+
!key.ctrl &&
|
|
350
|
+
!key.meta &&
|
|
351
|
+
!inDescriptionTextEntryForHelp &&
|
|
352
|
+
!inStringDefaultTextEntryForHelp &&
|
|
353
|
+
!inValueListTextEntry) {
|
|
354
|
+
setShowHelp(true);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// ── Inline value text-entry (add or edit) ─ highest priority ───────────
|
|
358
|
+
// Handles enum prop $values AND slot $allowedComponents — both use the
|
|
359
|
+
// same add/edit/remove/reorder list shape.
|
|
360
|
+
const isPropValuesEntry = editingValue && currentProp && activeField === 'values' && !inSlots;
|
|
361
|
+
const isSlotAllowedEntry = editingValue && currentSlot && activeField === 'allowedComponents' && inSlots;
|
|
362
|
+
if (isPropValuesEntry || isSlotAllowedEntry) {
|
|
363
|
+
const vals = isPropValuesEntry ? currentProp.values : currentSlot.allowedComponents;
|
|
364
|
+
if (key.return) {
|
|
365
|
+
const trimmed = valueText.trim();
|
|
366
|
+
if (trimmed) {
|
|
367
|
+
let nextVals;
|
|
368
|
+
let cursorAfter;
|
|
369
|
+
if (editingValue.mode === 'add') {
|
|
370
|
+
nextVals = [...vals, trimmed];
|
|
371
|
+
cursorAfter = nextVals.length - 1;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
const idx = editingValue.index ?? 0;
|
|
375
|
+
nextVals = vals.map((v, i) => (i === idx ? trimmed : v));
|
|
376
|
+
cursorAfter = idx;
|
|
377
|
+
}
|
|
378
|
+
if (isPropValuesEntry) {
|
|
379
|
+
const nextProps = props.map((p, i) => (i === propIdx ? { ...p, values: nextVals } : p));
|
|
380
|
+
commit({ ...editorState, props: nextProps });
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
const nextSlots = slots.map((s, i) => (i === slotIdx ? { ...s, allowedComponents: nextVals } : s));
|
|
384
|
+
commit({ ...editorState, slots: nextSlots });
|
|
385
|
+
}
|
|
386
|
+
setValueCursor(cursorAfter);
|
|
387
|
+
}
|
|
388
|
+
setEditingValue(null);
|
|
389
|
+
setValueText('');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
if (key.escape) {
|
|
393
|
+
setEditingValue(null);
|
|
394
|
+
setValueText('');
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (key.backspace) {
|
|
398
|
+
setValueText((t) => t.slice(0, -1));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (input && input.length === 1 && !key.ctrl && !key.meta) {
|
|
402
|
+
setValueText((t) => t + input);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
181
407
|
// ── Save / Discard ───────────────────────────────────────────────────────
|
|
182
408
|
if (key.ctrl && input === 's') {
|
|
183
409
|
setValidationError(null);
|
|
184
410
|
onSave();
|
|
185
411
|
return;
|
|
186
412
|
}
|
|
413
|
+
// ── Feature 1: source-view panel toggle ─────────────────────────────────
|
|
414
|
+
// `s` (without Ctrl) opens/closes the source-view panel. Skip when in
|
|
415
|
+
// inline text-entry contexts (description text-entry, value-list edit).
|
|
416
|
+
// Description text-entry is gated by focusLevel === 'field' && activeField
|
|
417
|
+
// === 'description' — we guard against typing 's' as a literal there.
|
|
418
|
+
const inDescriptionTextEntry = focusLevel === 'field' && activeField === 'description';
|
|
419
|
+
const inComponentDescTextEntry = focusLevel === 'field' && inComponentDesc && activeField === 'description';
|
|
420
|
+
if (input === 's' && !key.ctrl && !key.meta && !inDescriptionTextEntry && !inComponentDescTextEntry) {
|
|
421
|
+
// When the parent owns the source panel, delegate; otherwise fall
|
|
422
|
+
// back to the internal toggle for backward compat.
|
|
423
|
+
if (onToggleSourceExternal) {
|
|
424
|
+
onToggleSourceExternal();
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
// Mutual exclusion with the rationale panel (Feature 11) — only when
|
|
428
|
+
// internally managed.
|
|
429
|
+
setRationaleOpen(false);
|
|
430
|
+
setRationaleScrollOffset(() => 0);
|
|
431
|
+
setSourceOpen((o) => !o);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
// ── Rationale-panel scroll while internally open (legacy path) ─────────
|
|
435
|
+
// When the parent does not own the panel (no onTogglePropRationale prop),
|
|
436
|
+
// FieldEditor preserves its legacy in-place panel for backward compat
|
|
437
|
+
// with mounts that pre-date the lifted-panel refactor.
|
|
438
|
+
if (rationaleOpen && !onTogglePropRationale) {
|
|
439
|
+
const PANEL_HEIGHT = 12;
|
|
440
|
+
const next = computeNextScrollOffset(rationaleScrollOffset, input, key, 9999, PANEL_HEIGHT);
|
|
441
|
+
if (next !== null) {
|
|
442
|
+
setRationaleScrollOffset(() => next);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
if (input === 'i' || key.escape) {
|
|
446
|
+
setRationaleOpen(false);
|
|
447
|
+
setRationaleScrollOffset(() => 0);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// ── Rationale + component-rationale keybinds (`i` / `I`) ────────────────
|
|
453
|
+
// `i`/`I` are literal in every text-entry context: description editor,
|
|
454
|
+
// component-description editor, string/token default editor, and the
|
|
455
|
+
// values/allowedComponents add/edit text-entry.
|
|
456
|
+
const inStringDefaultTextEntry = focusLevel === 'field' &&
|
|
457
|
+
activeField === 'default' &&
|
|
458
|
+
currentProp != null &&
|
|
459
|
+
(currentProp.type === 'string' || currentProp.type === 'token');
|
|
460
|
+
const rationaleKeyAllowed = !key.ctrl &&
|
|
461
|
+
!key.meta &&
|
|
462
|
+
!inDescriptionTextEntry &&
|
|
463
|
+
!inComponentDescTextEntry &&
|
|
464
|
+
!inStringDefaultTextEntry &&
|
|
465
|
+
!inValueListTextEntry;
|
|
466
|
+
if (input === 'i' && rationaleKeyAllowed) {
|
|
467
|
+
if (onTogglePropRationale) {
|
|
468
|
+
onTogglePropRationale();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
// Legacy in-place toggle (no parent callback wired).
|
|
472
|
+
setSourceOpen(false);
|
|
473
|
+
setRationaleScrollOffset(() => 0);
|
|
474
|
+
setRationaleOpen((o) => !o);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (input === 'I' && rationaleKeyAllowed && onToggleComponentRationale) {
|
|
478
|
+
onToggleComponentRationale();
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// ── Esc when source panel is open: close panel only, do not bubble ─────
|
|
482
|
+
if (key.escape && sourceOpen) {
|
|
483
|
+
setSourceOpen(false);
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
187
486
|
if (key.escape) {
|
|
188
487
|
if (focusLevel === 'field') {
|
|
189
|
-
// Exit field editing back to prop/slot row level
|
|
190
|
-
|
|
488
|
+
// Exit field editing back to prop/slot/component-description row level
|
|
489
|
+
if (inComponentDesc)
|
|
490
|
+
setFocusLevel('componentDescription');
|
|
491
|
+
else
|
|
492
|
+
setFocusLevel(inSlots ? 'slot' : 'prop');
|
|
191
493
|
setActiveField(null);
|
|
192
|
-
setAddingValue(false);
|
|
193
|
-
setNewValueText('');
|
|
194
494
|
return;
|
|
195
495
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
496
|
+
// Row-level Esc: bounce focus back out of the panel via onExit.
|
|
497
|
+
// Falls back to onDiscard when the caller hasn't wired onExit.
|
|
498
|
+
if (onExit) {
|
|
499
|
+
onExit();
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
onDiscard();
|
|
201
503
|
}
|
|
202
|
-
onDiscard();
|
|
203
504
|
return;
|
|
204
505
|
}
|
|
205
506
|
// ── Prop-level navigation (not inside a field) ───────────────────────────
|
|
507
|
+
// Arrows / j / k move between rows. Return enters field-edit at the FIRST
|
|
508
|
+
// field of the current prop (type). No auto-focus on description.
|
|
206
509
|
if (focusLevel === 'prop') {
|
|
207
510
|
if (key.upArrow || input === 'k') {
|
|
208
511
|
if (propIdx > 0) {
|
|
209
|
-
setPropIdx(
|
|
210
|
-
setActiveField(null);
|
|
512
|
+
setPropIdx(propIdx - 1);
|
|
211
513
|
}
|
|
212
|
-
else
|
|
213
|
-
|
|
214
|
-
setFocusLevel('
|
|
215
|
-
|
|
514
|
+
else {
|
|
515
|
+
// From prop[0], k enters the component-description row above.
|
|
516
|
+
setFocusLevel('componentDescription');
|
|
517
|
+
setInSlots(false);
|
|
518
|
+
setInComponentDesc(true);
|
|
216
519
|
}
|
|
217
520
|
return;
|
|
218
521
|
}
|
|
219
522
|
if (key.downArrow || input === 'j') {
|
|
220
523
|
if (propIdx < props.length - 1) {
|
|
221
|
-
setPropIdx(
|
|
222
|
-
setActiveField(null);
|
|
524
|
+
setPropIdx(propIdx + 1);
|
|
223
525
|
}
|
|
224
526
|
else if (slots.length > 0) {
|
|
225
527
|
setInSlots(true);
|
|
528
|
+
setSlotIdx(0);
|
|
226
529
|
setFocusLevel('slot');
|
|
227
|
-
setActiveField(null);
|
|
228
530
|
}
|
|
229
531
|
return;
|
|
230
532
|
}
|
|
231
533
|
if (key.return && currentProp) {
|
|
232
|
-
// Enter field editing on the first field of this prop
|
|
534
|
+
// Enter field editing on the first field of this prop (type).
|
|
233
535
|
setFocusLevel('field');
|
|
234
536
|
setActiveField(propFields(currentProp)[0] ?? null);
|
|
235
537
|
setTextCursor(currentProp.description.length);
|
|
@@ -237,25 +539,55 @@ export function FieldEditor({ value, width, height, onChange, onSave, onDiscard,
|
|
|
237
539
|
}
|
|
238
540
|
return;
|
|
239
541
|
}
|
|
542
|
+
// ── Component-description row navigation ─────────────────────────────────
|
|
543
|
+
if (focusLevel === 'componentDescription') {
|
|
544
|
+
if (key.upArrow || input === 'k') {
|
|
545
|
+
// Already at the top-most row — stay put.
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
if (key.downArrow || input === 'j') {
|
|
549
|
+
// Move down into the first prop row, or first slot row when no props.
|
|
550
|
+
if (props.length > 0) {
|
|
551
|
+
setFocusLevel('prop');
|
|
552
|
+
setPropIdx(0);
|
|
553
|
+
setInSlots(false);
|
|
554
|
+
setInComponentDesc(false);
|
|
555
|
+
}
|
|
556
|
+
else if (slots.length > 0) {
|
|
557
|
+
setFocusLevel('slot');
|
|
558
|
+
setSlotIdx(0);
|
|
559
|
+
setInSlots(true);
|
|
560
|
+
setInComponentDesc(false);
|
|
561
|
+
}
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
if (key.return) {
|
|
565
|
+
// Enter the single field — description text input.
|
|
566
|
+
setFocusLevel('field');
|
|
567
|
+
setActiveField('description');
|
|
568
|
+
setTextCursor(editorState.componentDescription.length);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
240
573
|
// ── Slot-level navigation ────────────────────────────────────────────────
|
|
574
|
+
// Arrows / j / k move between rows. Return enters field-edit at the FIRST
|
|
575
|
+
// field of the slot (required). No auto-focus on description.
|
|
241
576
|
if (focusLevel === 'slot') {
|
|
242
577
|
if (key.upArrow || input === 'k') {
|
|
243
578
|
if (slotIdx > 0) {
|
|
244
|
-
setSlotIdx(
|
|
245
|
-
setActiveField(null);
|
|
579
|
+
setSlotIdx(slotIdx - 1);
|
|
246
580
|
}
|
|
247
581
|
else if (props.length > 0) {
|
|
248
582
|
setInSlots(false);
|
|
249
|
-
setFocusLevel('prop');
|
|
250
583
|
setPropIdx(props.length - 1);
|
|
251
|
-
|
|
584
|
+
setFocusLevel('prop');
|
|
252
585
|
}
|
|
253
586
|
return;
|
|
254
587
|
}
|
|
255
588
|
if (key.downArrow || input === 'j') {
|
|
256
589
|
if (slotIdx < slots.length - 1) {
|
|
257
|
-
setSlotIdx(
|
|
258
|
-
setActiveField(null);
|
|
590
|
+
setSlotIdx(slotIdx + 1);
|
|
259
591
|
}
|
|
260
592
|
return;
|
|
261
593
|
}
|
|
@@ -271,32 +603,67 @@ export function FieldEditor({ value, width, height, onChange, onSave, onDiscard,
|
|
|
271
603
|
if (focusLevel === 'field') {
|
|
272
604
|
const fields = inSlots ? SLOT_FIELDS : currentProp ? propFields(currentProp) : [];
|
|
273
605
|
const currentFieldIdx = fields.indexOf(activeField);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
606
|
+
const isDescriptionTextEntry = activeField === 'description';
|
|
607
|
+
// String/token defaults are also text-entry; j/k must type literals
|
|
608
|
+
// there too (consistency with description). boolean/enum defaults use
|
|
609
|
+
// ←/→ pickers so j/k can keep cycling fields.
|
|
610
|
+
const isDefaultTextEntry = activeField === 'default' &&
|
|
611
|
+
currentProp != null &&
|
|
612
|
+
(currentProp.type === 'string' || currentProp.type === 'token');
|
|
613
|
+
const isValuesNav = activeField === 'values' || activeField === 'allowedComponents';
|
|
614
|
+
const arrowUp = key.upArrow;
|
|
615
|
+
const arrowDown = key.downArrow;
|
|
616
|
+
// ── Inside values: arrow keys AND j/k navigate BETWEEN VALUES, not
|
|
617
|
+
// between fields. Reorder is K/J (capital). Same logic for prop
|
|
618
|
+
// enum $values and slot $allowedComponents.
|
|
619
|
+
if (isValuesNav) {
|
|
620
|
+
const vals = activeField === 'values' && currentProp
|
|
621
|
+
? currentProp.values
|
|
622
|
+
: activeField === 'allowedComponents' && currentSlot
|
|
623
|
+
? currentSlot.allowedComponents
|
|
624
|
+
: null;
|
|
625
|
+
if (vals !== null) {
|
|
626
|
+
if (arrowUp || input === 'k') {
|
|
627
|
+
setValueCursor((c) => Math.max(0, c - 1));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (arrowDown || input === 'j') {
|
|
631
|
+
setValueCursor((c) => Math.max(0, Math.min(vals.length - 1, c + 1)));
|
|
632
|
+
return;
|
|
281
633
|
}
|
|
282
634
|
}
|
|
283
|
-
else {
|
|
284
|
-
// Back to row navigation
|
|
285
|
-
setFocusLevel(inSlots ? 'slot' : 'prop');
|
|
286
|
-
setActiveField(null);
|
|
287
|
-
}
|
|
288
|
-
return;
|
|
289
635
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
636
|
+
// ── Field cycling within the current prop/slot.
|
|
637
|
+
// Arrows always cycle (including from description, which means in
|
|
638
|
+
// description-active state arrows leave text-entry to navigate fields
|
|
639
|
+
// of the SAME prop). j/k only cycle when NOT in description, so that
|
|
640
|
+
// description preserves literal text-entry for those characters. Use
|
|
641
|
+
// Esc to leave the current prop and return to row-level navigation.
|
|
642
|
+
if (!isValuesNav) {
|
|
643
|
+
const navUp = arrowUp || (!isDescriptionTextEntry && !isDefaultTextEntry && input === 'k');
|
|
644
|
+
const navDown = arrowDown || (!isDescriptionTextEntry && !isDefaultTextEntry && input === 'j');
|
|
645
|
+
if ((navUp || navDown) && fields.length > 0) {
|
|
646
|
+
const lastIdx = fields.length - 1;
|
|
647
|
+
const targetIdx = navDown
|
|
648
|
+
? currentFieldIdx >= lastIdx
|
|
649
|
+
? 0
|
|
650
|
+
: currentFieldIdx + 1
|
|
651
|
+
: currentFieldIdx <= 0
|
|
652
|
+
? lastIdx
|
|
653
|
+
: currentFieldIdx - 1;
|
|
654
|
+
const next = fields[targetIdx];
|
|
293
655
|
setActiveField(next);
|
|
294
656
|
if (next === 'description') {
|
|
295
657
|
const desc = inSlots ? (currentSlot?.description ?? '') : (currentProp?.description ?? '');
|
|
296
658
|
setTextCursor(desc.length);
|
|
297
659
|
}
|
|
660
|
+
else if (next === 'default' && currentProp) {
|
|
661
|
+
// Land cursor at end of any string-typed default for ergonomic typing.
|
|
662
|
+
const cur = typeof currentProp.default === 'string' ? currentProp.default : '';
|
|
663
|
+
setTextCursor(cur.length);
|
|
664
|
+
}
|
|
665
|
+
return;
|
|
298
666
|
}
|
|
299
|
-
return;
|
|
300
667
|
}
|
|
301
668
|
// ── Picker fields (left/right cycle) ──────────────────────────────────
|
|
302
669
|
if (activeField === 'type' && (key.leftArrow || key.rightArrow) && currentProp) {
|
|
@@ -348,11 +715,76 @@ export function FieldEditor({ value, width, height, onChange, onSave, onDiscard,
|
|
|
348
715
|
}
|
|
349
716
|
return;
|
|
350
717
|
}
|
|
718
|
+
// ── $default editing per type ─────────────────────────────────────────
|
|
719
|
+
if (activeField === 'default' && currentProp) {
|
|
720
|
+
const setProp = (next) => {
|
|
721
|
+
const nextProps = props.map((p, i) => (i === propIdx ? next : p));
|
|
722
|
+
commit({ ...editorState, props: nextProps });
|
|
723
|
+
};
|
|
724
|
+
// boolean: tri-state cycle (unset → true → false → unset).
|
|
725
|
+
if (currentProp.type === 'boolean' && (key.leftArrow || key.rightArrow)) {
|
|
726
|
+
const cycle = [null, true, false];
|
|
727
|
+
const curIdx = cycle.findIndex((v) => v === currentProp.default);
|
|
728
|
+
const idx = curIdx < 0 ? 0 : curIdx;
|
|
729
|
+
const nextIdx = key.rightArrow ? (idx + 1) % cycle.length : (idx - 1 + cycle.length) % cycle.length;
|
|
730
|
+
setProp({ ...currentProp, default: cycle[nextIdx] });
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
// enum: cycle through prop.values plus (unset).
|
|
734
|
+
if (currentProp.type === 'enum' && (key.leftArrow || key.rightArrow)) {
|
|
735
|
+
const opts = [null, ...currentProp.values];
|
|
736
|
+
const cur = typeof currentProp.default === 'string' ? currentProp.default : null;
|
|
737
|
+
const curIdx = opts.findIndex((v) => v === cur);
|
|
738
|
+
const idx = curIdx < 0 ? 0 : curIdx;
|
|
739
|
+
const nextIdx = key.rightArrow ? (idx + 1) % opts.length : (idx - 1 + opts.length) % opts.length;
|
|
740
|
+
setProp({ ...currentProp, default: opts[nextIdx] });
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
// string/token: text input. Mirrors description input subroutine.
|
|
744
|
+
if (currentProp.type === 'string' || currentProp.type === 'token') {
|
|
745
|
+
const cur = typeof currentProp.default === 'string' ? currentProp.default : '';
|
|
746
|
+
const setVal = (next) => setProp({ ...currentProp, default: next === '' ? null : next });
|
|
747
|
+
if (key.leftArrow) {
|
|
748
|
+
setTextCursor((c) => Math.max(0, c - 1));
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (key.rightArrow) {
|
|
752
|
+
setTextCursor((c) => Math.min(cur.length, c + 1));
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
if (key.backspace) {
|
|
756
|
+
if (textCursor > 0) {
|
|
757
|
+
setVal(cur.slice(0, textCursor - 1) + cur.slice(textCursor));
|
|
758
|
+
setTextCursor((c) => c - 1);
|
|
759
|
+
}
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (key.delete) {
|
|
763
|
+
if (textCursor < cur.length)
|
|
764
|
+
setVal(cur.slice(0, textCursor) + cur.slice(textCursor + 1));
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
if (input && input.length === 1 && !key.ctrl && !key.meta && !key.return) {
|
|
768
|
+
setVal(cur.slice(0, textCursor) + input + cur.slice(textCursor));
|
|
769
|
+
setTextCursor((c) => c + 1);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
351
776
|
// ── Description text input ─────────────────────────────────────────────
|
|
352
777
|
if (activeField === 'description') {
|
|
353
|
-
const getDesc = () =>
|
|
778
|
+
const getDesc = () => inComponentDesc
|
|
779
|
+
? editorState.componentDescription
|
|
780
|
+
: inSlots
|
|
781
|
+
? (currentSlot?.description ?? '')
|
|
782
|
+
: (currentProp?.description ?? '');
|
|
354
783
|
const setDesc = (next) => {
|
|
355
|
-
if (
|
|
784
|
+
if (inComponentDesc) {
|
|
785
|
+
commit({ ...editorState, componentDescription: next });
|
|
786
|
+
}
|
|
787
|
+
else if (inSlots && currentSlot) {
|
|
356
788
|
const nextSlots = slots.map((s, i) => (i === slotIdx ? { ...s, description: next } : s));
|
|
357
789
|
commit({ ...editorState, slots: nextSlots });
|
|
358
790
|
}
|
|
@@ -397,84 +829,88 @@ export function FieldEditor({ value, width, height, onChange, onSave, onDiscard,
|
|
|
397
829
|
}
|
|
398
830
|
return;
|
|
399
831
|
}
|
|
400
|
-
// ──
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
// Typing a new value
|
|
413
|
-
if (key.return) {
|
|
414
|
-
if (newValueText.trim()) {
|
|
415
|
-
const nextVals = [...vals, newValueText.trim()];
|
|
416
|
-
const nextProps = props.map((p, i) => (i === propIdx ? { ...p, values: nextVals } : p));
|
|
417
|
-
commit({ ...editorState, props: nextProps });
|
|
418
|
-
setValueCursor(nextVals.length - 1);
|
|
419
|
-
}
|
|
420
|
-
setAddingValue(false);
|
|
421
|
-
setNewValueText('');
|
|
832
|
+
// ── $allowedComponents inline manipulation (slot-level) ──────────────
|
|
833
|
+
// Mirrors the $values block exactly, but operates on the slot's
|
|
834
|
+
// allowedComponents list.
|
|
835
|
+
if (activeField === 'allowedComponents' && currentSlot) {
|
|
836
|
+
const vals = currentSlot.allowedComponents;
|
|
837
|
+
const setSlotVals = (next) => {
|
|
838
|
+
const nextSlots = slots.map((s, i) => (i === slotIdx ? { ...s, allowedComponents: next } : s));
|
|
839
|
+
commit({ ...editorState, slots: nextSlots });
|
|
840
|
+
};
|
|
841
|
+
if (input === 'a') {
|
|
842
|
+
setEditingValue({ mode: 'add' });
|
|
843
|
+
setValueText('');
|
|
422
844
|
return;
|
|
423
845
|
}
|
|
424
|
-
if (
|
|
425
|
-
|
|
426
|
-
|
|
846
|
+
if (input === 'e' && vals.length > 0) {
|
|
847
|
+
setEditingValue({ mode: 'edit', index: valueCursor });
|
|
848
|
+
setValueText(vals[valueCursor] ?? '');
|
|
427
849
|
return;
|
|
428
850
|
}
|
|
429
|
-
if (
|
|
430
|
-
|
|
851
|
+
if (input === 'r' && vals.length > 0) {
|
|
852
|
+
const nextVals = vals.filter((_, i) => i !== valueCursor);
|
|
853
|
+
setSlotVals(nextVals);
|
|
854
|
+
setValueCursor((c) => Math.max(0, Math.min(c, nextVals.length - 1)));
|
|
431
855
|
return;
|
|
432
856
|
}
|
|
433
|
-
if (input
|
|
434
|
-
|
|
857
|
+
if (input === 'K' && valueCursor > 0) {
|
|
858
|
+
const nextVals = [...vals];
|
|
859
|
+
[nextVals[valueCursor - 1], nextVals[valueCursor]] = [nextVals[valueCursor], nextVals[valueCursor - 1]];
|
|
860
|
+
setSlotVals(nextVals);
|
|
861
|
+
setValueCursor((c) => c - 1);
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
if (input === 'J' && valueCursor < vals.length - 1) {
|
|
865
|
+
const nextVals = [...vals];
|
|
866
|
+
[nextVals[valueCursor], nextVals[valueCursor + 1]] = [nextVals[valueCursor + 1], nextVals[valueCursor]];
|
|
867
|
+
setSlotVals(nextVals);
|
|
868
|
+
setValueCursor((c) => c + 1);
|
|
435
869
|
return;
|
|
436
870
|
}
|
|
437
871
|
return;
|
|
438
872
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
873
|
+
// ── $values inline manipulation (flat — no extra Return) ───────────────
|
|
874
|
+
if (activeField === 'values' && currentProp) {
|
|
875
|
+
const vals = currentProp.values;
|
|
876
|
+
// a — add new value (inline text-entry)
|
|
877
|
+
if (input === 'a') {
|
|
878
|
+
setEditingValue({ mode: 'add' });
|
|
879
|
+
setValueText('');
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
// e — edit value at cursor (inline text-entry, pre-filled)
|
|
883
|
+
if (input === 'e' && vals.length > 0) {
|
|
884
|
+
setEditingValue({ mode: 'edit', index: valueCursor });
|
|
885
|
+
setValueText(vals[valueCursor] ?? '');
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
// r — remove value at cursor
|
|
889
|
+
if (input === 'r' && vals.length > 0) {
|
|
890
|
+
const nextVals = vals.filter((_, i) => i !== valueCursor);
|
|
891
|
+
const nextProps = props.map((p, i) => (i === propIdx ? { ...p, values: nextVals } : p));
|
|
892
|
+
commit({ ...editorState, props: nextProps });
|
|
893
|
+
setValueCursor((c) => Math.max(0, Math.min(c, nextVals.length - 1)));
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
// K (Shift+K) — move value up
|
|
897
|
+
if (input === 'K' && valueCursor > 0) {
|
|
898
|
+
const nextVals = [...vals];
|
|
899
|
+
[nextVals[valueCursor - 1], nextVals[valueCursor]] = [nextVals[valueCursor], nextVals[valueCursor - 1]];
|
|
900
|
+
const nextProps = props.map((p, i) => (i === propIdx ? { ...p, values: nextVals } : p));
|
|
901
|
+
commit({ ...editorState, props: nextProps });
|
|
902
|
+
setValueCursor((c) => c - 1);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
// J (Shift+J) — move value down
|
|
906
|
+
if (input === 'J' && valueCursor < vals.length - 1) {
|
|
907
|
+
const nextVals = [...vals];
|
|
908
|
+
[nextVals[valueCursor], nextVals[valueCursor + 1]] = [nextVals[valueCursor + 1], nextVals[valueCursor]];
|
|
909
|
+
const nextProps = props.map((p, i) => (i === propIdx ? { ...p, values: nextVals } : p));
|
|
910
|
+
commit({ ...editorState, props: nextProps });
|
|
911
|
+
setValueCursor((c) => c + 1);
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
478
914
|
return;
|
|
479
915
|
}
|
|
480
916
|
return;
|
|
@@ -486,20 +922,38 @@ export function FieldEditor({ value, width, height, onChange, onSave, onDiscard,
|
|
|
486
922
|
return (_jsxs(Box, { flexDirection: "column", width: width, borderStyle: "single", borderColor: "red", children: [_jsx(Text, { bold: true, color: "red", children: "FIELD EDITOR \u2014 parse error" }), _jsx(Text, { color: "red", children: parseErr }), _jsx(Text, { dimColor: true, children: "Cannot display structured editor. Fix the JSON first." })] }));
|
|
487
923
|
}
|
|
488
924
|
if (props.length === 0 && slots.length === 0) {
|
|
489
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, borderStyle: "single", borderColor: "yellow", children: [_jsx(Text, { bold: true, children: "FIELD EDITOR \u2014 no fields" }), _jsx(Text, { dimColor: true, children: "
|
|
925
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, borderStyle: "single", borderColor: "yellow", children: [_jsx(Text, { bold: true, color: "yellow", children: "FIELD EDITOR \u2014 no fields" }), _jsx(Text, { color: "yellow", children: "⚠ No properties classified for this component. The LLM didn't find anything to classify." }), _jsx(Text, { dimColor: true, children: "You can add fields manually below or reject this component." }), _jsx(Text, { dimColor: true, children: "Ctrl+S to save \u00B7 Esc to discard" })] }));
|
|
490
926
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
927
|
+
// When $properties is empty but $slots exist, surface the same warning
|
|
928
|
+
// prominently in the panel so the user understands why the component looks
|
|
929
|
+
// sparse — and can act on it (manually add a prop or reject).
|
|
930
|
+
const hasEmptyProperties = props.length === 0;
|
|
931
|
+
const modeLabel = (() => {
|
|
932
|
+
if (editingValue) {
|
|
933
|
+
return editingValue.mode === 'add' ? 'Enter to add · Esc to cancel' : 'Enter to save edit · Esc to cancel';
|
|
934
|
+
}
|
|
935
|
+
if (focusLevel === 'field' && activeField === 'description') {
|
|
936
|
+
return 'Type to edit ←→ cursor ↑↓ cycle field Esc row Ctrl+S save';
|
|
937
|
+
}
|
|
938
|
+
if (focusLevel === 'field' &&
|
|
939
|
+
(activeField === 'type' || activeField === 'category' || activeField === 'tokenKind')) {
|
|
940
|
+
return '←→ cycle value ↑↓/jk cycle field Esc row';
|
|
941
|
+
}
|
|
942
|
+
if (focusLevel === 'field' && activeField === 'required') {
|
|
943
|
+
return 'Space/Enter toggle ↑↓/jk cycle field Esc row';
|
|
944
|
+
}
|
|
945
|
+
if (focusLevel === 'field' && (activeField === 'values' || activeField === 'allowedComponents')) {
|
|
946
|
+
return '[a]dd [e]dit [r]emove ↑↓/jk navigate [K/J] reorder Esc row';
|
|
947
|
+
}
|
|
948
|
+
if (rationaleOpen) {
|
|
949
|
+
return 'jk/Ctrl+u/d scroll i/Esc close rationale panel';
|
|
950
|
+
}
|
|
951
|
+
return '↑↓/jk navigate rows Enter edit fields s source i prop rationale I component rationale ? help Ctrl+S save Esc exit panel';
|
|
952
|
+
})();
|
|
502
953
|
const rows = [];
|
|
954
|
+
// Component-level $description always renders first so it's reachable as
|
|
955
|
+
// the topmost navigable row, even when empty (the operator can populate it).
|
|
956
|
+
rows.push({ kind: 'component-description' });
|
|
503
957
|
if (props.length > 0) {
|
|
504
958
|
rows.push({ kind: 'header', label: `── $properties (${props.length}) ` });
|
|
505
959
|
props.forEach((_, i) => rows.push({ kind: 'prop', idx: i }));
|
|
@@ -509,23 +963,45 @@ export function FieldEditor({ value, width, height, onChange, onSave, onDiscard,
|
|
|
509
963
|
slots.forEach((_, i) => rows.push({ kind: 'slot', idx: i }));
|
|
510
964
|
}
|
|
511
965
|
// Scroll to keep selected row visible
|
|
512
|
-
const selectedRowIdx = rows.findIndex((r) => (r.kind === 'prop' && !inSlots && r.idx === propIdx
|
|
513
|
-
(r.kind === 'slot' && inSlots && r.idx === slotIdx)
|
|
966
|
+
const selectedRowIdx = rows.findIndex((r) => (r.kind === 'prop' && !inSlots && !inComponentDesc && r.idx === propIdx) ||
|
|
967
|
+
(r.kind === 'slot' && inSlots && r.idx === slotIdx) ||
|
|
968
|
+
(r.kind === 'component-description' && inComponentDesc));
|
|
514
969
|
const visibleRows = Math.max(1, height - 3); // title + hint bar + border
|
|
515
970
|
const scrollStart = selectedRowIdx < 0 ? 0 : Math.max(0, Math.min(selectedRowIdx, rows.length - visibleRows));
|
|
516
971
|
const visibleRowSlice = rows.slice(scrollStart, scrollStart + visibleRows);
|
|
517
|
-
return (_jsxs(Box, { flexDirection: "column", width: width, borderStyle: "single", borderColor:
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
const
|
|
529
|
-
|
|
530
|
-
|
|
972
|
+
return (_jsxs(Box, { flexDirection: "column", width: width, borderStyle: "single", borderColor: hasEmptyProperties ? 'yellow' : 'cyan', children: [_jsx(Text, { bold: true, color: hasEmptyProperties ? 'yellow' : 'cyan', children: 'FIELDS [Ctrl+S save · Esc discard]' }), hasEmptyProperties && (_jsx(Text, { color: "yellow", children: "⚠ No properties classified for this component. The LLM didn't find anything to classify. Reject this component or add fields manually." })), _jsx(Box, { flexDirection: "column", width: innerWidth, children: visibleRowSlice.map((row, i) => {
|
|
973
|
+
if (row.kind === 'header') {
|
|
974
|
+
return (_jsx(Text, { dimColor: true, children: row.label }, `header-${i}`));
|
|
975
|
+
}
|
|
976
|
+
if (row.kind === 'component-description') {
|
|
977
|
+
const isSelected = inComponentDesc;
|
|
978
|
+
const isEditing = isSelected && focusLevel === 'field' && activeField === 'description';
|
|
979
|
+
const desc = editorState.componentDescription;
|
|
980
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { gap: 1, children: _jsx(Text, { color: isSelected ? 'white' : 'cyan', bold: isSelected, backgroundColor: isSelected ? 'blue' : undefined, children: ' component-$description ' }) }), isEditing ? (_jsx(Box, { paddingLeft: 2, flexDirection: "row", children: _jsxs(Box, { flexGrow: 1, borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { children: desc.slice(0, textCursor) }), _jsx(Text, { inverse: cursorVisible, children: desc[textCursor] ?? (cursorVisible ? '█' : ' ') }), _jsx(Text, { children: desc.slice(textCursor + 1) })] }) })) : (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { color: desc ? 'green' : undefined, dimColor: !desc, children: desc || '(none — Return to edit)' }) }))] }, `component-description-${i}`));
|
|
981
|
+
}
|
|
982
|
+
if (row.kind === 'prop') {
|
|
983
|
+
const p = props[row.idx];
|
|
984
|
+
const isSelected = !inSlots && !inComponentDesc && row.idx === propIdx;
|
|
985
|
+
const propMeta = metadata?.props?.[p.name];
|
|
986
|
+
return (_jsx(PropRow, { prop: p, selected: isSelected, activeField: isSelected && focusLevel === 'field' ? activeField : null, textCursor: textCursor, valueCursor: valueCursor, cursorVisible: cursorVisible, editingValue: isSelected ? editingValue : null, valueText: isSelected ? valueText : '', width: innerWidth, rationale: propMeta?.rationale ?? null, rowKey: String(row.idx) }, `prop-${row.idx}`));
|
|
987
|
+
}
|
|
988
|
+
// slot row
|
|
989
|
+
const s = slots[row.idx];
|
|
990
|
+
const isSelected = inSlots && row.idx === slotIdx;
|
|
991
|
+
return (_jsx(SlotRow, { slot: s, selected: isSelected, activeField: isSelected && focusLevel === 'field' ? activeField : null, textCursor: textCursor, valueCursor: valueCursor, cursorVisible: cursorVisible, editingValue: isSelected ? editingValue : null, valueText: isSelected ? valueText : '', width: innerWidth }, `slot-${row.idx}`));
|
|
992
|
+
}) }), sourceOpen &&
|
|
993
|
+
!onToggleSourceExternal &&
|
|
994
|
+
(() => {
|
|
995
|
+
const propMeta = !inSlots && !inComponentDesc && currentProp ? metadata?.props?.[currentProp.name] : undefined;
|
|
996
|
+
const start = propMeta?.sourceStartLine ?? null;
|
|
997
|
+
const end = propMeta?.sourceEndLine ?? null;
|
|
998
|
+
const path = metadata?.sourcePath ?? null;
|
|
999
|
+
const src = metadata?.componentSource ?? null;
|
|
1000
|
+
const headerPath = path ?? '<unknown source path>';
|
|
1001
|
+
if (!start || !end || !src) {
|
|
1002
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: `source: ${headerPath}` }), _jsx(Text, { dimColor: true, children: "(no source location captured for this prop)" }), _jsx(Text, { dimColor: true, children: "[s] close" })] }));
|
|
1003
|
+
}
|
|
1004
|
+
const lines = src.split('\n').slice(Math.max(0, start - 1), end);
|
|
1005
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "gray", paddingX: 1, children: [_jsx(Text, { dimColor: true, bold: true, children: `${headerPath}: lines ${start}–${end}` }), lines.map((ln, i) => (_jsx(Text, { dimColor: true, children: ln }, `source-line-${i}`))), _jsx(Text, { dimColor: true, children: "[s] close \u00B7 [Esc] close" })] }));
|
|
1006
|
+
})(), showHelp && (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "Keybindings" }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Row navigation" }), _jsx(Text, { children: ' ↑/↓ or j/k move between rows' }), _jsx(Text, { children: ' Enter edit fields on the current row' }), _jsx(Text, { children: ' Esc exit the panel' }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Field editing" }), _jsx(Text, { children: ' ↑/↓ or j/k cycle through fields (j/k literal in text inputs)' }), _jsx(Text, { children: ' ←/→ cycle picker values · move cursor in text inputs' }), _jsx(Text, { children: ' Space/Enter toggle required' }), _jsx(Text, { children: ' Type edit description / string default' }), _jsx(Text, { children: ' Ctrl+S save changes' }), _jsx(Text, { children: ' Esc exit field-edit back to the row' }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Values / allowedComponents" }), _jsx(Text, { children: ' a / e / r add · edit · remove' }), _jsx(Text, { children: ' ↑↓ or j/k navigate the list' }), _jsx(Text, { children: ' K / J reorder up · down' }), _jsx(Text, { children: " " }), _jsx(Text, { bold: true, children: "Panels" }), _jsx(Text, { children: ' s toggle source-view for the current prop' }), _jsx(Text, { children: ' i toggle prop rationale panel' }), _jsx(Text, { children: ' I toggle component rationale panel' }), _jsx(Text, { children: ' d toggle removed-components panel (wizard final-review)' }), _jsx(Text, { children: ' ? toggle this overlay' }), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "press ? or Esc to close" })] })), validationError && _jsx(Text, { color: "red", children: '✗ ' + validationError }), _jsx(Text, { dimColor: true, children: modeLabel })] }));
|
|
531
1007
|
}
|