@contentful/experience-design-system-cli 2.12.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.
Files changed (142) hide show
  1. package/README.md +214 -357
  2. package/dist/package.json +1 -1
  3. package/dist/src/analyze/extract/astro.js +9 -0
  4. package/dist/src/analyze/extract/react.js +23 -0
  5. package/dist/src/analyze/extract/stencil.js +3 -0
  6. package/dist/src/analyze/extract/vue.js +6 -0
  7. package/dist/src/analyze/extract/web-components.js +10 -0
  8. package/dist/src/analyze/pre-classify.d.ts +1 -1
  9. package/dist/src/analyze/pre-classify.js +3 -3
  10. package/dist/src/analyze/select/persistence.d.ts +13 -0
  11. package/dist/src/analyze/select/persistence.js +38 -0
  12. package/dist/src/analyze/select/preview-annotations.d.ts +26 -0
  13. package/dist/src/analyze/select/preview-annotations.js +62 -0
  14. package/dist/src/analyze/select/tui/App.js +5 -2
  15. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.d.ts +30 -0
  16. package/dist/src/analyze/select/tui/components/ComponentRationalePanel.js +137 -0
  17. package/dist/src/analyze/select/tui/components/FieldEditor.d.ts +58 -1
  18. package/dist/src/analyze/select/tui/components/FieldEditor.js +637 -161
  19. package/dist/src/analyze/select/tui/components/FinalizeDialog.js +1 -1
  20. package/dist/src/analyze/select/tui/components/JsonPanel.js +5 -1
  21. package/dist/src/analyze/select/tui/components/RationalePanel.d.ts +34 -0
  22. package/dist/src/analyze/select/tui/components/RationalePanel.js +92 -0
  23. package/dist/src/analyze/select/tui/components/Sidebar.d.ts +13 -1
  24. package/dist/src/analyze/select/tui/components/Sidebar.js +27 -3
  25. package/dist/src/analyze/select/tui/hooks/scroll-offset.d.ts +29 -0
  26. package/dist/src/analyze/select/tui/hooks/scroll-offset.js +26 -0
  27. package/dist/src/analyze/select-agent/command.d.ts +2 -0
  28. package/dist/src/analyze/select-agent/command.js +259 -45
  29. package/dist/src/analyze/select-agent/show-rationale.d.ts +35 -0
  30. package/dist/src/analyze/select-agent/show-rationale.js +92 -0
  31. package/dist/src/apply/command.js +7 -3
  32. package/dist/src/apply/tui/ServerApplyView.d.ts +3 -1
  33. package/dist/src/apply/tui/ServerApplyView.js +3 -2
  34. package/dist/src/credentials-store.d.ts +5 -0
  35. package/dist/src/credentials-store.js +7 -1
  36. package/dist/src/generate/agent-runner.d.ts +14 -0
  37. package/dist/src/generate/agent-runner.js +16 -0
  38. package/dist/src/generate/command.d.ts +6 -0
  39. package/dist/src/generate/command.js +45 -10
  40. package/dist/src/generate/progress.d.ts +9 -0
  41. package/dist/src/generate/progress.js +11 -0
  42. package/dist/src/generate/prompt-builder.d.ts +8 -0
  43. package/dist/src/generate/prompt-builder.js +22 -11
  44. package/dist/src/import/agent-model-resolve.d.ts +23 -0
  45. package/dist/src/import/agent-model-resolve.js +35 -0
  46. package/dist/src/import/auto-filter-resolve.d.ts +12 -0
  47. package/dist/src/import/auto-filter-resolve.js +16 -0
  48. package/dist/src/import/command.js +248 -8
  49. package/dist/src/import/orchestrator.d.ts +2 -0
  50. package/dist/src/import/orchestrator.js +11 -3
  51. package/dist/src/import/print-prompt.d.ts +35 -0
  52. package/dist/src/import/print-prompt.js +31 -0
  53. package/dist/src/import/tui/CustomPromptBanner.d.ts +11 -0
  54. package/dist/src/import/tui/CustomPromptBanner.js +7 -0
  55. package/dist/src/import/tui/WizardApp.d.ts +112 -8
  56. package/dist/src/import/tui/WizardApp.js +814 -333
  57. package/dist/src/import/tui/auto-filter-error.d.ts +2 -0
  58. package/dist/src/import/tui/auto-filter-error.js +29 -0
  59. package/dist/src/import/tui/final-review-host.d.ts +15 -0
  60. package/dist/src/import/tui/final-review-host.js +20 -0
  61. package/dist/src/import/tui/push-decision-gate-helpers.d.ts +8 -0
  62. package/dist/src/import/tui/push-decision-gate-helpers.js +8 -0
  63. package/dist/src/import/tui/push-progress.d.ts +23 -0
  64. package/dist/src/import/tui/push-progress.js +14 -0
  65. package/dist/src/import/tui/run-print-files-helpers.d.ts +26 -0
  66. package/dist/src/import/tui/run-print-files-helpers.js +8 -0
  67. package/dist/src/import/tui/run-teaser.d.ts +5 -0
  68. package/dist/src/import/tui/run-teaser.js +9 -0
  69. package/dist/src/import/tui/runLivePreview.d.ts +35 -0
  70. package/dist/src/import/tui/runLivePreview.js +73 -0
  71. package/dist/src/import/tui/runScopeGate.d.ts +22 -0
  72. package/dist/src/import/tui/runScopeGate.js +28 -0
  73. package/dist/src/import/tui/scope-gate-host.d.ts +21 -0
  74. package/dist/src/import/tui/scope-gate-host.js +20 -0
  75. package/dist/src/import/tui/spawn-generate.d.ts +33 -0
  76. package/dist/src/import/tui/spawn-generate.js +53 -0
  77. package/dist/src/import/tui/steps/CredentialsStep.d.ts +25 -1
  78. package/dist/src/import/tui/steps/CredentialsStep.js +31 -3
  79. package/dist/src/import/tui/steps/DoneStep.d.ts +6 -1
  80. package/dist/src/import/tui/steps/DoneStep.js +10 -4
  81. package/dist/src/import/tui/steps/GenerateReviewStep.d.ts +29 -2
  82. package/dist/src/import/tui/steps/GenerateReviewStep.js +412 -48
  83. package/dist/src/import/tui/steps/PushDecisionGateStep.d.ts +19 -0
  84. package/dist/src/import/tui/steps/PushDecisionGateStep.js +76 -0
  85. package/dist/src/import/tui/steps/PushingStep.d.ts +10 -0
  86. package/dist/src/import/tui/steps/PushingStep.js +40 -0
  87. package/dist/src/import/tui/steps/ScopeGateStep.d.ts +23 -0
  88. package/dist/src/import/tui/steps/ScopeGateStep.js +243 -0
  89. package/dist/src/import/tui/steps/TokenInputStep.js +20 -3
  90. package/dist/src/import/tui/useLivePreview.d.ts +35 -0
  91. package/dist/src/import/tui/useLivePreview.js +120 -0
  92. package/dist/src/import/tui/wizard-generate-progress.d.ts +21 -0
  93. package/dist/src/import/tui/wizard-generate-progress.js +25 -0
  94. package/dist/src/import/tui/wizard-save-flow.d.ts +37 -0
  95. package/dist/src/import/tui/wizard-save-flow.js +20 -0
  96. package/dist/src/import/tui/wizard-state-transitions.d.ts +80 -0
  97. package/dist/src/import/tui/wizard-state-transitions.js +79 -0
  98. package/dist/src/lib/contentful-urls.d.ts +34 -0
  99. package/dist/src/lib/contentful-urls.js +41 -0
  100. package/dist/src/program.js +2 -0
  101. package/dist/src/runs/export-helpers.d.ts +30 -0
  102. package/dist/src/runs/export-helpers.js +59 -0
  103. package/dist/src/runs/fingerprint.d.ts +47 -0
  104. package/dist/src/runs/fingerprint.js +75 -0
  105. package/dist/src/runs/ls-command.d.ts +12 -0
  106. package/dist/src/runs/ls-command.js +137 -0
  107. package/dist/src/runs/modify-launcher.d.ts +24 -0
  108. package/dist/src/runs/modify-launcher.js +36 -0
  109. package/dist/src/runs/path-prompt.d.ts +8 -0
  110. package/dist/src/runs/path-prompt.js +72 -0
  111. package/dist/src/runs/push-creds-prompt.d.ts +25 -0
  112. package/dist/src/runs/push-creds-prompt.js +39 -0
  113. package/dist/src/runs/push-helpers.d.ts +29 -0
  114. package/dist/src/runs/push-helpers.js +69 -0
  115. package/dist/src/runs/replay-helpers.d.ts +67 -0
  116. package/dist/src/runs/replay-helpers.js +148 -0
  117. package/dist/src/runs/resolve-run-target.d.ts +12 -0
  118. package/dist/src/runs/resolve-run-target.js +45 -0
  119. package/dist/src/runs/run-picker-mount.d.ts +46 -0
  120. package/dist/src/runs/run-picker-mount.js +72 -0
  121. package/dist/src/runs/run-picker.d.ts +17 -0
  122. package/dist/src/runs/run-picker.js +145 -0
  123. package/dist/src/runs/save-conflict.d.ts +8 -0
  124. package/dist/src/runs/save-conflict.js +56 -0
  125. package/dist/src/runs/save-path-resolver.d.ts +55 -0
  126. package/dist/src/runs/save-path-resolver.js +71 -0
  127. package/dist/src/runs/staleness.d.ts +38 -0
  128. package/dist/src/runs/staleness.js +140 -0
  129. package/dist/src/runs/store.d.ts +78 -0
  130. package/dist/src/runs/store.js +136 -0
  131. package/dist/src/session/cache-keys.d.ts +15 -0
  132. package/dist/src/session/cache-keys.js +38 -0
  133. package/dist/src/session/db.d.ts +89 -3
  134. package/dist/src/session/db.js +447 -104
  135. package/dist/src/setup/auto-filter-prompt.d.ts +13 -0
  136. package/dist/src/setup/auto-filter-prompt.js +24 -0
  137. package/dist/src/setup/command.d.ts +7 -0
  138. package/dist/src/setup/command.js +58 -2
  139. package/dist/src/types.d.ts +6 -0
  140. package/package.json +2 -2
  141. package/skills/generate-components.md +54 -6
  142. 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: CDF_PROPERTY_TYPES.includes(p.$type) ? p.$type : 'string',
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 PropRow({ prop, selected, activeField, textCursor, valueCursor, cursorVisible, width, }) {
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, gap: 1, children: [_jsx(Text, { dimColor: true, children: "desc:" }), activeField === 'description' ? (_jsxs(Box, { 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) })] })) : (_jsx(Text, { color: "green", children: prop.description || '—' }))] })), selected && prop.type === 'enum' && (_jsxs(Box, { paddingLeft: 2, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "values:" }), prop.values.length === 0 && _jsx(Text, { dimColor: true, children: " (none)" }), prop.values.map((v, i) => (_jsx(Box, { gap: 1, paddingLeft: 2, children: _jsx(Text, { color: activeField === 'values' && valueCursor === i ? 'cyan' : 'white', children: activeField === 'values' && valueCursor === i ? `▶ ${v}` : ` ${v}` }) }, i))), activeField === 'values' && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: "[+] add [x] remove [[] up []] down [Esc] done" }) }))] }))] }));
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, gap: 1, children: [_jsx(Text, { dimColor: true, children: "desc:" }), activeField === 'description' ? (_jsxs(Box, { 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) })] })) : (_jsx(Text, { color: "green", children: slot.description || '' }))] }))] }));
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
- const PROP_FIELDS_BASE = ['type', 'category', 'required', 'description'];
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 = [...PROP_FIELDS_BASE];
226
+ const fields = ['type', 'category', 'required'];
142
227
  if (prop.type === 'token')
143
- fields.splice(3, 0, 'tokenKind');
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
- const SLOT_FIELDS = ['required', 'description'];
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
- const [focusLevel, setFocusLevel] = useState('prop');
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 (true) or slots (false) at the top level
159
- const [inSlots, setInSlots] = useState(false);
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(null);
285
+ const [activeField, setActiveField] = useState(initialFocus.activeField);
162
286
  // Text cursor position for description fields
163
- const [textCursor, setTextCursor] = useState(0);
287
+ const [textCursor, setTextCursor] = useState(initialFocus.textCursor);
164
288
  // Value list cursor for $values editing
165
289
  const [valueCursor, setValueCursor] = useState(0);
166
- // New value being typed in $values add mode
167
- const [addingValue, setAddingValue] = useState(false);
168
- const [newValueText, setNewValueText] = useState('');
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
- setFocusLevel(inSlots ? 'slot' : 'prop');
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
- if (focusLevel === 'value') {
197
- setFocusLevel('field');
198
- setAddingValue(false);
199
- setNewValueText('');
200
- return;
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((i) => i - 1);
210
- setActiveField(null);
512
+ setPropIdx(propIdx - 1);
211
513
  }
212
- else if (slots.length > 0) {
213
- setInSlots(true);
214
- setFocusLevel('slot');
215
- setActiveField(null);
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((i) => i + 1);
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((i) => i - 1);
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
- setActiveField(null);
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((i) => i + 1);
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
- if (key.upArrow || input === 'k') {
275
- if (currentFieldIdx > 0) {
276
- const next = fields[currentFieldIdx - 1];
277
- setActiveField(next);
278
- if (next === 'description') {
279
- const desc = inSlots ? (currentSlot?.description ?? '') : (currentProp?.description ?? '');
280
- setTextCursor(desc.length);
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
- if (key.downArrow || input === 'j') {
291
- if (currentFieldIdx < fields.length - 1) {
292
- const next = fields[currentFieldIdx + 1];
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 = () => (inSlots ? (currentSlot?.description ?? '') : (currentProp?.description ?? ''));
778
+ const getDesc = () => inComponentDesc
779
+ ? editorState.componentDescription
780
+ : inSlots
781
+ ? (currentSlot?.description ?? '')
782
+ : (currentProp?.description ?? '');
354
783
  const setDesc = (next) => {
355
- if (inSlots && currentSlot) {
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
- // ── Enter $values sub-list ─────────────────────────────────────────────
401
- if (activeField === 'values' && key.return && currentProp) {
402
- setFocusLevel('value');
403
- setValueCursor(0);
404
- return;
405
- }
406
- return;
407
- }
408
- // ── Value-list editing ────────────────────────────────────────────────────
409
- if (focusLevel === 'value' && currentProp) {
410
- const vals = currentProp.values;
411
- if (addingValue) {
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 (key.escape) {
425
- setAddingValue(false);
426
- setNewValueText('');
846
+ if (input === 'e' && vals.length > 0) {
847
+ setEditingValue({ mode: 'edit', index: valueCursor });
848
+ setValueText(vals[valueCursor] ?? '');
427
849
  return;
428
850
  }
429
- if (key.backspace) {
430
- setNewValueText((t) => t.slice(0, -1));
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 && input.length === 1 && !key.ctrl && !key.meta) {
434
- setNewValueText((t) => t + input);
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
- if (key.upArrow || input === 'k') {
440
- setValueCursor((c) => Math.max(0, c - 1));
441
- return;
442
- }
443
- if (key.downArrow || input === 'j') {
444
- setValueCursor((c) => Math.max(0, Math.min(vals.length - 1, c + 1)));
445
- return;
446
- }
447
- if (key.escape) {
448
- setFocusLevel('field');
449
- return;
450
- }
451
- if (input === '+') {
452
- setAddingValue(true);
453
- setNewValueText('');
454
- return;
455
- }
456
- if (input === 'x' && vals.length > 0) {
457
- const nextVals = vals.filter((_, i) => i !== valueCursor);
458
- const nextProps = props.map((p, i) => (i === propIdx ? { ...p, values: nextVals } : p));
459
- commit({ ...editorState, props: nextProps });
460
- setValueCursor((c) => Math.max(0, Math.min(c, nextVals.length - 1)));
461
- return;
462
- }
463
- // '[' moves item up, ']' moves item down
464
- if (input === '[' && valueCursor > 0) {
465
- const nextVals = [...vals];
466
- [nextVals[valueCursor - 1], nextVals[valueCursor]] = [nextVals[valueCursor], nextVals[valueCursor - 1]];
467
- const nextProps = props.map((p, i) => (i === propIdx ? { ...p, values: nextVals } : p));
468
- commit({ ...editorState, props: nextProps });
469
- setValueCursor((c) => c - 1);
470
- return;
471
- }
472
- if (input === ']' && valueCursor < vals.length - 1) {
473
- const nextVals = [...vals];
474
- [nextVals[valueCursor], nextVals[valueCursor + 1]] = [nextVals[valueCursor + 1], nextVals[valueCursor]];
475
- const nextProps = props.map((p, i) => (i === propIdx ? { ...p, values: nextVals } : p));
476
- commit({ ...editorState, props: nextProps });
477
- setValueCursor((c) => c + 1);
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: "This component has no properties or slots to edit." }), _jsx(Text, { dimColor: true, children: "Ctrl+S to save \u00B7 Esc to discard" })] }));
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
- const modeLabel = focusLevel === 'field' && activeField === 'description'
492
- ? '← move cursor Esc exit field'
493
- : focusLevel === 'field' && (activeField === 'type' || activeField === 'category' || activeField === 'tokenKind')
494
- ? '← cycle values ↑↓ next field Esc exit'
495
- : focusLevel === 'field' && activeField === 'required'
496
- ? 'Space/Enter toggle ↑↓ next field Esc exit'
497
- : focusLevel === 'field' && activeField === 'values'
498
- ? 'Enter edit list Esc exit field'
499
- : focusLevel === 'value'
500
- ? '+ add x remove [ up ] down Esc done'
501
- : '↑↓ navigate Enter edit fields Ctrl+S save Esc discard';
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 && focusLevel !== 'section') ||
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: "cyan", children: [_jsx(Text, { bold: true, color: "cyan", children: 'FIELDS [Ctrl+S save · Esc discard]' }), _jsxs(Box, { flexDirection: "column", width: innerWidth, children: [visibleRowSlice.map((row, i) => {
518
- if (row.kind === 'header') {
519
- return (_jsx(Text, { dimColor: true, children: row.label }, i));
520
- }
521
- if (row.kind === 'prop') {
522
- const p = props[row.idx];
523
- const isSelected = !inSlots && row.idx === propIdx;
524
- return (_jsx(PropRow, { prop: p, selected: isSelected, activeField: isSelected && focusLevel === 'field' ? activeField : null, textCursor: textCursor, valueCursor: valueCursor, cursorVisible: cursorVisible, width: innerWidth }, row.idx));
525
- }
526
- // slot row
527
- const s = slots[row.idx];
528
- const isSelected = inSlots && row.idx === slotIdx;
529
- return (_jsx(SlotRow, { slot: s, selected: isSelected, activeField: isSelected && focusLevel === 'field' ? activeField : null, textCursor: textCursor, cursorVisible: cursorVisible, width: innerWidth }, `slot-${row.idx}`));
530
- }), focusLevel === 'value' && addingValue && (_jsxs(Box, { paddingLeft: 4, gap: 1, children: [_jsx(Text, { color: "cyan", children: "+ " }), _jsx(Text, { children: newValueText }), _jsx(Text, { inverse: cursorVisible, children: " " }), _jsx(Text, { dimColor: true, children: "(Enter confirm \u00B7 Esc cancel)" })] }))] }), validationError && _jsx(Text, { color: "red", children: '✗ ' + validationError }), _jsx(Text, { dimColor: true, children: modeLabel })] }));
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
  }