@multiplatform.one/node-graph 7.34.4 → 7.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/css/graph.css +0 -2
  2. package/dist/cjs/diagnostics/GraphDiagnostics.cjs +1 -1
  3. package/dist/cjs/diagnostics/GraphDiagnostics.native.js +1 -1
  4. package/dist/cjs/diagnostics/GraphDiagnostics.native.js.map +1 -1
  5. package/dist/cjs/index.cjs +1 -1
  6. package/dist/cjs/index.native.js +1 -1
  7. package/dist/cjs/index.native.js.map +1 -1
  8. package/dist/cjs/knobs/KnobEnvelopeProvider.cjs +0 -1
  9. package/dist/cjs/knobs/KnobEnvelopeProvider.native.js +0 -1
  10. package/dist/cjs/knobs/KnobEnvelopeProvider.native.js.map +1 -1
  11. package/dist/cjs/knobs/envelope.cjs +1 -1
  12. package/dist/cjs/knobs/envelope.native.js +1 -1
  13. package/dist/cjs/knobs/envelope.native.js.map +1 -1
  14. package/dist/cjs/knobs/useKnobEnvelope.cjs +1 -2
  15. package/dist/cjs/knobs/useKnobEnvelope.native.js +6 -7
  16. package/dist/cjs/knobs/useKnobEnvelope.native.js.map +1 -1
  17. package/dist/cjs/nodes/GraphNode.cjs +3 -2
  18. package/dist/cjs/nodes/GraphNode.native.js +3 -2
  19. package/dist/cjs/nodes/GraphNode.native.js.map +1 -1
  20. package/dist/cjs/nodes/GraphPort.cjs +2 -1
  21. package/dist/cjs/nodes/GraphPort.native.js +2 -1
  22. package/dist/cjs/nodes/GraphPort.native.js.map +1 -1
  23. package/dist/cjs/nodes/nodeMeasure.cjs +12 -19
  24. package/dist/cjs/nodes/nodeMeasure.native.js +15 -19
  25. package/dist/cjs/nodes/nodeMeasure.native.js.map +1 -1
  26. package/dist/cjs/nodes/overviewNode.cjs +1 -1
  27. package/dist/cjs/nodes/overviewNode.native.js +1 -1
  28. package/dist/cjs/nodes/overviewNode.native.js.map +1 -1
  29. package/dist/cjs/react-flow/ConnectionPuck.cjs +1 -1
  30. package/dist/cjs/react-flow/ConnectionPuck.native.js +1 -1
  31. package/dist/cjs/react-flow/ConnectionPuck.native.js.map +1 -1
  32. package/dist/cjs/react-flow/NodeGraphWeb.cjs +2 -1
  33. package/dist/cjs/react-flow/NodeGraphWeb.native.js +2 -1
  34. package/dist/cjs/react-flow/NodeGraphWeb.native.js.map +1 -1
  35. package/dist/cjs/theme/graphVars.cjs +13 -16
  36. package/dist/cjs/theme/graphVars.native.js +18 -19
  37. package/dist/cjs/theme/graphVars.native.js.map +1 -1
  38. package/dist/esm/diagnostics/GraphDiagnostics.mjs +1 -1
  39. package/dist/esm/diagnostics/GraphDiagnostics.mjs.map +1 -1
  40. package/dist/esm/diagnostics/GraphDiagnostics.native.js +1 -1
  41. package/dist/esm/diagnostics/GraphDiagnostics.native.js.map +1 -1
  42. package/dist/esm/index.mjs +2 -2
  43. package/dist/esm/index.mjs.map +1 -1
  44. package/dist/esm/index.native.js +2 -2
  45. package/dist/esm/index.native.js.map +1 -1
  46. package/dist/esm/knobs/KnobEnvelopeProvider.mjs +0 -1
  47. package/dist/esm/knobs/KnobEnvelopeProvider.mjs.map +1 -1
  48. package/dist/esm/knobs/KnobEnvelopeProvider.native.js +0 -1
  49. package/dist/esm/knobs/KnobEnvelopeProvider.native.js.map +1 -1
  50. package/dist/esm/knobs/envelope.mjs +1 -1
  51. package/dist/esm/knobs/envelope.mjs.map +1 -1
  52. package/dist/esm/knobs/envelope.native.js +1 -1
  53. package/dist/esm/knobs/envelope.native.js.map +1 -1
  54. package/dist/esm/knobs/useKnobEnvelope.mjs +1 -2
  55. package/dist/esm/knobs/useKnobEnvelope.mjs.map +1 -1
  56. package/dist/esm/knobs/useKnobEnvelope.native.js +6 -7
  57. package/dist/esm/knobs/useKnobEnvelope.native.js.map +1 -1
  58. package/dist/esm/nodes/GraphNode.mjs +4 -3
  59. package/dist/esm/nodes/GraphNode.mjs.map +1 -1
  60. package/dist/esm/nodes/GraphNode.native.js +4 -3
  61. package/dist/esm/nodes/GraphNode.native.js.map +1 -1
  62. package/dist/esm/nodes/GraphPort.mjs +2 -1
  63. package/dist/esm/nodes/GraphPort.mjs.map +1 -1
  64. package/dist/esm/nodes/GraphPort.native.js +2 -1
  65. package/dist/esm/nodes/GraphPort.native.js.map +1 -1
  66. package/dist/esm/nodes/nodeMeasure.mjs +13 -20
  67. package/dist/esm/nodes/nodeMeasure.mjs.map +1 -1
  68. package/dist/esm/nodes/nodeMeasure.native.js +16 -20
  69. package/dist/esm/nodes/nodeMeasure.native.js.map +1 -1
  70. package/dist/esm/nodes/overviewNode.mjs +2 -2
  71. package/dist/esm/nodes/overviewNode.mjs.map +1 -1
  72. package/dist/esm/nodes/overviewNode.native.js +2 -2
  73. package/dist/esm/nodes/overviewNode.native.js.map +1 -1
  74. package/dist/esm/react-flow/ConnectionPuck.mjs +2 -2
  75. package/dist/esm/react-flow/ConnectionPuck.mjs.map +1 -1
  76. package/dist/esm/react-flow/ConnectionPuck.native.js +2 -2
  77. package/dist/esm/react-flow/ConnectionPuck.native.js.map +1 -1
  78. package/dist/esm/react-flow/NodeGraphWeb.mjs +2 -1
  79. package/dist/esm/react-flow/NodeGraphWeb.mjs.map +1 -1
  80. package/dist/esm/react-flow/NodeGraphWeb.native.js +2 -1
  81. package/dist/esm/react-flow/NodeGraphWeb.native.js.map +1 -1
  82. package/dist/esm/theme/graphVars.mjs +14 -17
  83. package/dist/esm/theme/graphVars.mjs.map +1 -1
  84. package/dist/esm/theme/graphVars.native.js +19 -20
  85. package/dist/esm/theme/graphVars.native.js.map +1 -1
  86. package/dist/jsx/diagnostics/GraphDiagnostics.mjs +1 -1
  87. package/dist/jsx/diagnostics/GraphDiagnostics.mjs.map +1 -1
  88. package/dist/jsx/diagnostics/GraphDiagnostics.native.js +1 -1
  89. package/dist/jsx/diagnostics/GraphDiagnostics.native.js.map +1 -1
  90. package/dist/jsx/index.js +2 -2
  91. package/dist/jsx/index.js.map +1 -1
  92. package/dist/jsx/index.mjs +2 -2
  93. package/dist/jsx/index.mjs.map +1 -1
  94. package/dist/jsx/index.native.js +1 -1
  95. package/dist/jsx/index.native.js.map +1 -1
  96. package/dist/jsx/knobs/KnobEnvelopeProvider.mjs +0 -1
  97. package/dist/jsx/knobs/KnobEnvelopeProvider.mjs.map +1 -1
  98. package/dist/jsx/knobs/KnobEnvelopeProvider.native.js +0 -1
  99. package/dist/jsx/knobs/KnobEnvelopeProvider.native.js.map +1 -1
  100. package/dist/jsx/knobs/envelope.mjs +1 -1
  101. package/dist/jsx/knobs/envelope.mjs.map +1 -1
  102. package/dist/jsx/knobs/envelope.native.js +1 -1
  103. package/dist/jsx/knobs/envelope.native.js.map +1 -1
  104. package/dist/jsx/knobs/useKnobEnvelope.mjs +1 -2
  105. package/dist/jsx/knobs/useKnobEnvelope.mjs.map +1 -1
  106. package/dist/jsx/knobs/useKnobEnvelope.native.js +6 -7
  107. package/dist/jsx/knobs/useKnobEnvelope.native.js.map +1 -1
  108. package/dist/jsx/nodes/GraphNode.mjs +4 -3
  109. package/dist/jsx/nodes/GraphNode.mjs.map +1 -1
  110. package/dist/jsx/nodes/GraphNode.native.js +3 -2
  111. package/dist/jsx/nodes/GraphNode.native.js.map +1 -1
  112. package/dist/jsx/nodes/GraphPort.mjs +2 -1
  113. package/dist/jsx/nodes/GraphPort.mjs.map +1 -1
  114. package/dist/jsx/nodes/GraphPort.native.js +2 -1
  115. package/dist/jsx/nodes/GraphPort.native.js.map +1 -1
  116. package/dist/jsx/nodes/nodeMeasure.mjs +13 -20
  117. package/dist/jsx/nodes/nodeMeasure.mjs.map +1 -1
  118. package/dist/jsx/nodes/nodeMeasure.native.js +15 -19
  119. package/dist/jsx/nodes/nodeMeasure.native.js.map +1 -1
  120. package/dist/jsx/nodes/overviewNode.mjs +2 -2
  121. package/dist/jsx/nodes/overviewNode.mjs.map +1 -1
  122. package/dist/jsx/nodes/overviewNode.native.js +1 -1
  123. package/dist/jsx/nodes/overviewNode.native.js.map +1 -1
  124. package/dist/jsx/react-flow/ConnectionPuck.mjs +2 -2
  125. package/dist/jsx/react-flow/ConnectionPuck.mjs.map +1 -1
  126. package/dist/jsx/react-flow/ConnectionPuck.native.js +1 -1
  127. package/dist/jsx/react-flow/ConnectionPuck.native.js.map +1 -1
  128. package/dist/jsx/react-flow/NodeGraphWeb.mjs +2 -1
  129. package/dist/jsx/react-flow/NodeGraphWeb.mjs.map +1 -1
  130. package/dist/jsx/react-flow/NodeGraphWeb.native.js +2 -1
  131. package/dist/jsx/react-flow/NodeGraphWeb.native.js.map +1 -1
  132. package/dist/jsx/theme/graphVars.mjs +14 -17
  133. package/dist/jsx/theme/graphVars.mjs.map +1 -1
  134. package/dist/jsx/theme/graphVars.native.js +18 -19
  135. package/dist/jsx/theme/graphVars.native.js.map +1 -1
  136. package/package.json +7 -7
  137. package/src/diagnostics/GraphDiagnostics.tsx +1 -1
  138. package/src/dom/GraphDomLayout.tsx +1 -1
  139. package/src/index.ts +1 -2
  140. package/src/knobs/KnobEnvelopeProvider.tsx +3 -20
  141. package/src/knobs/envelope.spec.tsx +18 -32
  142. package/src/knobs/envelope.ts +8 -14
  143. package/src/knobs/useKnobEnvelope.ts +1 -1
  144. package/src/nodes/GraphNode.spec.tsx +34 -43
  145. package/src/nodes/GraphNode.stories.tsx +2 -8
  146. package/src/nodes/GraphNode.tsx +5 -3
  147. package/src/nodes/GraphPort.spec.tsx +4 -6
  148. package/src/nodes/GraphPort.stories.tsx +6 -3
  149. package/src/nodes/GraphPort.tsx +3 -2
  150. package/src/nodes/Overview.stories.tsx +2 -2
  151. package/src/nodes/handleGeometry.ts +1 -1
  152. package/src/nodes/lod.spec.tsx +3 -2
  153. package/src/nodes/nodeMeasure.spec.ts +52 -43
  154. package/src/nodes/nodeMeasure.ts +32 -51
  155. package/src/nodes/overviewNode.tsx +3 -3
  156. package/src/react-flow/ConnectionPuck.spec.tsx +4 -4
  157. package/src/react-flow/ConnectionPuck.tsx +2 -2
  158. package/src/react-flow/NodeGraphWeb.spec.tsx +2 -6
  159. package/src/react-flow/NodeGraphWeb.stories.tsx +3 -9
  160. package/src/react-flow/NodeGraphWeb.tsx +2 -2
  161. package/src/theme/graphVars.spec.ts +65 -33
  162. package/src/theme/graphVars.ts +22 -27
  163. package/types/dom/GraphDomLayout.d.ts +1 -1
  164. package/types/index.d.ts +2 -2
  165. package/types/index.d.ts.map +1 -1
  166. package/types/knobs/KnobEnvelopeProvider.d.ts +1 -1
  167. package/types/knobs/KnobEnvelopeProvider.d.ts.map +1 -1
  168. package/types/knobs/envelope.d.ts +7 -14
  169. package/types/knobs/envelope.d.ts.map +1 -1
  170. package/types/nodes/GraphNode.d.ts.map +1 -1
  171. package/types/nodes/GraphPort.d.ts.map +1 -1
  172. package/types/nodes/handleGeometry.d.ts +1 -1
  173. package/types/nodes/nodeMeasure.d.ts +10 -28
  174. package/types/nodes/nodeMeasure.d.ts.map +1 -1
  175. package/types/nodes/overviewNode.d.ts.map +1 -1
  176. package/types/theme/graphVars.d.ts +1 -10
  177. package/types/theme/graphVars.d.ts.map +1 -1
@@ -25,8 +25,7 @@ import {
25
25
  type Knobs,
26
26
  type ResolvedKnobs,
27
27
  Surface,
28
- type SurfaceDensity,
29
- type SurfaceSizeIntent,
28
+ type SurfaceSize,
30
29
  defaultKnobs,
31
30
  registerPreset,
32
31
  useResolvedKnobs,
@@ -95,34 +94,25 @@ const knobVocabulary = {
95
94
  cornerSmoothing: ["round", "smooth"],
96
95
  borderWidth: ["none", "small", "medium", "large"],
97
96
  elevation: ["none", "small", "medium", "large"],
98
- space: ["small", "medium", "large"],
99
- size: ["small", "medium", "large"],
100
- density: ["comfortable", "compact"],
101
97
  textAccent: ["low", "medium", "high"],
102
98
  headingFont: ["sans-serif", "serif", "mono", "slab", "rounded"],
103
99
  bodyFont: ["sans-serif", "serif", "mono", "condensed"],
104
100
  fontWeight: ["regular", "bold"],
105
101
  pageTitleScale: ["moderate", "display"],
106
- animation: ["none", "quick", "bouncy", "lazy", "snappy"],
102
+ animation: ["none", "quick", "bouncy", "lazy", "quickest"],
107
103
  } as const;
108
104
 
109
- const surfaceSizes: readonly (SurfaceSizeIntent | undefined)[] = [
105
+ const surfaceSizes: readonly (Exclude<SurfaceSize, "unset"> | undefined)[] = [
110
106
  undefined,
111
- "small",
112
- "medium",
113
- "large",
114
- ];
115
- const surfaceDensities: readonly (SurfaceDensity | undefined)[] = [
116
- undefined,
117
- "compact",
118
- "comfortable",
107
+ "$3",
108
+ "$true",
109
+ "$5",
119
110
  ];
120
111
  const intents: readonly (string | null)[] = [null, "accent", "error", "warning", "success"];
121
112
 
122
113
  interface Combo {
123
114
  readonly knobs: Partial<Knobs>;
124
- readonly surfaceSize: SurfaceSizeIntent | undefined;
125
- readonly surfaceDensity: SurfaceDensity | undefined;
115
+ readonly surfaceSize: Exclude<SurfaceSize, "unset"> | undefined;
126
116
  readonly intent: string | null;
127
117
  }
128
118
 
@@ -141,7 +131,6 @@ function seededCombos(label: string, count: number): Combo[] {
141
131
  combos.push({
142
132
  knobs: knobs as Partial<Knobs>,
143
133
  surfaceSize: pick(rand, surfaceSizes),
144
- surfaceDensity: pick(rand, surfaceDensities),
145
134
  intent: pick(rand, intents),
146
135
  });
147
136
  }
@@ -179,8 +168,7 @@ interface CrossOptions {
179
168
  readonly presetName?: string;
180
169
  /** Passed to `useKnobEnvelope`; omit to prove the fallback to `defaultIntents`. */
181
170
  readonly carryPresetName?: boolean;
182
- readonly surfaceSize?: SurfaceSizeIntent;
183
- readonly surfaceDensity?: SurfaceDensity;
171
+ readonly surfaceSize?: Exclude<SurfaceSize, "unset">;
184
172
  readonly contextIntent?: string | null;
185
173
  /** Explicit `useResolvedKnobs({ intent })` on both sides; omit to lean on context. */
186
174
  readonly resolveIntent?: string;
@@ -196,7 +184,7 @@ function cross(options: CrossOptions): Crossing {
196
184
  const hostCapture: Capture = {};
197
185
  const tree: ReactNode = (
198
186
  <Preset preset={options.presetName} overrides={options.knobs}>
199
- <Surface size={options.surfaceSize} density={options.surfaceDensity}>
187
+ <Surface size={options.surfaceSize}>
200
188
  <IntentContext.Provider value={options.contextIntent ?? null}>
201
189
  <HostProbe
202
190
  capture={hostCapture}
@@ -234,9 +222,8 @@ function cross(options: CrossOptions): Crossing {
234
222
  describe("the knob envelope crosses the stack (MPO-191 AC-3)", () => {
235
223
  it("resolves identically for the design's own host tree", () => {
236
224
  const { host, rehydrated } = cross({
237
- knobs: { borderRadius: "large", elevation: "medium", space: "large", animation: "bouncy" },
238
- surfaceSize: "small",
239
- surfaceDensity: "compact",
225
+ knobs: { borderRadius: "large", elevation: "medium", animation: "bouncy" },
226
+ surfaceSize: "$3",
240
227
  contextIntent: "error",
241
228
  resolveIntent: "error",
242
229
  });
@@ -251,7 +238,6 @@ describe("the knob envelope crosses the stack (MPO-191 AC-3)", () => {
251
238
  const { host, rehydrated } = cross({
252
239
  knobs: combo.knobs,
253
240
  surfaceSize: combo.surfaceSize,
254
- surfaceDensity: combo.surfaceDensity,
255
241
  contextIntent: combo.intent,
256
242
  resolveIntent: combo.intent ?? undefined,
257
243
  });
@@ -262,7 +248,7 @@ describe("the knob envelope crosses the stack (MPO-191 AC-3)", () => {
262
248
  it("resolves identically when the intent comes only from context", () => {
263
249
  const { host, rehydrated } = cross({
264
250
  knobs: { borderRadius: "full", textAccent: "low" },
265
- surfaceSize: "medium",
251
+ surfaceSize: "$true",
266
252
  contextIntent: "warning",
267
253
  });
268
254
  expect(rehydrated).toEqual(host);
@@ -271,7 +257,7 @@ describe("the knob envelope crosses the stack (MPO-191 AC-3)", () => {
271
257
  it("carries a NAMED preset's own intents across, which is what presetName is for", () => {
272
258
  const { host, rehydrated, envelope } = cross({
273
259
  presetName: "graphFuzz",
274
- surfaceSize: "large",
260
+ surfaceSize: "$5",
275
261
  contextIntent: "error",
276
262
  resolveIntent: "error",
277
263
  });
@@ -283,7 +269,7 @@ describe("the knob envelope crosses the stack (MPO-191 AC-3)", () => {
283
269
  // these two would be identical and the test above would prove nothing.
284
270
  const control = cross({
285
271
  presetName: "graphFuzz",
286
- surfaceSize: "large",
272
+ surfaceSize: "$5",
287
273
  contextIntent: "warning",
288
274
  resolveIntent: "warning",
289
275
  });
@@ -295,7 +281,7 @@ describe("the knob envelope crosses the stack (MPO-191 AC-3)", () => {
295
281
  const { host, rehydrated } = cross({
296
282
  presetName: "graphFuzz",
297
283
  carryPresetName: false,
298
- surfaceSize: "large",
284
+ surfaceSize: "$5",
299
285
  contextIntent: "error",
300
286
  resolveIntent: "error",
301
287
  });
@@ -306,14 +292,14 @@ describe("the knob envelope crosses the stack (MPO-191 AC-3)", () => {
306
292
  });
307
293
 
308
294
  describe("the envelope itself", () => {
309
- it("keeps `unset` surface axes instead of coercing them", () => {
295
+ it("keeps an `unset` surface size instead of coercing it", () => {
310
296
  const capture: Capture = {};
311
297
  const render = renderWithProviders(
312
- <Preset overrides={{ size: "large" }}>
298
+ <Preset overrides={{ borderRadius: "large" }}>
313
299
  <HostProbe capture={capture} />
314
300
  </Preset>,
315
301
  );
316
- expect(capture.envelope?.surface).toEqual({ size: "unset", density: "unset" });
302
+ expect(capture.envelope?.surface).toEqual({ size: "unset" });
317
303
  render.unmount();
318
304
  });
319
305
 
@@ -11,22 +11,16 @@
11
11
  * Design: docs/design/node-graph-editor.md section 6.1.
12
12
  */
13
13
 
14
- import type { Animation, Knobs, SurfaceDensity, SurfaceSize } from "@multiplatform.one/theme";
14
+ import type { Animation, Knobs, SurfaceSize } from "@multiplatform.one/theme";
15
15
 
16
16
  /**
17
17
  * `unset` is part of the surface vocabulary, and dropping it changes what
18
- * resolves.
19
- *
20
- * Design 6.1 types the envelope's surface as `{ size: SurfaceSize; density:
21
- * SurfaceDensity }`, but `SurfaceContext`'s default is `{ size: "unset",
22
- * density: "unset" }` (Surface.tsx:23-26) and `applySurfaceToKnobs`
23
- * (useResolvedKnobs.ts:44-70) SKIPS an `unset` axis entirely. Coercing a host
24
- * that sits under no `<Surface>` to "medium" would then step `knobs.size` down
25
- * from `large`, and the rehydrated tree would resolve differently from the host
26
- * it copied. So the envelope carries the third value.
18
+ * resolves: `SurfaceContext`'s default is `{ size: "unset" }` and
19
+ * `useResolvedKnobs` treats an `unset` ceiling as none. Coercing a host that
20
+ * sits under no `<Surface>` to a token would clamp the rehydrated tree where
21
+ * the host it copied was not clamped. So the envelope carries the value as is.
27
22
  */
28
- export type GraphSurfaceSize = SurfaceSize | "unset";
29
- export type GraphSurfaceDensity = SurfaceDensity | "unset";
23
+ export type GraphSurfaceSize = SurfaceSize;
30
24
 
31
25
  /** A face the DOM side has to register itself, because WKWebView cannot see an expo-font registration. */
32
26
  export interface GraphFontRef {
@@ -38,7 +32,7 @@ export interface GraphFontRef {
38
32
  }
39
33
 
40
34
  /** Bumped when the envelope's shape changes in a way the DOM side must refuse. */
41
- export const knobEnvelopeSchemaVersion = 1;
35
+ export const knobEnvelopeSchemaVersion = 2;
42
36
 
43
37
  export interface KnobEnvelope {
44
38
  readonly schemaVersion: typeof knobEnvelopeSchemaVersion;
@@ -58,7 +52,7 @@ export interface KnobEnvelope {
58
52
  */
59
53
  readonly presetName?: string;
60
54
  readonly intent: string | null;
61
- readonly surface: { readonly size: GraphSurfaceSize; readonly density: GraphSurfaceDensity };
55
+ readonly surface: { readonly size: GraphSurfaceSize };
62
56
  readonly scheme: "light" | "dark";
63
57
  readonly themeName: string;
64
58
  /**
@@ -125,7 +125,7 @@ export function useKnobEnvelope(options?: UseKnobEnvelopeOptions): KnobEnvelope
125
125
  knobs: merged,
126
126
  presetName: options?.presetName,
127
127
  intent: options?.intent !== undefined ? options.intent : contextIntent,
128
- surface: { size: surfaceCtx?.size ?? "unset", density: surfaceCtx?.density ?? "unset" },
128
+ surface: { size: surfaceCtx?.size ?? "unset" },
129
129
  scheme: themeName.startsWith("dark") ? "dark" : "light",
130
130
  themeName,
131
131
  prefersReducedMotion,
@@ -15,7 +15,13 @@
15
15
  * is what gets mocked, which is the point of having one import site.
16
16
  */
17
17
 
18
- import { Preset, resolveRadiusForPart, Surface, useResolvedKnobs } from "@multiplatform.one/theme";
18
+ import {
19
+ panelPaddingPx,
20
+ Preset,
21
+ resolveRadiusForPart,
22
+ Surface,
23
+ useResolvedKnobs,
24
+ } from "@multiplatform.one/theme";
19
25
  import { renderWithProviders } from "@multiplatform.one/test-utils";
20
26
  import { cleanup } from "@testing-library/react";
21
27
  import { readFileSync } from "node:fs";
@@ -25,7 +31,7 @@ import type { ReactElement } from "react";
25
31
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
26
32
  import { declarationFor, hexLiteralsIn, paintDeclarationCount } from "../../tests/styles";
27
33
  import type { Knobs } from "@multiplatform.one/theme";
28
- import { containerRadiusPx, type GraphNodePort, nodePanelPaddingPx } from "./nodeMeasure";
34
+ import { containerRadiusPx, type GraphNodePort } from "./nodeMeasure";
29
35
 
30
36
  const updateNodeInternals = vi.fn();
31
37
 
@@ -43,14 +49,11 @@ const radiusStops = [
43
49
  "large",
44
50
  "full",
45
51
  ] as const satisfies readonly Knobs["borderRadius"][];
46
- const spaces = ["small", "medium", "large"] as const satisfies readonly Knobs["space"][];
47
52
 
48
53
  function renderNode(ui: ReactElement, overrides?: Partial<Knobs>) {
49
54
  return renderWithProviders(
50
55
  <Preset cascade={false} overrides={overrides}>
51
- <Surface size="medium" density="comfortable">
52
- {ui}
53
- </Surface>
56
+ <Surface size="$true">{ui}</Surface>
54
57
  </Preset>,
55
58
  );
56
59
  }
@@ -71,41 +74,30 @@ afterEach(cleanup);
71
74
 
72
75
  describe("radius: CONTAINER-CAP at all five stops, measured on the rendered frame", () => {
73
76
  it.each(radiusStops)("resolves %s through the doctrine table", (borderRadius) => {
74
- for (const space of spaces) {
75
- const { container } = renderNode(<GraphNode id="n1" title="Transform" />, {
76
- borderRadius,
77
- space,
78
- });
79
- const frame = frameOf(container);
80
- const expected = resolveRadiusForPart({
81
- radiusClass: "CONTAINER-CAP",
82
- stop: borderRadius,
83
- paddingPx: nodePanelPaddingPx[space],
84
- }).px;
85
- expect(frame.getAttribute("data-graph-node-radius")).toBe(String(expected));
86
- cleanup();
87
- }
77
+ const { container } = renderNode(<GraphNode id="n1" title="Transform" />, { borderRadius });
78
+ const frame = frameOf(container);
79
+ const expected = resolveRadiusForPart({
80
+ radiusClass: "CONTAINER-CAP",
81
+ stop: borderRadius,
82
+ paddingPx: panelPaddingPx(),
83
+ }).px;
84
+ expect(frame.getAttribute("data-graph-node-radius")).toBe(String(expected));
88
85
  });
89
86
 
90
87
  it("paints that same radius — the emitted declaration, not just the attribute", () => {
91
88
  for (const borderRadius of radiusStops) {
92
- for (const space of spaces) {
93
- const { container } = renderNode(<GraphNode id="n1" title="Transform" />, {
94
- borderRadius,
95
- space,
96
- });
97
- const frame = frameOf(container);
98
- const painted = declarationFor(frame, "border-top-left-radius");
99
- expect(painted, `${borderRadius}/${space} paints no radius at all`).toBeDefined();
100
- const reported = Number(frame.getAttribute("data-graph-node-radius"));
101
- // happy-dom does not resolve custom properties, so a token-valued
102
- // declaration reads back as `var(--t-radius-N)`. Decode it the same
103
- // way the component does and require the two to agree.
104
- const token = /^var\(--(?:t-)?radius-(\d+)\)$/.exec(painted!);
105
- const paintedPx = token ? containerRadiusPx(`$${token[1]}`) : Number.parseFloat(painted!);
106
- expect(paintedPx, `${borderRadius}/${space} painted ${painted}`).toBe(reported);
107
- cleanup();
108
- }
89
+ const { container } = renderNode(<GraphNode id="n1" title="Transform" />, { borderRadius });
90
+ const frame = frameOf(container);
91
+ const painted = declarationFor(frame, "border-top-left-radius");
92
+ expect(painted, `${borderRadius} paints no radius at all`).toBeDefined();
93
+ const reported = Number(frame.getAttribute("data-graph-node-radius"));
94
+ // happy-dom does not resolve custom properties, so a token-valued
95
+ // declaration reads back as `var(--t-radius-N)`. Decode it the same
96
+ // way the component does and require the two to agree.
97
+ const token = /^var\(--(?:t-)?radius-(\d+)\)$/.exec(painted!);
98
+ const paintedPx = token ? containerRadiusPx(`$${token[1]}`) : Number.parseFloat(painted!);
99
+ expect(paintedPx, `${borderRadius} painted ${painted}`).toBe(reported);
100
+ cleanup();
109
101
  }
110
102
  });
111
103
  });
@@ -181,7 +173,7 @@ describe("useUpdateNodeInternals fires on a port change and never on a knob flip
181
173
  ];
182
174
  rerender(
183
175
  <Preset cascade={false}>
184
- <Surface size="medium" density="comfortable">
176
+ <Surface size="$true">
185
177
  <GraphNode id="n1" title="Transform" ports={grown} />
186
178
  </Surface>
187
179
  </Preset>,
@@ -193,20 +185,19 @@ describe("useUpdateNodeInternals fires on a port change and never on a knob flip
193
185
  it("does NOT fire on a pure knob flip that keeps the same ports array", () => {
194
186
  const { rerender } = renderNode(<GraphNode id="n1" title="Transform" ports={twoPorts} />, {
195
187
  borderRadius: "none",
196
- space: "small",
197
188
  });
198
189
  expect(updateNodeInternals).toHaveBeenCalledTimes(0);
199
190
 
200
191
  rerender(
201
- <Preset cascade={false} overrides={{ borderRadius: "full", space: "large", size: "large" }}>
202
- <Surface size="medium" density="comfortable">
192
+ <Preset cascade={false} overrides={{ borderRadius: "full" }}>
193
+ <Surface size="$true">
203
194
  <GraphNode id="n1" title="Transform" ports={twoPorts} />
204
195
  </Surface>
205
196
  </Preset>,
206
197
  );
207
198
  // The flip must actually have restyled the node, or this proves nothing.
208
199
  expect(frameOf(document.body as HTMLElement).getAttribute("data-graph-node-radius")).toBe(
209
- String(nodePanelPaddingPx.large),
200
+ String(panelPaddingPx()),
210
201
  );
211
202
  expect(updateNodeInternals).toHaveBeenCalledTimes(0);
212
203
  });
@@ -223,7 +214,7 @@ function renderResolved(overrides: Partial<Knobs>): {
223
214
  }
224
215
  renderWithProviders(
225
216
  <Preset cascade={false} overrides={overrides}>
226
- <Surface size="medium" density="comfortable">
217
+ <Surface size="$true">
227
218
  <Probe />
228
219
  </Surface>
229
220
  </Preset>,
@@ -171,13 +171,11 @@ const fuzzCombos = [
171
171
  borderRadius: "medium",
172
172
  borderWidth: "none",
173
173
  elevation: "none",
174
- space: "large",
175
- size: "medium",
176
174
  textAccent: "medium",
177
175
  headingFont: "geometric",
178
176
  bodyFont: "sans-serif",
179
177
  fontWeight: "bold",
180
- animation: "tooltip",
178
+ animation: "slow",
181
179
  },
182
180
  },
183
181
  {
@@ -187,8 +185,6 @@ const fuzzCombos = [
187
185
  borderRadius: "none",
188
186
  borderWidth: "none",
189
187
  elevation: "small",
190
- space: "small",
191
- size: "small",
192
188
  textAccent: "medium",
193
189
  headingFont: "rounded",
194
190
  bodyFont: "cursive",
@@ -203,8 +199,6 @@ const fuzzCombos = [
203
199
  borderRadius: "full",
204
200
  borderWidth: "small",
205
201
  elevation: "large",
206
- space: "medium",
207
- size: "large",
208
202
  textAccent: "high",
209
203
  headingFont: "mono",
210
204
  bodyFont: "serif",
@@ -242,7 +236,7 @@ export const KnobFuzz: Story = {
242
236
  <FuzzColumn key={scheme} scheme={scheme}>
243
237
  {fuzzCombos.map((combo) => (
244
238
  <Preset key={combo.label} cascade={false} overrides={combo.knobs}>
245
- <Surface size="medium" density="comfortable">
239
+ <Surface size="$4">
246
240
  <YStack gap="$2">
247
241
  <SizableText size="$1" opacity={0.6}>
248
242
  {combo.label}
@@ -18,6 +18,8 @@
18
18
  * would stack a second forced-layout storm on the observer's.
19
19
  */
20
20
 
21
+ import { transitionProps } from "@multiplatform.one/theme";
22
+
21
23
  import { type ReactNode, useEffect, useRef } from "react";
22
24
  import { useGraphNodeKnobs } from "../knobs/graphKnobs";
23
25
  import { SizableText, XStack, YStack } from "tamagui";
@@ -29,10 +31,10 @@ import {
29
31
  containerRadiusPx,
30
32
  type GraphNodeMeasure,
31
33
  type GraphNodePort,
32
- type GraphNodeSizeKnob,
33
34
  graphNodeMinHeight,
34
35
  graphNodeWidth,
35
36
  portSpacing,
37
+ graphNodeSize,
36
38
  } from "./nodeMeasure";
37
39
 
38
40
  export interface GraphNodeProps {
@@ -100,7 +102,7 @@ export function GraphNode({
100
102
 
101
103
  usePortInternals(id, ports);
102
104
 
103
- const size = knobProps.size as GraphNodeSizeKnob;
105
+ const size = graphNodeSize(knobProps.sizeToken);
104
106
  const portSpacingPx = portSpacing({ coarsePointer });
105
107
  // The dot straddles the row's edge, so half of it lands inside the row. The
106
108
  // label clears the whole diameter rather than the half it strictly needs, so
@@ -143,7 +145,7 @@ export function GraphNode({
143
145
  // §7.1 H6. Handles overhang the frame; a clipping node eats them.
144
146
  overflow="visible"
145
147
  opacity={dragging ? 0.85 : 1}
146
- transition={knobProps.transition}
148
+ {...transitionProps(knobProps.motion.control, ["borderRadius", "borderWidth"])}
147
149
  >
148
150
  <XStack {...knobProps.gap} alignItems="center" justifyContent="space-between">
149
151
  {/*
@@ -46,6 +46,7 @@ const radiusStops = [
46
46
  "full",
47
47
  ] as const satisfies readonly Knobs["borderRadius"][];
48
48
  const sizes = ["small", "medium", "large"] as const satisfies readonly GraphNodeSizeKnob[];
49
+ const sizeTokenFor = { small: "$3", medium: "$true", large: "$5" } as const;
49
50
 
50
51
  const graphCss = readFileSync(
51
52
  join(dirname(fileURLToPath(import.meta.url)), "../../css/graph.css"),
@@ -56,11 +57,8 @@ const afterRule = graphCss.match(/\.mp-graph-port::after\s*\{([^}]*)\}/);
56
57
  const discRule = graphCss.match(/\.mp-graph-port\s*\{([^}]*)\}/);
57
58
 
58
59
  /**
59
- * `Surface` is the size AUTHORITY, not the size knob: `applySurfaceToKnobs`
60
- * replaces `knobs.size` outright when no explicit `size` option is passed to
61
- * `useResolvedKnobs` (LC-68). So the size stops are driven through the Surface
62
- * here — setting `overrides={{ size }}` under a fixed Surface is a no-op, and a
63
- * spec that did it would be testing nothing while looking like it tested a lot.
60
+ * `Surface` is the size authority: the size token reaches the port only
61
+ * through the Surface ceiling, so each size row is driven through it here.
64
62
  */
65
63
  function renderPort(
66
64
  props: { side: GraphPortSide; coarsePointer?: boolean; portSpacingPx?: number },
@@ -69,7 +67,7 @@ function renderPort(
69
67
  ) {
70
68
  return renderWithProviders(
71
69
  <Preset cascade={false} overrides={overrides}>
72
- <Surface size={surfaceSize} density="comfortable">
70
+ <Surface size={sizeTokenFor[surfaceSize]}>
73
71
  <ReactFlowProvider>
74
72
  <GraphPort id="p1" direction="out" {...props} />
75
73
  </ReactFlowProvider>
@@ -40,6 +40,9 @@ import { GraphPort } from "./GraphPort";
40
40
  import { paintedPortSize, portGeometry, portHitSize } from "./handleGeometry";
41
41
  import type { GraphNodePort, GraphNodeSizeKnob, GraphPortSide } from "./nodeMeasure";
42
42
 
43
+ /** The size token whose side of `$true` reads each table row. */
44
+ const rowToken = { small: "$3", medium: "$4", large: "$5" } as const;
45
+
43
46
  const sides: GraphPortSide[] = ["left", "right", "top", "bottom"];
44
47
 
45
48
  const crowded: GraphNodePort[] = [
@@ -59,7 +62,7 @@ const meta: Meta<typeof GraphPort> = {
59
62
  docs: {
60
63
  description: {
61
64
  component:
62
- "A port is painted only — it draws a dot and declares a hit region, and decides nothing about whether a connection is legal. The painted dot is 8/10/12 by the size knob and NEVER 44 (H1); the 44pt target is a ::after pseudo-element that grows outward (H2), so the rendered edge endpoint stays where the 8px dot put it. The circle survives borderRadius:none, because a square port reads as a resize grip.",
65
+ "A port is painted only — it draws a dot and declares a hit region, and decides nothing about whether a connection is legal. The painted dot is 8/10/12 by the size token and NEVER 44 (H1); the 44pt target is a ::after pseudo-element that grows outward (H2), so the rendered edge endpoint stays where the 8px dot put it. The circle survives borderRadius:none, because a square port reads as a resize grip.",
63
66
  },
64
67
  },
65
68
  },
@@ -111,7 +114,7 @@ export const Main: Story = {
111
114
  </SizableText>
112
115
  {(["small", "medium", "large"] as const satisfies readonly GraphNodeSizeKnob[]).map(
113
116
  (size) => (
114
- <Surface key={size} size={size} density="comfortable">
117
+ <Surface key={size} size={rowToken[size]}>
115
118
  <XStack gap="$8" alignItems="center">
116
119
  <SizableText size="$2" opacity={0.6} width={70}>
117
120
  size {size}
@@ -329,7 +332,7 @@ export const HitTarget: Story = {
329
332
  <XStack gap="$9" alignItems="center">
330
333
  {(["small", "medium", "large"] as const satisfies readonly GraphNodeSizeKnob[]).map(
331
334
  (size) => (
332
- <Surface key={size} size={size} density="comfortable">
335
+ <Surface key={size} size={rowToken[size]}>
333
336
  <YStack gap="$3" alignItems="center" width={120}>
334
337
  {/*
335
338
  64 square so a 44 px region has room to paint without
@@ -21,7 +21,8 @@ import { isTouchSurface, useResolvedKnobs } from "@multiplatform.one/theme";
21
21
  import type { CSSProperties } from "react";
22
22
  import { Handle, Position } from "../react-flow/xy";
23
23
  import { paintedPortSize, portHitSize, portRegionInset } from "./handleGeometry";
24
- import type { GraphNodeSizeKnob, GraphPortSide } from "./nodeMeasure";
24
+ import type { GraphPortSide } from "./nodeMeasure";
25
+ import { graphNodeSize } from "./nodeMeasure";
25
26
 
26
27
  const positionForSide: Record<GraphPortSide, Position> = {
27
28
  left: Position.Left,
@@ -61,7 +62,7 @@ export function GraphPort({
61
62
  isConnectable = true,
62
63
  }: GraphPortProps) {
63
64
  const { knobProps } = useResolvedKnobs({ component: "GraphPort", intent });
64
- const size = knobProps.size as GraphNodeSizeKnob;
65
+ const size = graphNodeSize(knobProps.sizeToken);
65
66
 
66
67
  const painted = paintedPortSize(size);
67
68
  const hit = portHitSize({ size, coarsePointer, portSpacingPx });
@@ -167,7 +167,7 @@ export const main = {
167
167
  // between reloads, and it is why `size` is not fuzzable here.
168
168
  render: () => (
169
169
  <Preset>
170
- <Surface size="medium" density="comfortable">
170
+ <Surface size="$4">
171
171
  <YStack gap="$3" padding="$3">
172
172
  <View width="100%" height={620}>
173
173
  <NodeGraphWeb schema={schema} defaultDocument={bigDoc}>
@@ -200,7 +200,7 @@ export const crossover = {
200
200
  const [high, setHigh] = useState(false);
201
201
  return (
202
202
  <Preset>
203
- <Surface size="medium" density="comfortable">
203
+ <Surface size="$4">
204
204
  <YStack gap="$3" padding="$3">
205
205
  <Switch
206
206
  id="overview-zoom"
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * The laws, verbatim from §7.1:
7
7
  *
8
- * H1. The painted dot is 8 / 10 / 12 by the size knob and NEVER grows to 44.
8
+ * H1. The painted dot is 8 / 10 / 12 by the size token and NEVER grows to 44.
9
9
  * A 44 px painted dot is a different component.
10
10
  * H2. The hit region is a pseudo-element, never element growth, because
11
11
  * `getEdgePosition` reads the handle box's outer edge (`center` defaults
@@ -48,6 +48,7 @@ const ports: GraphNodePort[] = [
48
48
  ];
49
49
 
50
50
  const sizes = ["small", "medium", "large"] as const satisfies readonly GraphNodeSizeKnob[];
51
+ const sizeTokenFor = { small: "$3", medium: "$true", large: "$5" } as const;
51
52
 
52
53
  // ── lodForZoom ───────────────────────────────────────────────────────────────
53
54
 
@@ -141,7 +142,7 @@ function renderNode(
141
142
  ) {
142
143
  return renderWithProviders(
143
144
  <Preset cascade={false} overrides={overrides}>
144
- <Surface size={size} density="comfortable">
145
+ <Surface size={sizeTokenFor[size]}>
145
146
  <ReactFlowProvider>
146
147
  <GraphNode
147
148
  id="n1"
@@ -255,7 +256,7 @@ describe("OverviewNode measures from the same recipe the detail body does", () =
255
256
  function renderOverviewNode(size: GraphNodeSizeKnob) {
256
257
  return renderWithProviders(
257
258
  <Preset cascade={false}>
258
- <Surface size={size} density="comfortable">
259
+ <Surface size={sizeTokenFor[size]}>
259
260
  <ReactFlowProvider>
260
261
  <OverviewNode id="n1" ports={ports} />
261
262
  </ReactFlowProvider>