nudge-ui 0.2.2 → 0.2.3

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 (226) hide show
  1. package/README.md +164 -46
  2. package/dist/client.mjs +252 -61
  3. package/dist/css/value-semantics/colorSemantics.d.ts +4 -0
  4. package/dist/css/value-semantics/colorSemantics.js +17 -0
  5. package/dist/hosts/astro/astroRuntimeConfig.js +1 -0
  6. package/dist/hosts/astro/clientTransport.js +38 -32
  7. package/dist/hosts/astro/integration.js +5 -1
  8. package/dist/hosts/astro/markdownIdentity.d.ts +28 -0
  9. package/dist/hosts/astro/markdownIdentity.js +81 -0
  10. package/dist/hosts/next/manifest.js +1 -1
  11. package/dist/hosts/next/sidecar.js +5 -1
  12. package/dist/hosts/static/manifest.js +1 -1
  13. package/dist/hosts/static/server.js +3 -0
  14. package/dist/hosts/vite/vite.d.ts +3 -0
  15. package/dist/hosts/vite/vite.js +24 -4
  16. package/dist/inspector/agent/McpConnectionDialog.js +13 -19
  17. package/dist/inspector/agent/activity.d.ts +18 -0
  18. package/dist/inspector/agent/activity.js +70 -0
  19. package/dist/inspector/agent/client.d.ts +2 -0
  20. package/dist/inspector/agent/client.js +29 -1
  21. package/dist/inspector/agent/connectionStatus.js +1 -1
  22. package/dist/inspector/agent/httpTransport.js +9 -2
  23. package/dist/inspector/agent/protocol.d.ts +6 -3
  24. package/dist/inspector/agent/protocol.js +1 -1
  25. package/dist/inspector/agent/verification.d.ts +11 -1
  26. package/dist/inspector/agent/verification.js +53 -25
  27. package/dist/inspector/canvas/AgentActivityOverlay.d.ts +13 -0
  28. package/dist/inspector/canvas/AgentActivityOverlay.js +111 -0
  29. package/dist/inspector/canvas/CanvasCard.d.ts +9 -2
  30. package/dist/inspector/canvas/CanvasCard.js +133 -38
  31. package/dist/inspector/canvas/CanvasElementOverlay.js +152 -16
  32. package/dist/inspector/canvas/CanvasToolIcons.d.ts +10 -0
  33. package/dist/inspector/canvas/CanvasToolIcons.js +20 -0
  34. package/dist/inspector/canvas/CanvasToolbar.js +8 -4
  35. package/dist/inspector/canvas/CanvasWorkspace.js +225 -57
  36. package/dist/inspector/canvas/StaleChangeIndicator.js +3 -13
  37. package/dist/inspector/canvas/agentPresentation.d.ts +8 -8
  38. package/dist/inspector/canvas/agentPresentation.js +64 -52
  39. package/dist/inspector/canvas/canvasGestures.d.ts +1 -1
  40. package/dist/inspector/canvas/canvasGestures.js +1 -1
  41. package/dist/inspector/canvas/canvasStore.d.ts +46 -82
  42. package/dist/inspector/canvas/canvasStore.js +251 -284
  43. package/dist/inspector/canvas/frameContent.d.ts +21 -0
  44. package/dist/inspector/canvas/frameContent.js +44 -0
  45. package/dist/inspector/canvas/frameProtocol.d.ts +21 -4
  46. package/dist/inspector/canvas/frameProtocol.js +3 -3
  47. package/dist/inspector/canvas/keyboardShortcuts.d.ts +3 -1
  48. package/dist/inspector/canvas/keyboardShortcuts.js +4 -4
  49. package/dist/inspector/canvas/layout.d.ts +52 -0
  50. package/dist/inspector/canvas/layout.js +106 -0
  51. package/dist/inspector/canvas/projection.d.ts +15 -5
  52. package/dist/inspector/canvas/projection.js +69 -13
  53. package/dist/inspector/canvas/rendererBootstrap.js +32 -4
  54. package/dist/inspector/canvas/rendererElementSelector.js +47 -6
  55. package/dist/inspector/canvas/rendererStylesheet.js +2 -2
  56. package/dist/inspector/canvas/resizeHandleScale.js +1 -1
  57. package/dist/inspector/canvas/sessionStore.d.ts +11 -26
  58. package/dist/inspector/canvas/sessionStore.js +43 -183
  59. package/dist/inspector/canvas/staleChangeDetector.js +9 -3
  60. package/dist/inspector/canvas/useCanvasLayoutAnimation.d.ts +5 -0
  61. package/dist/inspector/canvas/useCanvasLayoutAnimation.js +107 -0
  62. package/dist/inspector/canvas/viewStore.d.ts +38 -0
  63. package/dist/inspector/canvas/viewStore.js +104 -0
  64. package/dist/inspector/canvas/wheelPan.d.ts +8 -0
  65. package/dist/inspector/canvas/wheelPan.js +8 -0
  66. package/dist/inspector/changes/changesLog.d.ts +6 -5
  67. package/dist/inspector/changes/changesLog.js +40 -32
  68. package/dist/inspector/changes/draftChanges.d.ts +56 -0
  69. package/dist/inspector/changes/draftChanges.js +300 -0
  70. package/dist/inspector/changes/previewDiagnostics.d.ts +4 -2
  71. package/dist/inspector/changes/previewDiagnostics.js +11 -25
  72. package/dist/inspector/clientManifest.d.ts +1 -1
  73. package/dist/inspector/comments/CommentsOverlay.d.ts +2 -0
  74. package/dist/inspector/comments/CommentsOverlay.js +250 -0
  75. package/dist/inspector/comments/element.d.ts +6 -0
  76. package/dist/inspector/comments/element.js +47 -0
  77. package/dist/inspector/comments/store.d.ts +29 -0
  78. package/dist/inspector/comments/store.js +121 -0
  79. package/dist/inspector/comments/verification.d.ts +5 -0
  80. package/dist/inspector/comments/verification.js +52 -0
  81. package/dist/inspector/drafts/model.d.ts +35 -0
  82. package/dist/inspector/drafts/model.js +45 -0
  83. package/dist/inspector/drafts/store.d.ts +28 -0
  84. package/dist/inspector/drafts/store.js +231 -0
  85. package/dist/inspector/index.js +20 -27
  86. package/dist/inspector/inline-text/inlineTextEditor.js +7 -0
  87. package/dist/inspector/inspection/useBrowserCssInspection.js +15 -1
  88. package/dist/inspector/iterations/capture.d.ts +2 -0
  89. package/dist/inspector/iterations/capture.js +173 -0
  90. package/dist/inspector/iterations/client.d.ts +7 -0
  91. package/dist/inspector/iterations/client.js +52 -0
  92. package/dist/inspector/iterations/coordinator.d.ts +15 -0
  93. package/dist/inspector/iterations/coordinator.js +130 -0
  94. package/dist/inspector/iterations/prompt.d.ts +3 -0
  95. package/dist/inspector/iterations/prompt.js +26 -0
  96. package/dist/inspector/overlay/DomNavigator.js +21 -15
  97. package/dist/inspector/overlay/interactionStyles.js +3 -1
  98. package/dist/inspector/overlay/spacingGestures.d.ts +13 -1
  99. package/dist/inspector/overlay/spacingGestures.js +82 -5
  100. package/dist/inspector/projection/{workspaceProjection.d.ts → draftProjection.d.ts} +6 -6
  101. package/dist/inspector/projection/{workspaceProjection.js → draftProjection.js} +4 -4
  102. package/dist/inspector/projection/renderedInstance.d.ts +2 -1
  103. package/dist/inspector/projection/renderedInstance.js +9 -5
  104. package/dist/inspector/projection/structuralProjection.d.ts +2 -1
  105. package/dist/inspector/projection/structuralProjection.js +19 -15
  106. package/dist/inspector/projection/textProjection.d.ts +2 -1
  107. package/dist/inspector/projection/textProjection.js +8 -4
  108. package/dist/inspector/prompt/PromptSettingsDialog.js +1 -5
  109. package/dist/inspector/prompt/clipboardHandoff.d.ts +10 -18
  110. package/dist/inspector/prompt/clipboardHandoff.js +92 -143
  111. package/dist/inspector/prompt/generatePrompt.d.ts +4 -1
  112. package/dist/inspector/prompt/generatePrompt.js +17 -16
  113. package/dist/inspector/runtime/runtimeConfig.d.ts +9 -0
  114. package/dist/inspector/runtime/runtimeConfig.js +24 -0
  115. package/dist/inspector/settings/SettingsDialog.js +21 -8
  116. package/dist/inspector/shell/AgentStatusButton.d.ts +11 -0
  117. package/dist/inspector/shell/AgentStatusButton.js +36 -0
  118. package/dist/inspector/shell/AppShell.js +4 -2
  119. package/dist/inspector/shell/ChangesLog.js +46 -26
  120. package/dist/inspector/shell/CopyPromptButton.js +138 -89
  121. package/dist/inspector/shell/EmptyState.js +9 -8
  122. package/dist/inspector/shell/InspectorShell.js +13 -21
  123. package/dist/inspector/shell/PeekOriginalButton.js +24 -23
  124. package/dist/inspector/shell/originalPreview.js +5 -5
  125. package/dist/inspector/shell/shortcuts.d.ts +3 -0
  126. package/dist/inspector/shell/shortcuts.js +11 -0
  127. package/dist/inspector/sketch/SketchEditor.js +14 -14
  128. package/dist/inspector/sketch/SketchEntryButton.js +1 -1
  129. package/dist/inspector/sketch/SketchFrameOverlay.d.ts +3 -1
  130. package/dist/inspector/sketch/SketchFrameOverlay.js +11 -8
  131. package/dist/inspector/sketch/SketchLayersPanel.js +13 -5
  132. package/dist/inspector/sketch/SketchOverlay.js +7 -7
  133. package/dist/inspector/sketch/SketchWorkspace.js +18 -12
  134. package/dist/inspector/sketch/interaction.d.ts +1 -0
  135. package/dist/inspector/sketch/interaction.js +8 -4
  136. package/dist/inspector/sketch/model.d.ts +5 -0
  137. package/dist/inspector/sketch/model.js +6 -2
  138. package/dist/inspector/sketch/raster.d.ts +1 -2
  139. package/dist/inspector/sketch/raster.js +68 -4
  140. package/dist/inspector/styleEditors/BackgroundEditor.d.ts +4 -0
  141. package/dist/inspector/styleEditors/BackgroundEditor.js +195 -0
  142. package/dist/inspector/styleEditors/BorderEditor.js +49 -78
  143. package/dist/inspector/styleEditors/BorderRadiusEditor.js +6 -1
  144. package/dist/inspector/styleEditors/BoxShadowEditor.d.ts +1 -2
  145. package/dist/inspector/styleEditors/BoxShadowEditor.js +132 -4
  146. package/dist/inspector/styleEditors/ColorPicker.d.ts +5 -1
  147. package/dist/inspector/styleEditors/ColorPicker.js +10 -4
  148. package/dist/inspector/styleEditors/GapField.js +3 -5
  149. package/dist/inspector/styleEditors/GridChildSection.d.ts +1 -1
  150. package/dist/inspector/styleEditors/GridChildSection.js +18 -9
  151. package/dist/inspector/styleEditors/GridPicker.d.ts +1 -1
  152. package/dist/inspector/styleEditors/GridPicker.js +8 -2
  153. package/dist/inspector/styleEditors/GridSection.js +1 -1
  154. package/dist/inspector/styleEditors/GridValueField.js +19 -14
  155. package/dist/inspector/styleEditors/ImageBackgroundEditor.d.ts +18 -0
  156. package/dist/inspector/styleEditors/ImageBackgroundEditor.js +118 -0
  157. package/dist/inspector/styleEditors/LayoutBlockedIndicator.d.ts +2 -2
  158. package/dist/inspector/styleEditors/LayoutBlockedIndicator.js +3 -3
  159. package/dist/inspector/styleEditors/LayoutComboField.js +6 -4
  160. package/dist/inspector/styleEditors/LayoutDropdown.js +6 -3
  161. package/dist/inspector/styleEditors/LayoutSection.js +32 -11
  162. package/dist/inspector/styleEditors/SpacingBox.js +7 -5
  163. package/dist/inspector/styleEditors/Typography.js +11 -3
  164. package/dist/inspector/styleEditors/backgroundLayers.d.ts +24 -0
  165. package/dist/inspector/styleEditors/backgroundLayers.js +51 -0
  166. package/dist/inspector/styleEditors/gradientValue.d.ts +17 -0
  167. package/dist/inspector/styleEditors/gradientValue.js +114 -0
  168. package/dist/inspector/styleEditors/inlineAuthored.d.ts +10 -0
  169. package/dist/inspector/styleEditors/inlineAuthored.js +114 -0
  170. package/dist/inspector/styleEditors/layoutValue.d.ts +0 -8
  171. package/dist/inspector/styleEditors/layoutValue.js +0 -42
  172. package/dist/inspector/styleEditors/nudgeValue.d.ts +2 -2
  173. package/dist/inspector/styleEditors/nudgeValue.js +36 -8
  174. package/dist/inspector/tokens/TokenField.d.ts +7 -0
  175. package/dist/inspector/tokens/TokenField.js +29 -21
  176. package/dist/inspector/tokens/TokenLabel.js +5 -14
  177. package/dist/inspector/tokens/editActions.d.ts +1 -1
  178. package/dist/inspector/tokens/editActions.js +1 -1
  179. package/dist/inspector/ui/AtRuleContext.js +4 -6
  180. package/dist/inspector/ui/Button.d.ts +6 -2
  181. package/dist/inspector/ui/Button.js +4 -3
  182. package/dist/inspector/ui/ControlSurface.d.ts +2 -2
  183. package/dist/inspector/ui/ControlSurface.js +4 -3
  184. package/dist/inspector/ui/InlineStyleWarning.d.ts +11 -0
  185. package/dist/inspector/ui/InlineStyleWarning.js +13 -0
  186. package/dist/inspector/ui/InspectorPopover.d.ts +2 -1
  187. package/dist/inspector/ui/InspectorPopover.js +4 -5
  188. package/dist/inspector/ui/PopoverListbox.d.ts +2 -1
  189. package/dist/inspector/ui/PopoverListbox.js +8 -7
  190. package/dist/inspector/ui/SegmentedControl.d.ts +2 -1
  191. package/dist/inspector/ui/SegmentedControl.js +2 -2
  192. package/dist/inspector/ui/Select.d.ts +6 -1
  193. package/dist/inspector/ui/Select.js +7 -11
  194. package/dist/inspector/ui/SideValuesField.js +1 -1
  195. package/dist/inspector/ui/Tooltip.d.ts +26 -0
  196. package/dist/inspector/ui/Tooltip.js +32 -0
  197. package/dist/inspector/ui/classNames.d.ts +2 -0
  198. package/dist/inspector/ui/classNames.js +4 -0
  199. package/dist/inspector/ui/customCursors.d.ts +23 -0
  200. package/dist/inspector/ui/customCursors.js +122 -0
  201. package/dist/inspector/ui/dragCursor.d.ts +0 -2
  202. package/dist/inspector/ui/dragCursor.js +2 -10
  203. package/dist/inspector/ui/portalContainer.d.ts +2 -0
  204. package/dist/inspector/ui/portalContainer.js +6 -0
  205. package/dist/inspector/ui/styles.js +29 -21
  206. package/dist/inspector/workspace/commands.d.ts +9 -0
  207. package/dist/inspector/workspace/commands.js +82 -0
  208. package/dist/inspector/workspace/controller.d.ts +4 -0
  209. package/dist/inspector/workspace/controller.js +66 -0
  210. package/dist/inspector/workspace/handoff.d.ts +42 -0
  211. package/dist/inspector/workspace/handoff.js +115 -0
  212. package/dist/inspector/workspace/timeline.d.ts +30 -0
  213. package/dist/inspector/workspace/timeline.js +108 -0
  214. package/dist/internal/agent-protocol.d.ts +19 -2
  215. package/dist/internal/agent-protocol.js +13 -0
  216. package/dist/nudge-ui.mjs +558 -311
  217. package/dist/project/artifacts.d.ts +13 -0
  218. package/dist/project/artifacts.js +253 -0
  219. package/dist/transport/artifacts.d.ts +8 -0
  220. package/dist/transport/artifacts.js +9 -0
  221. package/dist/transport/editor.js +3 -1
  222. package/dist/transport/routes.d.ts +2 -0
  223. package/dist/transport/routes.js +2 -0
  224. package/package.json +6 -2
  225. package/dist/inspector/changes/workspaceChanges.d.ts +0 -47
  226. package/dist/inspector/changes/workspaceChanges.js +0 -265
@@ -1,3 +1,4 @@
1
+ import type { FrameProjection } from "../canvas/projection.js";
1
2
  import { isTextProjectionTargetValue, type TextContentChangeRecord, type TextProjectionTarget, type TextProjectionScope, type TextBindingEvidence } from "../inline-text/textChangeBoundary.js";
2
3
  /** Strict JSON boundary guard for a text projection target. */
3
4
  export declare const isTextProjectionTarget: typeof isTextProjectionTargetValue;
@@ -48,7 +49,7 @@ export declare function setCanonicalTextContentChanges(changes: ReadonlyArray<Te
48
49
  export declare function getTextProjectionReports(doc: Document): readonly TextProjectionReport[];
49
50
  export declare function subscribeTextProjectionDiagnostics(listener: () => void): () => void;
50
51
  export declare function getTextProjectionDiagnosticRevision(): number;
51
- export declare function recordCanvasTextProjectionReports(cardId: string, revision: number, reports: readonly TextProjectionReport[]): void;
52
+ export declare function recordCanvasTextProjectionReports(cardId: string, revision: number, reports: readonly TextProjectionReport[], projection?: FrameProjection): void;
52
53
  export declare function clearCanvasTextProjectionReports(cardId: string): void;
53
54
  export declare function getTextContentChangeDiagnostics(changeId: string): TextContentProjectionDiagnostic[];
54
55
  export declare function isTextProjectionReport(value: unknown): value is TextProjectionReport;
@@ -1,3 +1,4 @@
1
+ import { getActiveDraftChanges, getDraftChanges } from "../changes/draftChanges.js";
1
2
  import { sourceSiteSelector } from "../selection/sourceSite.js";
2
3
  import { isNudgeUiDev } from "../runtime/devFlag.js";
3
4
  import { isTextContentChangeValue, isTextProjectionTargetValue, } from "../inline-text/textChangeBoundary.js";
@@ -640,10 +641,10 @@ export function subscribeTextProjectionDiagnostics(listener) {
640
641
  export function getTextProjectionDiagnosticRevision() {
641
642
  return diagnosticRevision;
642
643
  }
643
- export function recordCanvasTextProjectionReports(cardId, revision, reports) {
644
+ export function recordCanvasTextProjectionReports(cardId, revision, reports, projection) {
644
645
  if (!Number.isSafeInteger(revision) || revision < 0 || !reports.every(isTextProjectionReport))
645
646
  return;
646
- const expected = new Set(canonicalChanges.keys());
647
+ const expected = projection ? new Set(projection.textContentChanges.map((record) => record.id)) : new Set(canonicalChanges.keys());
647
648
  if (reports.length !== expected.size
648
649
  || new Set(reports.map((report) => report.changeId)).size !== reports.length
649
650
  || reports.some((report) => !expected.has(report.changeId)))
@@ -654,7 +655,8 @@ export function recordCanvasTextProjectionReports(cardId, revision, reports) {
654
655
  const next = reports.map((report) => ({ ...report }));
655
656
  if (existing && existing.revision === revision && sameReports(existing.reports, next))
656
657
  return;
657
- reportsByCanvasCard.set(cardId, { revision, reports: next });
658
+ const current = getActiveDraftChanges();
659
+ reportsByCanvasCard.set(cardId, { revision, reports: next, draftId: projection?.draftId ?? current.draftId, draftRevision: projection?.draftRevision ?? current.revision });
658
660
  notifyDiagnostics();
659
661
  }
660
662
  export function clearCanvasTextProjectionReports(cardId) {
@@ -664,7 +666,7 @@ export function clearCanvasTextProjectionReports(cardId) {
664
666
  }
665
667
  export function getTextContentChangeDiagnostics(changeId) {
666
668
  const diagnostics = [];
667
- const change = canonicalChanges.get(changeId);
669
+ const change = getActiveDraftChanges().changes.find((change) => change.kind === "text-content" && change.id === changeId) ?? canonicalChanges.get(changeId);
668
670
  const host = reportsByDocument.get(document)?.find((report) => report.changeId === changeId);
669
671
  if (host)
670
672
  diagnostics.push({
@@ -674,6 +676,8 @@ export function getTextContentChangeDiagnostics(changeId) {
674
676
  evidence: change?.evidence,
675
677
  });
676
678
  for (const [cardId, entry] of reportsByCanvasCard) {
679
+ if (entry.draftId !== getActiveDraftChanges().draftId || entry.draftRevision !== getDraftChanges(entry.draftId).revision)
680
+ continue;
677
681
  const report = entry.reports.find((candidate) => candidate.changeId === changeId);
678
682
  if (report)
679
683
  diagnostics.push({
@@ -2,11 +2,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
2
2
  import { useRef } from "react";
3
3
  import { Dialog } from "@base-ui/react/dialog";
4
4
  import { IconX } from "@tabler/icons-react";
5
- function portalContainer() {
6
- return typeof document !== "undefined"
7
- ? document.getElementById("nudge-ui-root")?.shadowRoot ?? document.body
8
- : null;
9
- }
5
+ import { portalContainer } from "../ui/portalContainer.js";
10
6
  /** Edits the instructions appended to every generated prompt. */
11
7
  export function PromptSettingsFields({ value, onChange, instructionsRef, }) {
12
8
  return (_jsxs(_Fragment, { children: [_jsx("div", { className: "prompt-settings__field", children: _jsx("textarea", { ref: instructionsRef, id: "nudge-ui-custom-instructions", className: "prompt-settings__textarea", "data-test": "prompt-custom-instructions", "aria-label": "Custom instructions", rows: 6, value: value, onChange: (event) => onChange(event.currentTarget.value) }) }), _jsx("div", { className: "prompt-settings__actions", children: _jsx(Dialog.Close, { className: "button button--primary", "data-test": "prompt-settings-done", type: "button", children: "Done" }) })] }));
@@ -1,34 +1,26 @@
1
- import { type ChangeRecord } from "../changes/changesLog.js";
2
- import { type StructuralChange } from "../projection/structuralProjection.js";
1
+ import type { ChangeRecord } from "../changes/types.js";
2
+ import type { StructuralChange } from "../changes/structuralTypes.js";
3
+ import { type HandoffOwner } from "../agent/verification.js";
3
4
  export interface ClipboardHandoffFingerprint {
4
5
  readonly key: string;
5
6
  readonly fingerprint: string;
6
7
  }
7
- export interface ClipboardHandoffSnapshot {
8
+ interface ClipboardDraftHandoff {
9
+ readonly owner: HandoffOwner;
8
10
  readonly changes: readonly ClipboardHandoffFingerprint[];
9
11
  readonly structuralChanges: readonly ClipboardHandoffFingerprint[];
10
12
  }
11
- /** Validates an origin-local persisted clipboard checkpoint. */
13
+ export interface ClipboardHandoffSnapshot {
14
+ readonly drafts: readonly ClipboardDraftHandoff[];
15
+ }
12
16
  export declare function isClipboardHandoffSnapshot(value: unknown): value is ClipboardHandoffSnapshot | null;
13
- /** Captures the exact records included in a successful clipboard copy. */
14
- export declare function recordClipboardHandoff(changes: readonly ChangeRecord[], structuralChanges?: readonly StructuralChange[]): void;
15
- /** Returns a defensive copy for durable-session serialization. */
17
+ export declare function recordClipboardHandoff(changes: readonly ChangeRecord[], structuralChanges?: readonly StructuralChange[], owner?: HandoffOwner): void;
16
18
  export declare function getClipboardHandoffSnapshot(): ClipboardHandoffSnapshot | null;
17
- /** Replaces the in-memory checkpoint from a validated durable session. */
18
19
  export declare function hydrateClipboardHandoff(value: ClipboardHandoffSnapshot | null): void;
19
20
  export declare function clearClipboardHandoff(): void;
20
21
  export declare function subscribeClipboardHandoff(listener: () => void): () => void;
21
- /** Monotonic external-store snapshot for persistence and React consumers. */
22
22
  export declare function getClipboardHandoffRevision(): number;
23
23
  export declare function getLastClipboardReconciledCount(): number;
24
- /**
25
- * Checks a copied prompt against source-rendered output and reconciles only
26
- * exact records that the browser can positively verify.
27
- */
28
24
  export declare function reconcileClipboardHandoff(doc?: Document): Promise<number>;
29
- /**
30
- * Watches source-driven document refreshes and page-return signals. The
31
- * trailing debounce lets an HMR commit settle before verification lifts the
32
- * inspector preview for two frames.
33
- */
34
25
  export declare function startClipboardHandoffController(doc?: Document): () => void;
26
+ export {};
@@ -1,212 +1,161 @@
1
1
  import { changeKey } from "../changes/model.js";
2
- import { subscribeChanges, } from "../changes/changesLog.js";
3
- import {} from "../projection/structuralProjection.js";
4
- import { getWorkspaceChanges } from "../changes/workspaceChanges.js";
2
+ import { getDraftChanges, getActiveDraftChanges, subscribeActiveDraftChanges } from "../changes/draftChanges.js";
5
3
  import { documentRevisions, subscribeDocumentRevision } from "../tokens/resolution/cssomCollector.js";
6
- import { handoffChangeFingerprint, handoffStructuralFingerprint, verifyAndReconcileHandoff, } from "../agent/verification.js";
7
- const MAX_HANDOFF_RECORDS = 10_000;
8
- const MAX_KEY_LENGTH = 16_384;
9
- const MAX_FINGERPRINT_LENGTH = 1_048_576;
10
- const RECONCILE_DEBOUNCE_MS = 450;
11
- let checkpoint = null;
4
+ import { captureHandoffOwner, handoffChangeFingerprint, handoffStructuralFingerprint, verifyAndReconcileHandoff, } from "../agent/verification.js";
5
+ import { isDraftTarget } from "../drafts/model.js";
6
+ const checkpoints = new Map();
12
7
  let revision = 0;
13
- let lastReconciledCount = 0;
8
+ const reconciledCounts = new Map();
14
9
  const listeners = new Set();
15
10
  function notify() {
16
11
  revision += 1;
17
12
  for (const listener of listeners)
18
13
  listener();
19
14
  }
20
- function cloneSnapshot(snapshot) {
21
- return {
22
- changes: snapshot.changes.map((entry) => ({ ...entry })),
23
- structuralChanges: snapshot.structuralChanges.map((entry) => ({ ...entry })),
24
- };
25
- }
26
15
  function isFingerprint(value) {
27
- if (!value || typeof value !== "object" || Array.isArray(value))
16
+ if (!value || typeof value !== "object")
28
17
  return false;
29
18
  const entry = value;
30
- return Object.keys(entry).every((key) => key === "key" || key === "fingerprint")
31
- && typeof entry.key === "string"
32
- && entry.key.length > 0
33
- && entry.key.length <= MAX_KEY_LENGTH
34
- && typeof entry.fingerprint === "string"
35
- && entry.fingerprint.length > 0
36
- && entry.fingerprint.length <= MAX_FINGERPRINT_LENGTH;
19
+ return typeof entry.key === "string" && entry.key.length > 0 && entry.key.length <= 16_384
20
+ && typeof entry.fingerprint === "string" && entry.fingerprint.length > 0 && entry.fingerprint.length <= 1_048_576;
37
21
  }
38
- /** Validates an origin-local persisted clipboard checkpoint. */
39
22
  export function isClipboardHandoffSnapshot(value) {
40
23
  if (value === null)
41
24
  return true;
42
- if (!value || typeof value !== "object" || Array.isArray(value))
25
+ if (!value || typeof value !== "object")
43
26
  return false;
44
- const candidate = value;
45
- if (!Object.keys(candidate).every((key) => key === "changes" || key === "structuralChanges")
46
- || !Array.isArray(candidate.changes)
47
- || !Array.isArray(candidate.structuralChanges)
48
- || candidate.changes.length + candidate.structuralChanges.length > MAX_HANDOFF_RECORDS
49
- || !candidate.changes.every(isFingerprint)
50
- || !candidate.structuralChanges.every(isFingerprint)) {
27
+ const snapshot = value;
28
+ if (!Array.isArray(snapshot.drafts) || snapshot.drafts.length > 512)
51
29
  return false;
30
+ const ids = new Set();
31
+ for (const draft of snapshot.drafts) {
32
+ const owner = draft?.owner;
33
+ if (!owner || typeof owner.draftId !== "string" || ids.has(owner.draftId)
34
+ || !Number.isSafeInteger(owner.draftRevision) || owner.draftRevision < 0 || !isDraftTarget(owner.target)
35
+ || !Array.isArray(owner.frameIds) || owner.frameIds.some((id) => typeof id !== "string")
36
+ || !Array.isArray(draft.changes) || !Array.isArray(draft.structuralChanges)
37
+ || draft.changes.length + draft.structuralChanges.length > 10_000
38
+ || !draft.changes.every(isFingerprint) || !draft.structuralChanges.every(isFingerprint)
39
+ || new Set(draft.changes.map((entry) => entry.key)).size !== draft.changes.length
40
+ || new Set(draft.structuralChanges.map((entry) => entry.key)).size !== draft.structuralChanges.length)
41
+ return false;
42
+ ids.add(owner.draftId);
52
43
  }
53
- const changeKeys = candidate.changes.map((entry) => entry.key);
54
- const structuralKeys = candidate.structuralChanges.map((entry) => entry.key);
55
- return new Set(changeKeys).size === changeKeys.length
56
- && new Set(structuralKeys).size === structuralKeys.length;
44
+ return true;
57
45
  }
58
- /** Captures the exact records included in a successful clipboard copy. */
59
- export function recordClipboardHandoff(changes, structuralChanges = []) {
60
- checkpoint = {
61
- changes: changes.map((change) => ({
62
- key: changeKey(change),
63
- fingerprint: handoffChangeFingerprint(change),
64
- })),
65
- structuralChanges: structuralChanges.map((change) => ({
66
- key: change.id,
67
- fingerprint: handoffStructuralFingerprint(change),
68
- })),
46
+ function fingerprintSnapshot(owner, changes, structuralChanges) {
47
+ return {
48
+ owner: structuredClone(owner),
49
+ changes: changes.map((change) => ({ key: changeKey(change), fingerprint: handoffChangeFingerprint(change) })),
50
+ structuralChanges: structuralChanges.map((change) => ({ key: change.id, fingerprint: handoffStructuralFingerprint(change) })),
69
51
  };
70
- lastReconciledCount = 0;
52
+ }
53
+ export function recordClipboardHandoff(changes, structuralChanges = [], owner = captureHandoffOwner()) {
54
+ checkpoints.set(owner.draftId, fingerprintSnapshot(owner, changes, structuralChanges));
55
+ reconciledCounts.delete(owner.draftId);
71
56
  notify();
72
57
  }
73
- /** Returns a defensive copy for durable-session serialization. */
74
58
  export function getClipboardHandoffSnapshot() {
75
- return checkpoint ? cloneSnapshot(checkpoint) : null;
59
+ return checkpoints.size ? structuredClone({ drafts: [...checkpoints.values()] }) : null;
76
60
  }
77
- /** Replaces the in-memory checkpoint from a validated durable session. */
78
61
  export function hydrateClipboardHandoff(value) {
79
- if (!value && !checkpoint && lastReconciledCount === 0)
80
- return;
81
- checkpoint = value ? cloneSnapshot(value) : null;
82
- lastReconciledCount = 0;
62
+ checkpoints.clear();
63
+ for (const draft of value?.drafts ?? [])
64
+ checkpoints.set(draft.owner.draftId, structuredClone(draft));
65
+ reconciledCounts.clear();
83
66
  notify();
84
67
  }
85
68
  export function clearClipboardHandoff() {
86
- if (!checkpoint && lastReconciledCount === 0)
69
+ if (!checkpoints.size && !reconciledCounts.size)
87
70
  return;
88
- checkpoint = null;
89
- lastReconciledCount = 0;
71
+ checkpoints.clear();
72
+ reconciledCounts.clear();
90
73
  notify();
91
74
  }
92
75
  export function subscribeClipboardHandoff(listener) {
93
76
  listeners.add(listener);
94
77
  return () => listeners.delete(listener);
95
78
  }
96
- /** Monotonic external-store snapshot for persistence and React consumers. */
97
- export function getClipboardHandoffRevision() {
98
- return revision;
99
- }
100
- export function getLastClipboardReconciledCount() {
101
- return lastReconciledCount;
102
- }
103
- function matchingChanges(snapshot) {
104
- const expected = new Map(snapshot.changes.map((entry) => [entry.key, entry.fingerprint]));
105
- return getWorkspaceChanges().changes.filter((change) => expected.get(changeKey(change)) === handoffChangeFingerprint(change));
106
- }
107
- function matchingStructuralChanges(snapshot) {
108
- const expected = new Map(snapshot.structuralChanges.map((entry) => [entry.key, entry.fingerprint]));
109
- return getWorkspaceChanges().structuralChanges.filter((change) => expected.get(change.id) === handoffStructuralFingerprint(change));
79
+ export function getClipboardHandoffRevision() { return revision; }
80
+ export function getLastClipboardReconciledCount() { return reconciledCounts.get(getActiveDraftChanges().draftId) ?? 0; }
81
+ function matchingRecords(snapshot) {
82
+ const workspace = getDraftChanges(snapshot.owner.draftId);
83
+ const changes = new Map(snapshot.changes.map((entry) => [entry.key, entry.fingerprint]));
84
+ const structures = new Map(snapshot.structuralChanges.map((entry) => [entry.key, entry.fingerprint]));
85
+ return {
86
+ changes: workspace.changes.filter((change) => changes.get(changeKey(change)) === handoffChangeFingerprint(change)),
87
+ structuralChanges: workspace.structuralChanges.filter((change) => structures.get(change.id) === handoffStructuralFingerprint(change)),
88
+ };
110
89
  }
111
- /** Drops checkpoint entries that no longer describe current canonical intent. */
112
- function pruneCheckpoint() {
113
- if (!checkpoint)
114
- return false;
115
- const changes = matchingChanges(checkpoint);
116
- const structuralChanges = matchingStructuralChanges(checkpoint);
117
- if (changes.length === checkpoint.changes.length
118
- && structuralChanges.length === checkpoint.structuralChanges.length) {
119
- return false;
90
+ function pruneCheckpoints() {
91
+ let changed = false;
92
+ for (const [id, snapshot] of checkpoints) {
93
+ const matching = matchingRecords(snapshot);
94
+ if (matching.changes.length === snapshot.changes.length && matching.structuralChanges.length === snapshot.structuralChanges.length)
95
+ continue;
96
+ changed = true;
97
+ if (matching.changes.length + matching.structuralChanges.length === 0)
98
+ checkpoints.delete(id);
99
+ else
100
+ checkpoints.set(id, fingerprintSnapshot(snapshot.owner, matching.changes, matching.structuralChanges));
120
101
  }
121
- checkpoint = changes.length + structuralChanges.length === 0
122
- ? null
123
- : {
124
- changes: changes.map((change) => ({
125
- key: changeKey(change),
126
- fingerprint: handoffChangeFingerprint(change),
127
- })),
128
- structuralChanges: structuralChanges.map((change) => ({
129
- key: change.id,
130
- fingerprint: handoffStructuralFingerprint(change),
131
- })),
132
- };
133
- notify();
134
- return true;
102
+ if (changed)
103
+ notify();
135
104
  }
136
- /**
137
- * Checks a copied prompt against source-rendered output and reconciles only
138
- * exact records that the browser can positively verify.
139
- */
140
105
  export async function reconcileClipboardHandoff(doc = document) {
141
- const snapshot = checkpoint;
142
- if (!snapshot)
143
- return 0;
144
- const changes = matchingChanges(snapshot);
145
- const structuralChanges = matchingStructuralChanges(snapshot);
146
- if (changes.length + structuralChanges.length === 0) {
147
- pruneCheckpoint();
148
- return 0;
106
+ let removed = 0;
107
+ for (const snapshot of [...checkpoints.values()]) {
108
+ const matching = matchingRecords(snapshot);
109
+ if (matching.changes.length + matching.structuralChanges.length === 0)
110
+ continue;
111
+ const count = await verifyAndReconcileHandoff({ owner: snapshot.owner, ...matching }, doc);
112
+ if (count > 0)
113
+ reconciledCounts.set(snapshot.owner.draftId, count);
114
+ removed += count;
149
115
  }
150
- const removed = await verifyAndReconcileHandoff({ changes, structuralChanges }, doc);
151
- pruneCheckpoint();
116
+ pruneCheckpoints();
152
117
  if (removed > 0) {
153
- lastReconciledCount = removed;
154
118
  notify();
155
119
  }
156
120
  return removed;
157
121
  }
158
- /**
159
- * Watches source-driven document refreshes and page-return signals. The
160
- * trailing debounce lets an HMR commit settle before verification lifts the
161
- * inspector preview for two frames.
162
- */
163
122
  export function startClipboardHandoffController(doc = document) {
164
- const ownerWindow = doc.defaultView;
123
+ const view = doc.defaultView;
165
124
  let timer = null;
166
125
  let checking = false;
167
126
  let stopped = false;
168
127
  const schedule = () => {
169
- if (stopped || checking || !checkpoint)
128
+ if (stopped || checking || !checkpoints.size)
170
129
  return;
171
130
  if (timer !== null)
172
131
  clearTimeout(timer);
173
132
  timer = setTimeout(() => {
174
133
  timer = null;
175
- if (stopped || checking || !checkpoint)
134
+ if (stopped || checking || !checkpoints.size)
176
135
  return;
177
136
  checking = true;
178
- void reconcileClipboardHandoff(doc)
179
- .catch(() => undefined)
180
- .finally(() => {
181
- // Keep the guard through the MutationObserver microtask generated by
182
- // restoring managed projections, preventing a verification loop.
137
+ void reconcileClipboardHandoff(doc).catch(() => undefined).finally(() => {
183
138
  setTimeout(() => { checking = false; }, 0);
184
139
  });
185
- }, RECONCILE_DEBOUNCE_MS);
186
- };
187
- const onCanonicalChange = () => {
188
- pruneCheckpoint();
189
- };
190
- const onVisibilityChange = () => {
191
- if (doc.visibilityState === "visible")
192
- schedule();
140
+ }, 450);
193
141
  };
194
- // Initializes the document observer before registering the listener.
142
+ const visible = () => { if (doc.visibilityState === "visible")
143
+ schedule(); };
195
144
  documentRevisions(doc);
196
- const unsubscribeRevision = subscribeDocumentRevision(doc, schedule);
197
- const unsubscribeChanges = subscribeChanges(onCanonicalChange);
198
- ownerWindow?.addEventListener("focus", schedule);
199
- ownerWindow?.addEventListener("pageshow", schedule);
200
- doc.addEventListener("visibilitychange", onVisibilityChange);
145
+ const stopRevision = subscribeDocumentRevision(doc, schedule);
146
+ const stopChanges = subscribeActiveDraftChanges(pruneCheckpoints);
147
+ view?.addEventListener("focus", schedule);
148
+ view?.addEventListener("pageshow", schedule);
149
+ doc.addEventListener("visibilitychange", visible);
201
150
  schedule();
202
151
  return () => {
203
152
  stopped = true;
204
153
  if (timer !== null)
205
154
  clearTimeout(timer);
206
- unsubscribeRevision();
207
- unsubscribeChanges();
208
- ownerWindow?.removeEventListener("focus", schedule);
209
- ownerWindow?.removeEventListener("pageshow", schedule);
210
- doc.removeEventListener("visibilitychange", onVisibilityChange);
155
+ stopRevision();
156
+ stopChanges();
157
+ view?.removeEventListener("focus", schedule);
158
+ view?.removeEventListener("pageshow", schedule);
159
+ doc.removeEventListener("visibilitychange", visible);
211
160
  };
212
161
  }
@@ -1,8 +1,11 @@
1
1
  import type { ChangeRecord } from "../changes/changesLog.js";
2
2
  import type { StructuralChange } from "../projection/structuralProjection.js";
3
3
  import type { AgentSketchMetadata } from "../agent/protocol.js";
4
+ import { type ElementComment } from "../comments/store.js";
4
5
  export interface FrameworkHints {
5
6
  framework?: string;
6
7
  stylingSystem?: string;
7
8
  }
8
- export declare function generatePrompt(changes: ChangeRecord[], frameworkHints?: FrameworkHints, structuralChanges?: readonly StructuralChange[], customInstructions?: string, sketches?: readonly AgentSketchMetadata[]): string;
9
+ /** Counts the final edit intents exported by the prompt, preserving gesture history for undo. */
10
+ export declare function countPromptChanges(changes: readonly ChangeRecord[], structuralChanges?: readonly StructuralChange[], sketchCount?: number): number;
11
+ export declare function generatePrompt(changes: ChangeRecord[], frameworkHints?: FrameworkHints, structuralChanges?: readonly StructuralChange[], customInstructions?: string, sketches?: readonly AgentSketchMetadata[], comments?: readonly ElementComment[]): string;
@@ -5,6 +5,8 @@ import { formatComponentPropBaseline, formatComponentPropValue, } from "../compo
5
5
  import { boundRuntimeEvidence, isRuntimeGeneratedSource, normalizeRuntimeTag, normalizeRuntimeText, } from "../runtime/staticHtmlRuntimeIdentity.js";
6
6
  import { getSourceCoordinatePolicy } from "../runtime/runtimeConfig.js";
7
7
  import { DEFAULT_CUSTOM_INSTRUCTIONS } from "./promptSettings.js";
8
+ import { getCommentsForTarget } from "../comments/store.js";
9
+ import { documentTarget } from "../drafts/model.js";
8
10
  const EMPTY_SENTINEL = "<!-- No changes to export -->\n\nThe changes log is empty. Make a change in the Nudge UI inspector first.";
9
11
  function renderPrompt(sections, customInstructions) {
10
12
  const lines = [
@@ -244,17 +246,6 @@ function structuralChangeLine(change) {
244
246
  ? `- Move ${target} before ${renderedInstanceDescription(before)} in ${destinationParent}.`
245
247
  : `- Move ${target} to the end of ${destinationParent}.`;
246
248
  }
247
- function sketchLines(sketches) {
248
- const titles = [...new Set(sketches.map((sketch) => sketch.capture.title || "(untitled)"))];
249
- return [
250
- ...titles.map((title) => `- Page title: ${promptText(title)}`),
251
- ...sketches.flatMap((sketch) => [
252
- ...(sketch.description ? [`- Description: ${promptText(sketch.description)}`] : []),
253
- ...(sketch.annotations ?? []).map((annotation) => `- Annotation ${annotation.number}: ${promptText(annotation.description)}`),
254
- ]),
255
- "- Interpret the attached frozen screenshot, blue strokes, and numbered annotations as visual feedback. Do not derive selectors, source coordinates, or semantics from the strokes. Use each annotation number to match the note to its numbered dot.",
256
- ];
257
- }
258
249
  function renderedInstanceKey(ref) {
259
250
  const { sourceSite, locator } = ref;
260
251
  return JSON.stringify([
@@ -315,10 +306,14 @@ function canonicalizeStructuralChanges(changes) {
315
306
  }
316
307
  return canonical;
317
308
  }
318
- export function generatePrompt(changes, frameworkHints, structuralChanges = [], customInstructions = DEFAULT_CUSTOM_INSTRUCTIONS, sketches = []) {
309
+ /** Counts the final edit intents exported by the prompt, preserving gesture history for undo. */
310
+ export function countPromptChanges(changes, structuralChanges = [], sketchCount = 0) {
311
+ return canonicalizeChanges([...changes]).length + canonicalizeStructuralChanges(structuralChanges).length + sketchCount;
312
+ }
313
+ export function generatePrompt(changes, frameworkHints, structuralChanges = [], customInstructions = DEFAULT_CUSTOM_INSTRUCTIONS, sketches = [], comments = getCommentsForTarget(documentTarget(window.location.href))) {
319
314
  const deduplicated = canonicalizeChanges(changes);
320
315
  const structuralIntent = canonicalizeStructuralChanges(structuralChanges);
321
- if (deduplicated.length === 0 && structuralIntent.length === 0 && sketches.length === 0)
316
+ if (deduplicated.length === 0 && structuralIntent.length === 0 && sketches.length === 0 && comments.length === 0)
322
317
  return EMPTY_SENTINEL;
323
318
  const tokenChanges = deduplicated.filter(isTokenChange);
324
319
  const componentChanges = deduplicated.filter(isComponentChange);
@@ -398,10 +393,16 @@ export function generatePrompt(changes, frameworkHints, structuralChanges = [],
398
393
  lines: structuralIntent.map(structuralChangeLine),
399
394
  });
400
395
  }
401
- if (sketches.length > 0) {
396
+ if (comments.length > 0) {
402
397
  sections.push({
403
- heading: "Sketch annotations",
404
- lines: sketchLines(sketches),
398
+ heading: "Element comments",
399
+ lines: comments.flatMap((comment) => [
400
+ `### <${comment.tag}> (${comment.target.sourceSite.src || "source unknown"})`,
401
+ `- Page: ${promptText(comment.route)}`,
402
+ `- Target: ${renderedInstanceDescription(comment.target)}`,
403
+ `- Note: ${promptText(comment.note)}`,
404
+ "",
405
+ ]),
405
406
  });
406
407
  }
407
408
  return renderPrompt(sections, customInstructions);
@@ -39,6 +39,11 @@ export interface NudgeUiRuntimeCapabilities {
39
39
  */
40
40
  readonly scopingSelectorPattern?: string;
41
41
  }
42
+ /** Product features that a host can selectively expose in its inspector UI. */
43
+ export interface NudgeUiRuntimeFeatures {
44
+ /** Whether Canvas can create filesystem-backed HTML iterations. */
45
+ readonly canvasIterations?: boolean;
46
+ }
42
47
  /**
43
48
  * Browser knowledge supplied by a host Adapter before the inspector mounts.
44
49
  *
@@ -53,6 +58,8 @@ export interface NudgeUiRuntimeConfig {
53
58
  readonly framework: NudgeUiRuntimeFramework;
54
59
  readonly stylingSystem: string;
55
60
  readonly capabilities: NudgeUiRuntimeCapabilities;
61
+ /** Product-level feature switches; omitted features remain enabled. */
62
+ readonly features?: NudgeUiRuntimeFeatures;
56
63
  readonly tokenCatalog: readonly TokenDefinition[];
57
64
  readonly tokens: readonly TokenEntry[];
58
65
  readonly tokenDiagnostics: readonly TokenCatalogDiagnostic[];
@@ -91,6 +98,8 @@ export declare function normalizeNudgeUiRuntimeConfig(input: unknown): NudgeUiRu
91
98
  export declare function configureNudgeUiRuntime(config: NudgeUiRuntimeConfig): void;
92
99
  /** Returns the immutable runtime snapshot used by shared inspector Modules. */
93
100
  export declare function getNudgeUiRuntimeConfig(): NudgeUiRuntimeConfig;
101
+ /** Returns whether a product feature is enabled; omitted flags default to enabled. */
102
+ export declare function isNudgeUiFeatureEnabled(features: NudgeUiRuntimeFeatures | undefined, feature: keyof NudgeUiRuntimeFeatures): boolean;
94
103
  /**
95
104
  * Whether this document runs the explicit public landing demo (ADR-0014 and
96
105
  * ADR-0016). The plugin emits `demo: true` for the flagged demo route and for
@@ -98,6 +98,15 @@ function optionalBoolean(input, field) {
98
98
  }
99
99
  return value;
100
100
  }
101
+ function optionalFeatureBoolean(input, field) {
102
+ const value = input[field];
103
+ if (value === undefined)
104
+ return undefined;
105
+ if (typeof value !== "boolean") {
106
+ throw new TypeError(`Nudge UI runtime feature "${field}" must be a boolean; received ${JSON.stringify(value)}.`);
107
+ }
108
+ return value;
109
+ }
101
110
  function optionalNonEmptyString(input, field) {
102
111
  const value = input[field];
103
112
  if (value === undefined)
@@ -142,6 +151,15 @@ function normalizeCapabilities(input) {
142
151
  scopingSelectorPattern: optionalNonEmptyString(input, "scopingSelectorPattern"),
143
152
  };
144
153
  }
154
+ function normalizeFeatures(input) {
155
+ if (input === undefined)
156
+ return undefined;
157
+ if (!isPlainRecord(input)) {
158
+ throw new TypeError('Nudge UI runtime configuration field "features" must be an object.');
159
+ }
160
+ const canvasIterations = optionalFeatureBoolean(input, "canvasIterations");
161
+ return canvasIterations === undefined ? {} : { canvasIterations };
162
+ }
145
163
  function optionalDemoFlag(input) {
146
164
  const value = input.demo;
147
165
  if (value === undefined || value === false)
@@ -172,12 +190,14 @@ export function normalizeNudgeUiRuntimeConfig(input) {
172
190
  const demo = optionalDemoFlag(input);
173
191
  const demoPages = optionalStringArray(input, "demoPages");
174
192
  const demoCardLabels = optionalStringArray(input, "demoCardLabels");
193
+ const features = normalizeFeatures(input.features);
175
194
  const normalized = cloneAndFreeze({
176
195
  projectId: requireString(input, "projectId"),
177
196
  host: requireEnum(input, "host", RUNTIME_HOSTS),
178
197
  framework: requireEnum(input, "framework", RUNTIME_FRAMEWORKS),
179
198
  stylingSystem: optionalString(input, "stylingSystem"),
180
199
  capabilities: normalizeCapabilities(input.capabilities),
200
+ ...(features === undefined ? {} : { features }),
181
201
  tokenCatalog: optionalArray(input, "tokenCatalog"),
182
202
  tokens: optionalArray(input, "tokens"),
183
203
  tokenDiagnostics: optionalArray(input, "tokenDiagnostics"),
@@ -217,6 +237,10 @@ export function configureNudgeUiRuntime(config) {
217
237
  export function getNudgeUiRuntimeConfig() {
218
238
  return activeRuntimeConfig;
219
239
  }
240
+ /** Returns whether a product feature is enabled; omitted flags default to enabled. */
241
+ export function isNudgeUiFeatureEnabled(features, feature) {
242
+ return features?.[feature] !== false;
243
+ }
220
244
  /**
221
245
  * Whether this document runs the explicit public landing demo (ADR-0014 and
222
246
  * ADR-0016). The plugin emits `demo: true` for the flagged demo route and for