nudge-ui 0.2.0 → 0.2.1

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 (201) hide show
  1. package/bin/nudge-ui.mjs +3 -1
  2. package/dist/cli/agentCommand.d.ts +8 -0
  3. package/dist/cli/agentCommand.js +59 -0
  4. package/dist/client.mjs +872 -12
  5. package/dist/hosts/astro/astroRuntimeConfig.js +1 -1
  6. package/dist/hosts/astro/clientTransport.js +60 -11
  7. package/dist/hosts/next/sidecar.d.ts +4 -0
  8. package/dist/hosts/next/sidecar.js +49 -6
  9. package/dist/hosts/next/wrapper.js +30 -2
  10. package/dist/hosts/projectBridge.d.ts +20 -0
  11. package/dist/hosts/projectBridge.js +97 -0
  12. package/dist/hosts/static/cli.d.ts +1 -1
  13. package/dist/hosts/static/cli.js +6 -1
  14. package/dist/hosts/static/manifest.d.ts +1 -1
  15. package/dist/hosts/static/manifest.js +1 -1
  16. package/dist/hosts/static/server.js +44 -3
  17. package/dist/hosts/vite/vite.d.ts +5 -1
  18. package/dist/hosts/vite/vite.js +78 -14
  19. package/dist/inspector/agent/McpConnectionDialog.d.ts +2 -2
  20. package/dist/inspector/agent/McpConnectionDialog.js +11 -35
  21. package/dist/inspector/agent/client.d.ts +10 -3
  22. package/dist/inspector/agent/client.js +80 -9
  23. package/dist/inspector/agent/connectionStatus.js +3 -3
  24. package/dist/inspector/agent/httpTransport.d.ts +2 -0
  25. package/dist/inspector/agent/httpTransport.js +37 -17
  26. package/dist/inspector/agent/protocol.d.ts +8 -3
  27. package/dist/inspector/agent/protocol.js +1 -1
  28. package/dist/inspector/agent/verification.d.ts +1 -1
  29. package/dist/inspector/agent/verification.js +69 -12
  30. package/dist/inspector/canvas/CanvasCard.d.ts +5 -3
  31. package/dist/inspector/canvas/CanvasCard.js +181 -91
  32. package/dist/inspector/canvas/CanvasElementOverlay.js +246 -52
  33. package/dist/inspector/canvas/CanvasToolbar.d.ts +12 -0
  34. package/dist/inspector/canvas/CanvasToolbar.js +24 -0
  35. package/dist/inspector/canvas/CanvasWorkspace.d.ts +4 -1
  36. package/dist/inspector/canvas/CanvasWorkspace.js +392 -73
  37. package/dist/inspector/canvas/activeCanvasDocument.d.ts +5 -0
  38. package/dist/inspector/canvas/activeCanvasDocument.js +23 -0
  39. package/dist/inspector/canvas/canvasResize.d.ts +30 -0
  40. package/dist/inspector/canvas/canvasResize.js +52 -0
  41. package/dist/inspector/canvas/canvasStore.d.ts +30 -6
  42. package/dist/inspector/canvas/canvasStore.js +128 -61
  43. package/dist/inspector/canvas/frameProtocol.d.ts +69 -5
  44. package/dist/inspector/canvas/frameProtocol.js +56 -2
  45. package/dist/inspector/canvas/keyboardShortcuts.d.ts +3 -0
  46. package/dist/inspector/canvas/keyboardShortcuts.js +14 -0
  47. package/dist/inspector/canvas/projection.d.ts +7 -0
  48. package/dist/inspector/canvas/projection.js +118 -25
  49. package/dist/inspector/canvas/rendererBootstrap.js +29 -29
  50. package/dist/inspector/canvas/rendererElementSelector.js +341 -30
  51. package/dist/inspector/canvas/rendererMessageRouter.d.ts +11 -0
  52. package/dist/inspector/canvas/rendererMessageRouter.js +43 -0
  53. package/dist/inspector/canvas/rendererSelectionProxy.js +6 -2
  54. package/dist/inspector/canvas/sessionStore.d.ts +1 -0
  55. package/dist/inspector/canvas/sessionStore.js +12 -4
  56. package/dist/inspector/canvas/staleChangeDetector.js +16 -14
  57. package/dist/inspector/changes/changesLog.js +31 -1
  58. package/dist/inspector/changes/managedStyleProjection.d.ts +1 -1
  59. package/dist/inspector/changes/managedStyleProjection.js +3 -3
  60. package/dist/inspector/changes/structuralTypes.d.ts +4 -0
  61. package/dist/inspector/clientManifest.d.ts +5 -0
  62. package/dist/inspector/clientManifest.js +11 -0
  63. package/dist/inspector/componentSemantics/adapterRegistry.js +4 -3
  64. package/dist/inspector/componentSemantics/runtimeBridge.d.ts +2 -2
  65. package/dist/inspector/componentSemantics/runtimeBridge.js +16 -3
  66. package/dist/inspector/index.d.ts +2 -0
  67. package/dist/inspector/index.js +74 -24
  68. package/dist/inspector/inline-text/inlineTextDiagnostics.d.ts +10 -0
  69. package/dist/inspector/inline-text/inlineTextDiagnostics.js +65 -0
  70. package/dist/inspector/inline-text/inlineTextEditor.d.ts +3 -5
  71. package/dist/inspector/inline-text/inlineTextEditor.js +74 -206
  72. package/dist/inspector/inline-text/inlineTextFeedback.d.ts +5 -0
  73. package/dist/inspector/inline-text/inlineTextFeedback.js +27 -0
  74. package/dist/inspector/inline-text/inlineTextLifecycle.js +2 -0
  75. package/dist/inspector/inline-text/textEditChange.d.ts +14 -0
  76. package/dist/inspector/inline-text/textEditChange.js +165 -0
  77. package/dist/inspector/inspection/selectionProperty.d.ts +1 -0
  78. package/dist/inspector/inspection/selectionProperty.js +1 -0
  79. package/dist/inspector/overlay/selectedGeometry.js +21 -2
  80. package/dist/inspector/overlay/spacingGestures.d.ts +45 -0
  81. package/dist/inspector/overlay/spacingGestures.js +353 -0
  82. package/dist/inspector/overlay/structuralGestures.d.ts +2 -2
  83. package/dist/inspector/overlay/structuralGestures.js +12 -10
  84. package/dist/inspector/projection/managedStylesheet.js +4 -1
  85. package/dist/inspector/projection/renderedInstance.d.ts +2 -0
  86. package/dist/inspector/projection/renderedInstance.js +5 -1
  87. package/dist/inspector/projection/structuralProjection.d.ts +2 -0
  88. package/dist/inspector/projection/structuralProjection.js +85 -1
  89. package/dist/inspector/projection/structuralProjectionBoundary.js +17 -1
  90. package/dist/inspector/projection/textProjection.d.ts +2 -0
  91. package/dist/inspector/projection/textProjection.js +10 -4
  92. package/dist/inspector/prompt/clipboardHandoff.d.ts +1 -1
  93. package/dist/inspector/prompt/clipboardHandoff.js +3 -3
  94. package/dist/inspector/prompt/generatePrompt.d.ts +2 -1
  95. package/dist/inspector/prompt/generatePrompt.js +19 -2
  96. package/dist/inspector/runtime/editorShell.d.ts +2 -0
  97. package/dist/inspector/runtime/editorShell.js +4 -0
  98. package/dist/inspector/runtime/runtimeConfig.d.ts +4 -0
  99. package/dist/inspector/runtime/runtimeConfig.js +4 -0
  100. package/dist/inspector/selection/selectionStore.js +2 -0
  101. package/dist/inspector/settings/SettingsDialog.js +10 -0
  102. package/dist/inspector/shell/AppShell.js +5 -1
  103. package/dist/inspector/shell/ChangesLog.js +8 -4
  104. package/dist/inspector/shell/CopyPromptButton.js +95 -19
  105. package/dist/inspector/shell/EmptyState.js +1 -1
  106. package/dist/inspector/shell/InspectorShell.d.ts +0 -1
  107. package/dist/inspector/shell/InspectorShell.js +37 -38
  108. package/dist/inspector/shell/PeekOriginalButton.d.ts +7 -0
  109. package/dist/inspector/shell/PeekOriginalButton.js +57 -0
  110. package/dist/inspector/shell/PeekOriginalIcon.d.ts +7 -0
  111. package/dist/inspector/shell/PeekOriginalIcon.js +9 -0
  112. package/dist/inspector/shell/originalPreview.d.ts +9 -0
  113. package/dist/inspector/shell/originalPreview.js +61 -0
  114. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  115. package/dist/inspector/shell/shortcuts.js +15 -0
  116. package/dist/inspector/sketch/SketchChanges.d.ts +2 -0
  117. package/dist/inspector/sketch/SketchChanges.js +72 -0
  118. package/dist/inspector/sketch/SketchEditor.d.ts +21 -0
  119. package/dist/inspector/sketch/SketchEditor.js +402 -0
  120. package/dist/inspector/sketch/SketchEntryButton.d.ts +4 -0
  121. package/dist/inspector/sketch/SketchEntryButton.js +11 -0
  122. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +11 -0
  123. package/dist/inspector/sketch/SketchFrameOverlay.js +119 -0
  124. package/dist/inspector/sketch/SketchLayersPanel.d.ts +2 -0
  125. package/dist/inspector/sketch/SketchLayersPanel.js +63 -0
  126. package/dist/inspector/sketch/SketchOverlay.d.ts +19 -0
  127. package/dist/inspector/sketch/SketchOverlay.js +420 -0
  128. package/dist/inspector/sketch/SketchPromptPanel.d.ts +15 -0
  129. package/dist/inspector/sketch/SketchPromptPanel.js +18 -0
  130. package/dist/inspector/sketch/SketchWorkspace.d.ts +13 -0
  131. package/dist/inspector/sketch/SketchWorkspace.js +152 -0
  132. package/dist/inspector/sketch/capture.d.ts +55 -0
  133. package/dist/inspector/sketch/capture.js +350 -0
  134. package/dist/inspector/sketch/freehand.d.ts +13 -0
  135. package/dist/inspector/sketch/freehand.js +64 -0
  136. package/dist/inspector/sketch/geometry.d.ts +17 -0
  137. package/dist/inspector/sketch/geometry.js +98 -0
  138. package/dist/inspector/sketch/handoff.d.ts +21 -0
  139. package/dist/inspector/sketch/handoff.js +72 -0
  140. package/dist/inspector/sketch/interaction.d.ts +21 -0
  141. package/dist/inspector/sketch/interaction.js +98 -0
  142. package/dist/inspector/sketch/model.d.ts +104 -0
  143. package/dist/inspector/sketch/model.js +282 -0
  144. package/dist/inspector/sketch/persistence.d.ts +21 -0
  145. package/dist/inspector/sketch/persistence.js +205 -0
  146. package/dist/inspector/sketch/raster.d.ts +22 -0
  147. package/dist/inspector/sketch/raster.js +203 -0
  148. package/dist/inspector/sketch/sketchNote.d.ts +10 -0
  149. package/dist/inspector/sketch/sketchNote.js +30 -0
  150. package/dist/inspector/sketch/store.d.ts +47 -0
  151. package/dist/inspector/sketch/store.js +409 -0
  152. package/dist/inspector/styleEditors/BorderEditor.js +15 -19
  153. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -6
  154. package/dist/inspector/styleEditors/BoxShadowEditor.js +12 -1
  155. package/dist/inspector/styleEditors/ColorPicker.js +11 -36
  156. package/dist/inspector/styleEditors/GapField.d.ts +3 -2
  157. package/dist/inspector/styleEditors/GapField.js +2 -2
  158. package/dist/inspector/styleEditors/GridSection.js +1 -1
  159. package/dist/inspector/styleEditors/LayoutSection.js +11 -8
  160. package/dist/inspector/styleEditors/SpacingBox.js +23 -30
  161. package/dist/inspector/styleEditors/layoutPreviewState.d.ts +5 -0
  162. package/dist/inspector/styleEditors/layoutPreviewState.js +19 -0
  163. package/dist/inspector/styleEditors/layoutValue.js +3 -0
  164. package/dist/inspector/styleEditors/nudgeValue.d.ts +11 -0
  165. package/dist/inspector/styleEditors/nudgeValue.js +91 -5
  166. package/dist/inspector/styleEditors/stylePresence.d.ts +7 -0
  167. package/dist/inspector/styleEditors/stylePresence.js +22 -0
  168. package/dist/inspector/styleEditors/textRelevance.d.ts +3 -0
  169. package/dist/inspector/styleEditors/textRelevance.js +35 -0
  170. package/dist/inspector/styleEditors/useFieldVisibility.d.ts +6 -0
  171. package/dist/inspector/styleEditors/useFieldVisibility.js +26 -0
  172. package/dist/inspector/tokens/TokenField.js +111 -7
  173. package/dist/inspector/tokens/editActions.d.ts +2 -0
  174. package/dist/inspector/tokens/editActions.js +2 -2
  175. package/dist/inspector/tokens/resolution/cssomCollector.d.ts +1 -1
  176. package/dist/inspector/tokens/resolution/cssomCollector.js +9 -5
  177. package/dist/inspector/tokens/resolution/ruleMatching.d.ts +40 -0
  178. package/dist/inspector/tokens/resolution/ruleMatching.js +423 -0
  179. package/dist/inspector/tokens/resolution/selectorSemantics.d.ts +19 -3
  180. package/dist/inspector/tokens/resolution/selectorSemantics.js +378 -4
  181. package/dist/inspector/tokens/resolution.d.ts +3 -26
  182. package/dist/inspector/tokens/resolution.js +44 -735
  183. package/dist/inspector/tokens/tokenAvailability.d.ts +18 -0
  184. package/dist/inspector/tokens/tokenAvailability.js +204 -0
  185. package/dist/inspector/ui/Button.d.ts +1 -1
  186. package/dist/inspector/ui/SideValuesField.js +2 -2
  187. package/dist/inspector/ui/StatusCallout.d.ts +4 -1
  188. package/dist/inspector/ui/StatusCallout.js +2 -2
  189. package/dist/inspector/ui/styles.js +13 -11
  190. package/dist/internal/agent-protocol.d.ts +65 -1
  191. package/dist/internal/agent-protocol.js +183 -3
  192. package/dist/nudge-ui.mjs +261 -57
  193. package/dist/transport/editor.d.ts +28 -0
  194. package/dist/transport/editor.js +132 -0
  195. package/dist/transport/index.d.ts +1 -0
  196. package/dist/transport/index.js +1 -0
  197. package/dist/transport/routes.d.ts +7 -0
  198. package/dist/transport/routes.js +7 -0
  199. package/package.json +3 -1
  200. package/dist/inspector/shell/panelLayout.d.ts +0 -2
  201. package/dist/inspector/shell/panelLayout.js +0 -29
@@ -5,8 +5,8 @@ import { inlineAuthoredValue, meaningfulLayoutValue } from "./layoutValue.js";
5
5
  import { LayoutBlockedIndicator } from "./LayoutBlockedIndicator.js";
6
6
  export const GAP_PRESETS = ["0", "0.25rem", "0.5rem", "0.75rem", "1rem", "1.5rem", "2rem", "3rem"];
7
7
  /** Renders a gap value with a token chip or the legacy raw-value fallback. */
8
- export function GapField({ property, domElement: el, editTarget, selection, entries, tokenRows, onAfterEdit, }) {
8
+ export function GapField({ property, domElement: el, editTarget, selection, entries, tokenRows, leading, onAfterEdit, }) {
9
9
  const tokenRow = tokenRows.find((row) => row.property === property) ?? null;
10
10
  const blockedBy = inlineAuthoredValue(el, property);
11
- return (_jsxs("span", { className: `layout-combo${blockedBy ? " layout-combo--blocked" : ""}`, "data-test": "layout-combo", "data-property": property, children: [_jsx(TokenField, { property: property, selection: selection, tokenRow: tokenRow, initialValue: meaningfulLayoutValue(el, property), domElement: el, editTarget: editTarget, entries: entries, suggestions: GAP_PRESETS, inputDataTest: `layout-combo-input-${property}`, label: formatInspectorLabel(property), chipVariant: "small", disabled: blockedBy !== null, onAfterEdit: onAfterEdit }), blockedBy ? _jsx(LayoutBlockedIndicator, { blockedBy: blockedBy }) : null] }));
11
+ return (_jsxs("span", { className: `layout-combo${blockedBy ? " layout-combo--blocked" : ""}`, "data-test": "layout-combo", "data-property": property, children: [_jsx(TokenField, { property: property, selection: selection, tokenRow: tokenRow, initialValue: meaningfulLayoutValue(el, property), domElement: el, editTarget: editTarget, entries: entries, suggestions: GAP_PRESETS, leading: leading, inputDataTest: `layout-combo-input-${property}`, label: formatInspectorLabel(property), chipVariant: "small", disabled: blockedBy !== null, onAfterEdit: onAfterEdit }), blockedBy ? _jsx(LayoutBlockedIndicator, { blockedBy: blockedBy }) : null] }));
12
12
  }
@@ -23,5 +23,5 @@ const GRID_CONTENT_ALIGNMENT_OPTIONS = [
23
23
  const GRID_ITEM_ALIGNMENT_OPTIONS = ["normal", "stretch", "start", "end", "center", "self-start", "self-end"];
24
24
  export function GridSection({ domElement: el, editTarget, selection, entries, tokenRows, showContainer, showChild, revision = 0, onAfterEdit, }) {
25
25
  const [advancedOpen, setAdvancedOpen] = useState(false);
26
- return (_jsxs(_Fragment, { children: [showContainer ? _jsx("div", { className: "layout__group layout__grid", "data-test": "layout-grid-container", children: _jsxs("div", { className: "layout__grid-primary", children: [_jsxs("div", { className: "layout__grid-picker-column", children: [_jsxs("div", { className: "layout__grid-heading", children: [_jsx("div", { className: "editor__title", children: "Grid" }), _jsx(InspectorPopover, { "data-test": "layout-grid-settings", side: "bottom", align: "end", triggerElement: (_jsx(IconButton, { variant: "quiet", size: "default", label: "Grid settings", title: "Grid settings", "data-active": advancedOpen, "data-test": "layout-grid-settings", children: _jsx(IconSettings, { size: 16, stroke: 1.8, "aria-hidden": "true" }) })), open: advancedOpen, onOpenChange: setAdvancedOpen, children: _jsxs("div", { className: "layout__grid-advanced", "data-test": "layout-grid-advanced", children: [_jsx("div", { className: "editor__title", children: "Advanced grid CSS" }), _jsxs("div", { className: "layout__grid-fields", children: [_jsx(GridValueField, { property: "grid-template-columns", domElement: el, editTarget: editTarget, selection: selection, revision: revision, onAfterEdit: onAfterEdit }), _jsx(GridValueField, { property: "grid-template-rows", domElement: el, editTarget: editTarget, selection: selection, revision: revision, onAfterEdit: onAfterEdit }), _jsx(LayoutDropdown, { property: "grid-auto-flow", options: GRID_AUTO_FLOW_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, revision: revision, onAfterEdit: onAfterEdit }), _jsx(GridValueField, { property: "grid-auto-columns", domElement: el, editTarget: editTarget, selection: selection, revision: revision, onAfterEdit: onAfterEdit }), _jsx(GridValueField, { property: "grid-auto-rows", domElement: el, editTarget: editTarget, selection: selection, revision: revision, onAfterEdit: onAfterEdit })] }), _jsxs("div", { className: "layout__grid-alignment", "data-test": "layout-grid-alignment", children: [_jsx(LayoutDropdown, { property: "justify-content", options: GRID_CONTENT_ALIGNMENT_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, stacked: true, revision: revision, onAfterEdit: onAfterEdit }), _jsx(LayoutDropdown, { property: "align-content", options: GRID_CONTENT_ALIGNMENT_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, stacked: true, revision: revision, onAfterEdit: onAfterEdit }), _jsx(LayoutDropdown, { property: "justify-items", options: GRID_ITEM_ALIGNMENT_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, stacked: true, revision: revision, onAfterEdit: onAfterEdit }), _jsx(LayoutDropdown, { property: "align-items", options: GRID_ITEM_ALIGNMENT_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, stacked: true, revision: revision, onAfterEdit: onAfterEdit })] })] }) })] }), _jsx(GridPicker, { domElement: el, editTarget: editTarget, revision: revision, onAfterEdit: onAfterEdit })] }), _jsxs("div", { className: "layout__grid-gap", "data-test": "layout-grid-gap", children: [_jsx("div", { className: "editor__title", children: "Gap" }), _jsxs("div", { className: "layout__grid-gap-fields", children: [_jsxs(ControlSurface, { className: "layout__spacing-field", "data-test": "layout-grid-row-gap", children: [_jsx(IconSpacingVertical, { className: "layout__spacing-icon", size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true", "data-test": "layout-spacing-icon-row-gap" }), _jsx(GapField, { property: "row-gap", domElement: el, editTarget: editTarget, selection: selection, entries: entries, tokenRows: tokenRows, onAfterEdit: onAfterEdit })] }), _jsxs(ControlSurface, { className: "layout__spacing-field", "data-test": "layout-grid-column-gap", children: [_jsx(IconSpacingHorizontal, { className: "layout__spacing-icon", size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true", "data-test": "layout-spacing-icon-column-gap" }), _jsx(GapField, { property: "column-gap", domElement: el, editTarget: editTarget, selection: selection, entries: entries, tokenRows: tokenRows, onAfterEdit: onAfterEdit })] })] })] })] }) }) : null, showChild ? _jsx(GridChildSection, { domElement: el, editTarget: editTarget, revision: revision, onAfterEdit: onAfterEdit }) : null] }));
26
+ return (_jsxs(_Fragment, { children: [showContainer ? _jsx("div", { className: "layout__group layout__grid", "data-test": "layout-grid-container", children: _jsxs("div", { className: "layout__grid-primary", children: [_jsxs("div", { className: "layout__grid-picker-column", children: [_jsxs("div", { className: "layout__grid-heading", children: [_jsx("div", { className: "editor__title", children: "Grid" }), _jsx(InspectorPopover, { "data-test": "layout-grid-settings", side: "bottom", align: "end", triggerElement: (_jsx(IconButton, { variant: "quiet", size: "default", label: "Grid settings", title: "Grid settings", "data-active": advancedOpen, "data-test": "layout-grid-settings", children: _jsx(IconSettings, { size: 16, stroke: 1.8, "aria-hidden": "true" }) })), open: advancedOpen, onOpenChange: setAdvancedOpen, children: _jsxs("div", { className: "layout__grid-advanced", "data-test": "layout-grid-advanced", children: [_jsx("div", { className: "editor__title", children: "Advanced grid CSS" }), _jsxs("div", { className: "layout__grid-fields", children: [_jsx(GridValueField, { property: "grid-template-columns", domElement: el, editTarget: editTarget, selection: selection, revision: revision, onAfterEdit: onAfterEdit }), _jsx(GridValueField, { property: "grid-template-rows", domElement: el, editTarget: editTarget, selection: selection, revision: revision, onAfterEdit: onAfterEdit }), _jsx(LayoutDropdown, { property: "grid-auto-flow", options: GRID_AUTO_FLOW_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, revision: revision, onAfterEdit: onAfterEdit }), _jsx(GridValueField, { property: "grid-auto-columns", domElement: el, editTarget: editTarget, selection: selection, revision: revision, onAfterEdit: onAfterEdit }), _jsx(GridValueField, { property: "grid-auto-rows", domElement: el, editTarget: editTarget, selection: selection, revision: revision, onAfterEdit: onAfterEdit })] }), _jsxs("div", { className: "layout__grid-alignment", "data-test": "layout-grid-alignment", children: [_jsx(LayoutDropdown, { property: "justify-content", options: GRID_CONTENT_ALIGNMENT_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, stacked: true, revision: revision, onAfterEdit: onAfterEdit }), _jsx(LayoutDropdown, { property: "align-content", options: GRID_CONTENT_ALIGNMENT_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, stacked: true, revision: revision, onAfterEdit: onAfterEdit }), _jsx(LayoutDropdown, { property: "justify-items", options: GRID_ITEM_ALIGNMENT_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, stacked: true, revision: revision, onAfterEdit: onAfterEdit }), _jsx(LayoutDropdown, { property: "align-items", options: GRID_ITEM_ALIGNMENT_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, stacked: true, revision: revision, onAfterEdit: onAfterEdit })] })] }) })] }), _jsx(GridPicker, { domElement: el, editTarget: editTarget, revision: revision, onAfterEdit: onAfterEdit })] }), _jsxs("div", { className: "layout__grid-gap", "data-test": "layout-grid-gap", children: [_jsx("div", { className: "editor__title", children: "Gap" }), _jsxs("div", { className: "layout__grid-gap-fields", children: [_jsx(ControlSurface, { className: "layout__spacing-field", "data-test": "layout-grid-row-gap", children: _jsx(GapField, { property: "row-gap", domElement: el, editTarget: editTarget, selection: selection, entries: entries, tokenRows: tokenRows, leading: (_jsx(IconSpacingVertical, { className: "layout__spacing-icon", size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true", "data-test": "layout-spacing-icon-row-gap" })), onAfterEdit: onAfterEdit }) }), _jsx(ControlSurface, { className: "layout__spacing-field", "data-test": "layout-grid-column-gap", children: _jsx(GapField, { property: "column-gap", domElement: el, editTarget: editTarget, selection: selection, entries: entries, tokenRows: tokenRows, leading: (_jsx(IconSpacingHorizontal, { className: "layout__spacing-icon", size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true", "data-test": "layout-spacing-icon-column-gap" })), onAfterEdit: onAfterEdit }) })] })] })] }) }) : null, showChild ? _jsx(GridChildSection, { domElement: el, editTarget: editTarget, revision: revision, onAfterEdit: onAfterEdit }) : null] }));
27
27
  }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useRef, useState } from "react";
2
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
3
3
  import { IconAdjustmentsHorizontal, IconArrowsMaximize, IconArrowsMinimize, IconCheck, IconLetterH, IconLetterW, IconSettings, IconSpacingHorizontal, IconSpacingVertical, IconTextWrap } from "@tabler/icons-react";
4
4
  import { TokenField } from "../tokens/TokenField.js";
5
5
  import { LayoutDropdown } from "./LayoutDropdown.js";
@@ -20,6 +20,7 @@ import { getElementComputedStyle } from "../runtime/domRealm.js";
20
20
  import { FieldRow } from "../ui/FieldRow.js";
21
21
  import { ControlSurface } from "../ui/ControlSurface.js";
22
22
  import { getNudgeUiTokenEntries } from "../runtime/runtimeConfig.js";
23
+ import { getCanvasProjectionAcknowledgementVersion, subscribeCanvasProjectionAcknowledgements, } from "../canvas/projection.js";
23
24
  const DISPLAY_OPTIONS = ["block", "inline", "inline-block", "flex", "inline-flex", "grid", "inline-grid", "none", "contents"];
24
25
  const POSITION_OPTIONS = ["static", "relative", "absolute", "fixed", "sticky"];
25
26
  const FLEX_DIRECTION_OPTIONS = ["row", "row-reverse", "column", "column-reverse"];
@@ -46,8 +47,10 @@ export function LayoutSection(props) {
46
47
  const [primaryIsGridChild, setPrimaryIsGridChild] = useState(false);
47
48
  const [primaryPosition, setPrimaryPosition] = useState(() => getStateStyleValue(el, "position", "static"));
48
49
  const [layoutRevision, setLayoutRevision] = useState(0);
49
- const [flexDirection] = useComputedLayoutValue(el, "flex-direction", "row", layoutRevision);
50
- const [flexWrap] = useComputedLayoutValue(el, "flex-wrap", "nowrap", layoutRevision);
50
+ const projectionAcknowledgementVersion = useSyncExternalStore(subscribeCanvasProjectionAcknowledgements, getCanvasProjectionAcknowledgementVersion, getCanvasProjectionAcknowledgementVersion);
51
+ const computedRevision = layoutRevision + projectionAcknowledgementVersion;
52
+ const [flexDirection] = useComputedLayoutValue(el, "flex-direction", "row", computedRevision);
53
+ const [flexWrap] = useComputedLayoutValue(el, "flex-wrap", "nowrap", computedRevision);
51
54
  const selectedDirection = selection?.getProperty("flex-direction")?.value;
52
55
  const selectedWrap = selection?.getProperty("flex-wrap")?.value;
53
56
  const mixedFlexDirection = selectedDirection?.kind === "mixed";
@@ -76,7 +79,7 @@ export function LayoutSection(props) {
76
79
  catch {
77
80
  // noop
78
81
  }
79
- }, [el, layoutRevision]);
82
+ }, [el, computedRevision]);
80
83
  useEffect(() => {
81
84
  try {
82
85
  const parentEl = el.parentElement;
@@ -93,7 +96,7 @@ export function LayoutSection(props) {
93
96
  setPrimaryIsFlexChild(false);
94
97
  setPrimaryIsGridChild(false);
95
98
  }
96
- }, [el, layoutRevision]);
99
+ }, [el, computedRevision]);
97
100
  const isFlexContainer = selection ? selection.supportsRole("flex-container") : primaryIsFlexContainer;
98
101
  const isFlexChild = selection ? selection.supportsRole("flex-child") : primaryIsFlexChild;
99
102
  const isGridContainer = selection ? selection.supportsRole("grid-container") : primaryIsGridContainer;
@@ -103,7 +106,7 @@ export function LayoutSection(props) {
103
106
  const showPositionInsets = selection
104
107
  ? selection.supportsRole("inset-position")
105
108
  : position === "absolute" || position === "fixed";
106
- return (_jsxs("div", { className: "editor", "data-test": "layout-section", children: [_jsx("div", { className: "editor__title", children: "Layout" }), _jsxs("div", { className: "layout", children: [_jsxs("div", { className: "layout__basic", children: [_jsx(SizeSection, { domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, revision: layoutRevision, onAfterEdit: notifyAfterEdit }), _jsxs("div", { className: "layout__tool-row", children: [_jsx(LayoutDropdown, { property: "display", options: DISPLAY_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, revision: layoutRevision, onAfterEdit: notifyAfterEdit }), _jsx(LayoutDropdown, { property: "position", options: POSITION_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, revision: layoutRevision, onAfterEdit: notifyAfterEdit })] })] }), isFlexContainer ? (_jsxs("div", { className: "layout__group", "data-test": "layout-flex-container", children: [_jsx("div", { className: "editor__title", children: "Flex" }), _jsx("div", { className: "layout__flex-toolbar", children: _jsxs("div", { className: "layout__direction-tools", children: [_jsx(FlexDirectionControl, { domElement: el, editTarget: editTarget, selection: selection, revision: layoutRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexWrapToggle, { domElement: el, editTarget: editTarget, selection: selection, revision: layoutRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexSettingsMenu, { domElement: el, editTarget: editTarget, selection: selection, revision: layoutRevision, onAfterEdit: notifyAfterEdit })] }) }), _jsxs("div", { className: "layout__flex-lower", children: [mixedFlexDirection ? (_jsx("div", { className: "layout__alignment-grid", "data-test": "layout-flex-axis-mixed", children: "Choose a shared flex direction to edit physical alignment." })) : (_jsx(FlexAlignmentGrid, { domElement: el, editTarget: editTarget, selection: selection, revision: layoutRevision, onAfterEdit: notifyAfterEdit })), _jsx("div", { className: "layout__gap-column", "data-test": "layout-gap", children: _jsxs("div", { className: "layout__gap-fields", children: [_jsxs("div", { className: "layout__spacing-primary", children: [_jsx(FlexGapField, { property: relevantGap, domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, onAfterEdit: notifyAfterEdit }), _jsx(FlexDistributionControl, { domElement: el, editTarget: editTarget, selection: selection, revision: layoutRevision, onAfterEdit: notifyAfterEdit })] }), isFlexWrapped ? (_jsx(FlexGapField, { property: lineGap, domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, onAfterEdit: notifyAfterEdit })) : null] }) })] })] })) : null, isFlexChild ? (_jsxs("div", { className: "layout__group", "data-test": "layout-flex-child", children: [_jsx("div", { className: "editor__title", children: "Flex Child" }), _jsxs("div", { className: "layout__flex-child-fields", children: [_jsx(FlexChildValueField, { label: "Grow", property: "flex-grow", presets: FLEX_GROW_PRESETS, domElement: el, editTarget: editTarget, selection: selection, inputOnly: true, revision: layoutRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexChildValueField, { label: "Shrink", property: "flex-shrink", presets: FLEX_SHRINK_PRESETS, domElement: el, editTarget: editTarget, selection: selection, inputOnly: true, revision: layoutRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexChildValueField, { label: "Basis", property: "flex-basis", presets: FLEX_BASIS_PRESETS, domElement: el, editTarget: editTarget, selection: selection, inputOnly: true, revision: layoutRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexChildSettingsMenu, { domElement: el, editTarget: editTarget, selection: selection, revision: layoutRevision, onAfterEdit: notifyAfterEdit })] })] })) : null, isGridContainer || isGridChild ? (_jsx(GridSection, { domElement: el, showContainer: isGridContainer, showChild: isGridChild, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, revision: layoutRevision, onAfterEdit: notifyAfterEdit })) : null, showPositionInsets ? (_jsx(PositionInsets, { domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, revision: layoutRevision, onAfterEdit: notifyAfterEdit })) : null] })] }));
109
+ return (_jsxs("div", { className: "editor", "data-test": "layout-section", children: [_jsx("div", { className: "editor__title", children: "Layout" }), _jsxs("div", { className: "layout", children: [_jsxs("div", { className: "layout__basic", children: [_jsx(SizeSection, { domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsxs("div", { className: "layout__tool-row", children: [_jsx(LayoutDropdown, { property: "display", options: DISPLAY_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(LayoutDropdown, { property: "position", options: POSITION_OPTIONS, domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] })] }), isFlexContainer ? (_jsxs("div", { className: "layout__group", "data-test": "layout-flex-container", children: [_jsx("div", { className: "editor__title", children: "Flex" }), _jsx("div", { className: "layout__flex-toolbar", children: _jsxs("div", { className: "layout__direction-tools", children: [_jsx(FlexDirectionControl, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexWrapToggle, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexSettingsMenu, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] }) }), _jsxs("div", { className: "layout__flex-lower", children: [mixedFlexDirection ? (_jsx("div", { className: "layout__alignment-grid", "data-test": "layout-flex-axis-mixed", children: "Choose a shared flex direction to edit physical alignment." })) : (_jsx(FlexAlignmentGrid, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })), _jsx("div", { className: "layout__gap-column", "data-test": "layout-gap", children: _jsxs("div", { className: "layout__gap-fields", children: [_jsxs("div", { className: "layout__spacing-primary", children: [_jsx(FlexGapField, { property: relevantGap, domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, onAfterEdit: notifyAfterEdit }), _jsx(FlexDistributionControl, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] }), isFlexWrapped ? (_jsx(FlexGapField, { property: lineGap, domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, onAfterEdit: notifyAfterEdit })) : null] }) })] })] })) : null, isFlexChild ? (_jsxs("div", { className: "layout__group", "data-test": "layout-flex-child", children: [_jsx("div", { className: "editor__title", children: "Flex Child" }), _jsxs("div", { className: "layout__flex-child-fields", children: [_jsx(FlexChildValueField, { label: "Grow", property: "flex-grow", presets: FLEX_GROW_PRESETS, domElement: el, editTarget: editTarget, selection: selection, inputOnly: true, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexChildValueField, { label: "Shrink", property: "flex-shrink", presets: FLEX_SHRINK_PRESETS, domElement: el, editTarget: editTarget, selection: selection, inputOnly: true, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexChildValueField, { label: "Basis", property: "flex-basis", presets: FLEX_BASIS_PRESETS, domElement: el, editTarget: editTarget, selection: selection, inputOnly: true, revision: computedRevision, onAfterEdit: notifyAfterEdit }), _jsx(FlexChildSettingsMenu, { domElement: el, editTarget: editTarget, selection: selection, revision: computedRevision, onAfterEdit: notifyAfterEdit })] })] })) : null, isGridContainer || isGridChild ? (_jsx(GridSection, { domElement: el, showContainer: isGridContainer, showChild: isGridChild, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, revision: computedRevision, onAfterEdit: notifyAfterEdit })) : null, showPositionInsets ? (_jsx(PositionInsets, { domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, revision: computedRevision, onAfterEdit: notifyAfterEdit })) : null] })] }));
107
110
  }
108
111
  function SizeSection({ domElement: el, editTarget, selection, entries, tokenRows, revision, onAfterEdit }) {
109
112
  const [expanded, setExpanded] = useState(false);
@@ -132,8 +135,8 @@ function FlexChildSettingsMenu({ domElement, editTarget, selection, revision = 0
132
135
  return (_jsx(InspectorPopover, { "data-test": "layout-flex-child-settings", side: "left", align: "start", triggerElement: (_jsx(IconButton, { variant: "quiet", size: "default", label: "Flex child settings", title: "Flex child settings", "data-test": "layout-flex-child-settings", children: _jsx(IconSettings, { size: 16, stroke: 1.8, "aria-hidden": "true" }) })), open: open, onOpenChange: setOpen, children: _jsxs("div", { className: "layout__flex-child-settings", "data-test": "layout-flex-child-settings-content", children: [_jsx("div", { className: "layout__flex-child-settings-title", children: "Flex Child Settings" }), _jsx(LayoutDropdown, { property: "align-self", options: ALIGN_SELF_OPTIONS, domElement: domElement, editTarget: editTarget, selection: selection, stacked: true, revision: revision, onAfterEdit: onAfterEdit }), _jsx(FlexChildValueField, { label: "Order", property: "order", presets: ORDER_PRESETS, domElement: domElement, editTarget: editTarget, selection: selection, revision: revision, onAfterEdit: onAfterEdit })] }) }));
133
136
  }
134
137
  function FlexGapField({ property, domElement, editTarget, selection, entries, tokenRows, onAfterEdit }) {
135
- const field = (_jsx(GapField, { property: property, domElement: domElement, editTarget: editTarget, selection: selection, entries: entries, tokenRows: tokenRows, onAfterEdit: onAfterEdit }));
136
- return (_jsxs(ControlSurface, { className: "layout__spacing-field", children: [property === "column-gap" ? (_jsx(IconSpacingHorizontal, { className: "layout__spacing-icon", size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true", "data-test": `layout-spacing-icon-${property}` })) : (_jsx(IconSpacingVertical, { className: "layout__spacing-icon", size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true", "data-test": `layout-spacing-icon-${property}` })), field] }));
138
+ const GapIcon = property === "column-gap" ? IconSpacingHorizontal : IconSpacingVertical;
139
+ return (_jsx(ControlSurface, { className: "layout__spacing-field", children: _jsx(GapField, { property: property, domElement: domElement, editTarget: editTarget, selection: selection, entries: entries, tokenRows: tokenRows, leading: (_jsx(GapIcon, { className: "layout__spacing-icon", size: "var(--icon-size-small)", stroke: 1.8, "aria-hidden": "true", "data-test": `layout-spacing-icon-${property}` })), onAfterEdit: onAfterEdit }) }));
137
140
  }
138
141
  function FlexDirectionControl({ domElement, editTarget, selection, revision = 0, onAfterEdit }) {
139
142
  const [direction, setDirection] = useComputedLayoutValue(domElement, "flex-direction", "row", revision);
@@ -1,5 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useState } from "react";
3
2
  import { IconPlus } from "@tabler/icons-react";
4
3
  import { findTokenRow, metadataFor } from "./rowLookup.js";
5
4
  import { TokenField, TokenValueField } from "../tokens/TokenField.js";
@@ -10,6 +9,8 @@ import { completeCssValue } from "./completeCssValue.js";
10
9
  import { valuePolicyFor } from "./valuePolicy.js";
11
10
  import { projectInspectorValues, projectInspectorValuesForSelection, } from "../spacing/projection.js";
12
11
  import { getStateStyleValue } from "../shell/stateValue.js";
12
+ import { isZeroCssValue } from "./stylePresence.js";
13
+ import { useFieldVisibility } from "./useFieldVisibility.js";
13
14
  export function SpacingBox(props) {
14
15
  const { element, selection, entries, tokenRows = [], onAfterEdit } = props;
15
16
  const el = element.domElement;
@@ -27,44 +28,36 @@ export function SpacingBox(props) {
27
28
  return (_jsxs("div", { className: "editor", "data-test": "spacing-box", children: [_jsx("div", { className: "editor__title", children: "Spacing" }), _jsxs("div", { className: "spacing", children: [_jsx(SpacingField, { property: "padding", projection: projection.spacing.padding, domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, onAfterEdit: onAfterEdit }), _jsx(SpacingField, { property: "margin", projection: projection.spacing.margin, domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, onAfterEdit: onAfterEdit }), showRelativeInset ? _jsx(SpacingField, { property: "inset", projection: projection.spacing.inset, domElement: el, editTarget: editTarget, selection: selection, entries: allEntries, tokenRows: tokenRows, onAfterEdit: onAfterEdit }) : null] })] }));
28
29
  }
29
30
  export function SpacingField({ property, projection: spacingProjection, domElement: el, editTarget, selection, entries, tokenRows, onAfterEdit, showLabel = true, showEmptyState = true, suggestions, }) {
30
- const [fieldsAdded, setFieldsAdded] = useState(false);
31
31
  const pairDefinitions = [
32
32
  { axis: "horizontal", sideProperties: [sideProperty(property, "right"), sideProperty(property, "left")] },
33
33
  { axis: "vertical", sideProperties: [sideProperty(property, "top"), sideProperty(property, "bottom")] },
34
34
  ];
35
- const sideSlots = SIDE_NAMES.map((side) => ({
36
- side,
37
- icon: property === "padding" ? _jsx(PaddingSideIndicator, { side: side }) : _jsx(MarginSideIndicator, { side: side }),
38
- control: (_jsx(TokenField, { property: sideProperty(property, side), selection: selection, tokenRow: findTokenRow(tokenRows, sideProperty(property, side)), domElement: el, editTarget: editTarget, entries: entries, suggestions: suggestions, editMetadata: metadataFor(findTokenRow(tokenRows, sideProperty(property, side))), onAfterEdit: onAfterEdit, chipVariant: "small" })),
39
- }));
40
- const spacingIsEmpty = SIDE_NAMES.every((side) => property === "inset"
41
- ? isEmptyInsetValue(spacingProjection.fields[side].value)
42
- : isZeroSpacingValue(spacingProjection.fields[side].value));
43
- useEffect(() => {
44
- setFieldsAdded(false);
45
- }, [el]);
46
- useEffect(() => {
47
- if (!spacingIsEmpty)
48
- setFieldsAdded(false);
49
- }, [spacingIsEmpty]);
35
+ const sideSlots = SIDE_NAMES.map((side) => {
36
+ const icon = property === "padding" ? _jsx(PaddingSideIndicator, { side: side }) : _jsx(MarginSideIndicator, { side: side });
37
+ const sideName = sideProperty(property, side);
38
+ return {
39
+ side,
40
+ icon: null,
41
+ control: (_jsx(TokenField, { property: sideName, selection: selection, tokenRow: findTokenRow(tokenRows, sideName), domElement: el, editTarget: editTarget, entries: entries, suggestions: suggestions, editMetadata: metadataFor(findTokenRow(tokenRows, sideName)), onAfterEdit: onAfterEdit, chipVariant: "small", leading: icon })),
42
+ };
43
+ });
44
+ const hasSpacing = SIDE_NAMES.some((side) => {
45
+ // Zero spacing is visually empty regardless of whether it came from a
46
+ // reset, a component rule, or a token. Open editors stay open below.
47
+ const value = spacingProjection.fields[side].value.trim();
48
+ return value !== "" && !isZeroCssValue(value) && (property !== "inset" || value !== "auto");
49
+ });
50
+ const visibility = useFieldVisibility(selection?.domElements ?? [el], property, hasSpacing);
50
51
  const pairSlots = pairDefinitions.map(({ axis, sideProperties }) => ({
51
52
  axis,
52
- icon: _jsx(SpacingAxisIndicator, { property: property, axis: axis }),
53
- control: (_jsx(PairedTokenField, { displayProperty: `${property}-${axis}`, sideProperties: sideProperties, domElement: el, editTarget: editTarget, axisProjection: spacingProjection.axes[axis], entries: entries, suggestions: suggestions, onAfterEdit: onAfterEdit, chipVariant: "small" })),
53
+ icon: null,
54
+ control: (_jsx(PairedTokenField, { displayProperty: `${property}-${axis}`, sideProperties: sideProperties, domElement: el, editTarget: editTarget, axisProjection: spacingProjection.axes[axis], entries: entries, suggestions: suggestions, onAfterEdit: onAfterEdit, chipVariant: "small", leading: _jsx(SpacingAxisIndicator, { property: property, axis: axis }) })),
54
55
  }));
55
- return (_jsx(SideValuesField, { label: property, "data-test": property === "inset" ? "layout-inset" : `spacing-${property}`, "data-property": property, resetKey: el, pairedControls: pairSlots, showLabel: showLabel, empty: showEmptyState && spacingIsEmpty && !fieldsAdded, onAdd: () => setFieldsAdded(true), emptyAction: property === "inset" ? (_jsx(IconButton, { variant: "quiet", size: "default", "data-test": "add-inset", label: "Add Inset Values", title: "Add Inset Values", onClick: () => setFieldsAdded(true), children: _jsx(IconPlus, { size: 16, "aria-hidden": "true" }) })) : undefined, sides: sideSlots }));
56
+ return (_jsx(SideValuesField, { label: property, "data-test": property === "inset" ? "layout-inset" : `spacing-${property}`, "data-property": property, resetKey: el, pairedControls: pairSlots, showLabel: showLabel, empty: showEmptyState && !visibility.visible, onAdd: visibility.show, emptyAction: property === "inset" ? (_jsx(IconButton, { variant: "quiet", size: "default", "data-test": "add-inset", label: "Add Inset Values", title: "Add Inset Values", onClick: visibility.show, children: _jsx(IconPlus, { size: 16, "aria-hidden": "true" }) })) : undefined, sides: sideSlots }));
56
57
  }
57
58
  function sideProperty(property, side) {
58
59
  return property === "inset" ? side : `${property}-${side}`;
59
60
  }
60
- function isZeroSpacingValue(value) {
61
- const normalized = value.trim().toLowerCase();
62
- return !normalized || /^-?0(?:\.0+)?(?:[a-z%]+)?$/.test(normalized);
63
- }
64
- function isEmptyInsetValue(value) {
65
- const normalized = value.trim().toLowerCase();
66
- return !normalized || normalized === "auto" || /^-?0(?:\.0+)?(?:[a-z%]+)?$/.test(normalized);
67
- }
68
61
  function PaddingSideIndicator({ side }) {
69
62
  if (side === "left") {
70
63
  return (_jsxs("svg", { className: "side-values__icon side-values__side-icon", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [_jsx("rect", { x: "3", y: "3", width: "18", height: "18", rx: "2", stroke: "currentColor", strokeWidth: "var(--icon-stroke-width)" }), _jsx("line", { x1: "6.75", y1: "7", x2: "6.75", y2: "17", stroke: "currentColor", strokeWidth: "var(--icon-stroke-width)", strokeLinecap: "round" })] }));
@@ -89,7 +82,7 @@ function SpacingAxisIndicator({ property, axis, }) {
89
82
  }
90
83
  return (_jsxs("svg", { className: "side-values__icon side-values__axis-icon", viewBox: "0 0 24 24", fill: "none", "aria-hidden": "true", children: [_jsx("rect", { x: "19", y: "6", width: "12", height: "14", rx: "2", transform: "rotate(90 19 6)", stroke: "currentColor", strokeWidth: "var(--icon-stroke-width)" }), _jsx("line", { x1: "19", y1: "2", x2: "5", y2: "2", stroke: "currentColor", strokeWidth: "var(--icon-stroke-width)", strokeLinecap: "round" }), _jsx("line", { x1: "19", y1: "22", x2: "5", y2: "22", stroke: "currentColor", strokeWidth: "var(--icon-stroke-width)", strokeLinecap: "round" })] }));
91
84
  }
92
- function PairedTokenField({ displayProperty, sideProperties, domElement: el, editTarget, axisProjection, entries, suggestions, onAfterEdit, chipVariant, }) {
85
+ function PairedTokenField({ displayProperty, sideProperties, domElement: el, editTarget, axisProjection, entries, suggestions, onAfterEdit, chipVariant, leading, }) {
93
86
  const row = pairTokenRow(displayProperty, axisProjection);
94
87
  const groupMixed = axisProjection.fields.some((field) => field.value === "Mixed");
95
88
  const valuesMatch = axisProjection.state === "shared" && !groupMixed;
@@ -130,7 +123,7 @@ function PairedTokenField({ displayProperty, sideProperties, domElement: el, edi
130
123
  })));
131
124
  if (records.some(Boolean))
132
125
  onAfterEdit?.();
133
- }, onUnlink: commitAxisValue }));
126
+ }, onUnlink: commitAxisValue, leading: leading }));
134
127
  }
135
128
  function fieldDisplayValue(field) {
136
129
  return field.authoredValue.trim() || field.value;
@@ -0,0 +1,5 @@
1
+ /** Marks a temporary inline preview so layout fields do not treat it as authored CSS. */
2
+ export declare function beginLayoutPreview(el: HTMLElement, property: string): void;
3
+ /** Clears a temporary inline preview after the canvas drag is restored or committed. */
4
+ export declare function endLayoutPreview(el: HTMLElement, property: string): void;
5
+ export declare function isLayoutPreview(el: HTMLElement, property: string): boolean;
@@ -0,0 +1,19 @@
1
+ const activePreviews = new WeakMap();
2
+ /** Marks a temporary inline preview so layout fields do not treat it as authored CSS. */
3
+ export function beginLayoutPreview(el, property) {
4
+ const properties = activePreviews.get(el) ?? new Set();
5
+ properties.add(property);
6
+ activePreviews.set(el, properties);
7
+ }
8
+ /** Clears a temporary inline preview after the canvas drag is restored or committed. */
9
+ export function endLayoutPreview(el, property) {
10
+ const properties = activePreviews.get(el);
11
+ if (!properties)
12
+ return;
13
+ properties.delete(property);
14
+ if (properties.size === 0)
15
+ activePreviews.delete(el);
16
+ }
17
+ export function isLayoutPreview(el, property) {
18
+ return activePreviews.get(el)?.has(property) ?? false;
19
+ }
@@ -2,6 +2,7 @@ import { getActiveStyleState } from "../shell/styleState.js";
2
2
  import { getStateStyleValue } from "../shell/stateValue.js";
3
3
  import { getElementComputedStyle } from "../runtime/domRealm.js";
4
4
  import { getBrowserCssInspection } from "../inspection/browserCssInspectionRegistry.js";
5
+ import { isLayoutPreview } from "./layoutPreviewState.js";
5
6
  const DEFAULT_LAYOUT_VALUES = {
6
7
  width: "auto",
7
8
  height: "auto",
@@ -76,6 +77,8 @@ const INLINE_SHORTHAND_SOURCES = {
76
77
  * revert (see `verifyPreview`'s "inline-style" conflict).
77
78
  */
78
79
  export function inlineAuthoredValue(el, property) {
80
+ if (isLayoutPreview(el, property))
81
+ return null;
79
82
  const direct = el.style.getPropertyValue(property).trim();
80
83
  if (direct)
81
84
  return `${property}: ${direct}`;
@@ -4,5 +4,16 @@ export type NudgeDirection = -1 | 1;
4
4
  * standalone number that the inspector can safely edit.
5
5
  */
6
6
  export declare function nudgeCssValue(property: string, rawValue: string, direction: NudgeDirection, large?: boolean): string | null;
7
+ /** Returns whether a style field exposes the horizontal drag nudge affordance. */
8
+ export declare function supportsDragNudge(property: string): boolean;
9
+ /**
10
+ * Applies one nudge per two horizontal pointer pixels. Large nudges still use
11
+ * one 8px step per pointer pixel. When a large drag starts from an off-grid
12
+ * value, the first step snaps to the next large-step boundary in the drag
13
+ * direction.
14
+ */
15
+ export declare function nudgeCssValueByDrag(property: string, rawValue: string, deltaX: number, large?: boolean, snapToLargeStep?: boolean): string | null;
16
+ /** Returns whether a field has a numeric value that can respond to dragging. */
17
+ export declare function canNudgeCssValueByDrag(property: string, rawValue: string): boolean;
7
18
  /** Returns a clamped opacity percentage nudged by 1%, or 10% with Shift. */
8
19
  export declare function nudgeOpacityValue(rawValue: string, direction: NudgeDirection, large?: boolean): string | null;
@@ -2,6 +2,7 @@ import { completeCssValue, CSS_NUMBER_SOURCE } from "./completeCssValue.js";
2
2
  import { valuePolicyFor } from "./valuePolicy.js";
3
3
  const CSS_NUMBER = new RegExp(`^(?:${CSS_NUMBER_SOURCE})$`, "i");
4
4
  const CSS_NUMERIC_LITERAL = new RegExp(`^(${CSS_NUMBER_SOURCE})(px|rem|em|%)?$`, "i");
5
+ const DRAG_NUDGE_PROPERTY = /^(?:(?:padding|margin)-(?:top|right|bottom|left|horizontal|vertical)|(?:top|right|bottom|left|inset|inset-(?:horizontal|vertical))|(?:row|column)-gap|border-radius|border-(?:top-left|top-right|bottom-right|bottom-left)-radius)$/;
5
6
  /**
6
7
  * Returns a nudged CSS numeric literal, or null when the value is not a
7
8
  * standalone number that the inspector can safely edit.
@@ -14,13 +15,46 @@ export function nudgeCssValue(property, rawValue, direction, large = false) {
14
15
  const step = nudgeStep(property, parsed.unit, large);
15
16
  if (step === null)
16
17
  return null;
17
- let next = parsed.number + direction * step;
18
- if (isFontWeight(property))
19
- next = Math.min(1000, Math.max(1, next));
20
- if (isLineHeight(property))
21
- next = Math.max(0, next);
18
+ const next = clampNudgeNumber(property, parsed.number + direction * step);
22
19
  return `${formatNumber(next)}${parsed.unit}`;
23
20
  }
21
+ /** Returns whether a style field exposes the horizontal drag nudge affordance. */
22
+ export function supportsDragNudge(property) {
23
+ return DRAG_NUDGE_PROPERTY.test(property);
24
+ }
25
+ /**
26
+ * Applies one nudge per two horizontal pointer pixels. Large nudges still use
27
+ * one 8px step per pointer pixel. When a large drag starts from an off-grid
28
+ * value, the first step snaps to the next large-step boundary in the drag
29
+ * direction.
30
+ */
31
+ export function nudgeCssValueByDrag(property, rawValue, deltaX, large = false, snapToLargeStep = false) {
32
+ const steps = Math.trunc(Math.abs(deltaX) / (large ? 1 : 2));
33
+ if (steps === 0)
34
+ return null;
35
+ const direction = deltaX < 0 ? -1 : 1;
36
+ let next = rawValue;
37
+ let firstRegularStep = 0;
38
+ if (large && snapToLargeStep) {
39
+ const snapped = snapCssValueToLargeStep(property, next, direction);
40
+ if (snapped === null)
41
+ return null;
42
+ next = snapped;
43
+ firstRegularStep = 1;
44
+ }
45
+ for (let index = firstRegularStep; index < steps; index += 1) {
46
+ const nudged = nudgeDragStep(property, next, direction, large);
47
+ if (nudged === null)
48
+ return null;
49
+ next = nudged;
50
+ }
51
+ return next;
52
+ }
53
+ /** Returns whether a field has a numeric value that can respond to dragging. */
54
+ export function canNudgeCssValueByDrag(property, rawValue) {
55
+ return nudgeCssValueByDrag(property, rawValue, 2) !== null
56
+ || nudgeCssValueByDrag(property, rawValue, -2) !== null;
57
+ }
24
58
  /** Returns a clamped opacity percentage nudged by 1%, or 10% with Shift. */
25
59
  export function nudgeOpacityValue(rawValue, direction, large = false) {
26
60
  const parsed = parseNumericLiteral(rawValue);
@@ -59,12 +93,64 @@ function nudgeStep(property, unit, large) {
59
93
  return large ? 1 : 0.125;
60
94
  return null;
61
95
  }
96
+ function nudgeDragStep(property, rawValue, direction, large) {
97
+ if (!isAxisPairProperty(property))
98
+ return nudgeCssValue(property, rawValue, direction, large);
99
+ const parts = rawValue.split(",").map((part) => part.trim());
100
+ if (parts.length !== 2)
101
+ return nudgeCssValue(property, rawValue, direction, large);
102
+ const nudgedParts = parts.map((part) => nudgeCssValue(property, part, direction, large));
103
+ if (nudgedParts.some((part) => part === null))
104
+ return null;
105
+ return nudgedParts.join(", ");
106
+ }
107
+ function snapCssValueToLargeStep(property, rawValue, direction) {
108
+ if (!isAxisPairProperty(property))
109
+ return snapNumericValueToLargeStep(property, rawValue, direction);
110
+ const parts = rawValue.split(",").map((part) => part.trim());
111
+ if (parts.length !== 2)
112
+ return snapNumericValueToLargeStep(property, rawValue, direction);
113
+ const snappedParts = parts.map((part) => snapNumericValueToLargeStep(property, part, direction));
114
+ if (snappedParts.some((part) => part === null))
115
+ return null;
116
+ return snappedParts.join(", ");
117
+ }
118
+ function snapNumericValueToLargeStep(property, rawValue, direction) {
119
+ const normalised = normaliseForNudge(property, rawValue);
120
+ const parsed = parseNumericLiteral(normalised);
121
+ if (!parsed)
122
+ return null;
123
+ const step = nudgeStep(property, parsed.unit, true);
124
+ if (step === null)
125
+ return null;
126
+ let next = direction > 0
127
+ ? Math.ceil(parsed.number / step) * step
128
+ : Math.floor(parsed.number / step) * step;
129
+ if ((direction > 0 && next <= parsed.number) || (direction < 0 && next >= parsed.number)) {
130
+ next += direction * step;
131
+ }
132
+ next = clampNudgeNumber(property, next);
133
+ return `${formatNumber(next)}${parsed.unit}`;
134
+ }
62
135
  function isLineHeight(property) {
63
136
  return property === "line-height" || property.includes("line-height");
64
137
  }
65
138
  function isFontWeight(property) {
66
139
  return property === "font-weight" || property.includes("font-weight");
67
140
  }
141
+ function isNonNegativeDimension(property) {
142
+ return /^(?:padding|(?:row|column)-gap|border-(?:top-left|top-right|bottom-right|bottom-left)-radius|border-radius)/.test(property);
143
+ }
144
+ function isAxisPairProperty(property) {
145
+ return /^(?:padding|margin|inset)-(?:horizontal|vertical)$/.test(property);
146
+ }
147
+ function clampNudgeNumber(property, value) {
148
+ if (isFontWeight(property))
149
+ return Math.min(1000, Math.max(1, value));
150
+ if (isLineHeight(property) || isNonNegativeDimension(property))
151
+ return Math.max(0, value);
152
+ return value;
153
+ }
68
154
  function formatNumber(value) {
69
155
  const rounded = Number(value.toFixed(12));
70
156
  return Object.is(rounded, -0) ? "0" : String(rounded);
@@ -0,0 +1,7 @@
1
+ import type { ResolvedProperty } from "../../css/model/index.js";
2
+ import type { StyleSelection } from "../selection/styleSelection.js";
3
+ /** Includes every selected element, even when their resolved zero values agree. */
4
+ export declare function hasAuthoredProperty(selection: StyleSelection | null | undefined, property: string, row?: ResolvedProperty | null): boolean;
5
+ export declare function isZeroCssValue(value: string): boolean;
6
+ /** A direct declaration is meaningful even when its resolved value is empty. */
7
+ export declare function hasAuthoredStyle(row: ResolvedProperty | null | undefined): boolean;
@@ -0,0 +1,22 @@
1
+ /** Includes every selected element, even when their resolved zero values agree. */
2
+ export function hasAuthoredProperty(selection, property, row) {
3
+ return selection
4
+ ? selection.getProperty(property)?.rows.some(hasAuthoredStyle) ?? false
5
+ : hasAuthoredStyle(row);
6
+ }
7
+ export function isZeroCssValue(value) {
8
+ return /^[+-]?(?:0+(?:\.0*)?|\.0+)(?:[a-z%]+)?$/i.test(value.trim());
9
+ }
10
+ /** A direct declaration is meaningful even when its resolved value is empty. */
11
+ export function hasAuthoredStyle(row) {
12
+ if (!row || row.evidence.inheritedFrom)
13
+ return false;
14
+ const value = (row.authored ?? row.declaredValue).trim();
15
+ if (!value)
16
+ return false;
17
+ // Keep tokens and expressions. Only neutral literals from blanket rules
18
+ // (such as Preflight's `* { padding: 0; border: 0 solid; }`) are baseline.
19
+ const blanketRule = row.evidence.selector?.split(",").every((selector) => /^(?:\*|\*?::?(?:before|after|backdrop))$/.test(selector.trim()));
20
+ const neutral = value.split(/\s+/).every((part) => isZeroCssValue(part) || ["none", "solid", "transparent", "currentcolor"].includes(part.toLowerCase()));
21
+ return !blanketRule || !neutral;
22
+ }
@@ -0,0 +1,3 @@
1
+ import type { ResolvedProperty } from "../../css/model/index.js";
2
+ /** Text styling is useful for text runs and containers that author descendant typography. */
3
+ export declare function isTextRelevant(element: HTMLElement, rows: readonly ResolvedProperty[]): boolean;
@@ -0,0 +1,35 @@
1
+ import { getElementComputedStyle } from "../runtime/domRealm.js";
2
+ import { hasAuthoredStyle } from "./stylePresence.js";
3
+ const NON_TEXT_ELEMENTS = new Set(["VIDEO", "AUDIO", "IMG", "CANVAS", "IFRAME", "OBJECT", "EMBED", "SCRIPT", "STYLE"]);
4
+ const TEXT_INPUT_TYPES = new Set(["text", "search", "email", "url", "tel", "password", "number", "submit", "reset", "button"]);
5
+ const INLINE_TEXT_ELEMENTS = new Set(["SPAN", "A", "STRONG", "EM", "B", "I", "U", "S", "SMALL", "LABEL", "CODE", "SUP", "SUB"]);
6
+ const TYPOGRAPHY_PROPERTIES = new Set([
7
+ "font", "font-family", "font-size", "font-style", "font-weight", "line-height",
8
+ "letter-spacing", "word-spacing", "text-align", "text-transform", "white-space",
9
+ ]);
10
+ function hasTextContent(element, includeBlocks) {
11
+ if (NON_TEXT_ELEMENTS.has(element.tagName))
12
+ return false;
13
+ const style = getElementComputedStyle(element);
14
+ if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse")
15
+ return false;
16
+ if (element.tagName === "TEXTAREA" || element.tagName === "SELECT")
17
+ return true;
18
+ if (element.tagName === "INPUT")
19
+ return TEXT_INPUT_TYPES.has(element.getAttribute("type")?.toLowerCase() ?? "text");
20
+ return Array.from(element.childNodes).some((node) => {
21
+ if (node.nodeType === 3)
22
+ return Boolean(node.textContent?.trim());
23
+ if (node.nodeType !== 1)
24
+ return false;
25
+ // SAFETY: ELEMENT_NODE above establishes the child is an Element.
26
+ const child = node;
27
+ const inline = INLINE_TEXT_ELEMENTS.has(child.tagName) || getElementComputedStyle(child).display.startsWith("inline");
28
+ return (includeBlocks || inline) && hasTextContent(child, includeBlocks);
29
+ });
30
+ }
31
+ /** Text styling is useful for text runs and containers that author descendant typography. */
32
+ export function isTextRelevant(element, rows) {
33
+ const authorsTypography = rows.some((row) => TYPOGRAPHY_PROPERTIES.has(row.property) && hasAuthoredStyle(row));
34
+ return hasTextContent(element, authorsTypography);
35
+ }
@@ -0,0 +1,6 @@
1
+ /** Keeps an opened field mounted when an edit reaches zero, none, or transparent. */
2
+ export declare function useFieldVisibility(elements: readonly HTMLElement[], property: string, present: boolean): {
3
+ visible: boolean;
4
+ show(): void;
5
+ hide(): void;
6
+ };
@@ -0,0 +1,26 @@
1
+ import { useState } from "react";
2
+ import { getActiveStyleState } from "../shell/styleState.js";
3
+ /** Keeps an opened field mounted when an edit reaches zero, none, or transparent. */
4
+ export function useFieldVisibility(elements, property, present) {
5
+ const styleState = getActiveStyleState();
6
+ const [previous, setPrevious] = useState({ elements, property, styleState, present, visible: present });
7
+ const selectionChanged = property !== previous.property
8
+ || styleState !== previous.styleState
9
+ || elements.length !== previous.elements.length
10
+ || elements.some((element, index) => element !== previous.elements[index]);
11
+ let current = previous;
12
+ if (selectionChanged || present !== previous.present) {
13
+ current = {
14
+ elements, property, styleState, present,
15
+ visible: selectionChanged ? present : previous.visible || present,
16
+ };
17
+ // Adjust before rendering children so a selection change cannot briefly
18
+ // mount the previous selection's controls or interrupt an active drag.
19
+ setPrevious(current);
20
+ }
21
+ return {
22
+ visible: current.visible,
23
+ show: () => setPrevious({ ...current, visible: true }),
24
+ hide: () => setPrevious({ ...current, visible: false }),
25
+ };
26
+ }