@multiplatform.one/node-graph 7.34.3 → 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
@@ -11,14 +11,20 @@
11
11
  import {
12
12
  capContainerRadius,
13
13
  defaultKnobs,
14
+ getActiveScaleTokens,
15
+ setActiveScaleTokens,
16
+ generateScale,
14
17
  type Knobs,
15
18
  MIN_PRESS_TARGET,
19
+ panelPaddingPx,
16
20
  resolveKnobs,
17
21
  resolveRadiusForPart,
18
22
  } from "@multiplatform.one/theme";
19
23
  import { describe, expect, it } from "vitest";
20
24
  import {
21
25
  defaultNodeMeasure,
26
+ containerRadiusPx,
27
+ nodeRadiusTokenPx,
22
28
  FINE_PORT_PITCH,
23
29
  type GraphNodePort,
24
30
  graphNodeMeasure,
@@ -26,13 +32,13 @@ import {
26
32
  graphNodeRadius,
27
33
  graphNodeRadiusPx,
28
34
  graphNodeWidth,
29
- nodePanelPaddingPx,
35
+ type GraphNodeSizeKnob,
30
36
  portSpacing,
31
37
  verticalPortCrowd,
32
38
  } from "./nodeMeasure";
33
39
 
34
- const sizes = ["small", "medium", "large"] as const satisfies readonly Knobs["size"][];
35
- const spaces = ["small", "medium", "large"] as const satisfies readonly Knobs["space"][];
40
+ const sizes = ["small", "medium", "large"] as const satisfies readonly GraphNodeSizeKnob[];
41
+ const sizeTokens = { small: "$3", medium: "$true", large: "$5" } as const;
36
42
  const radiusStops = [
37
43
  "none",
38
44
  "small",
@@ -60,7 +66,11 @@ describe("width: remapped to the per-kind table, never the control ladder", () =
60
66
 
61
67
  it("never lands on a control height — the ladder a container must not ride", () => {
62
68
  const controlHeights = new Set(
63
- sizes.map((size) => resolveKnobs({ ...defaultKnobs, size }).knobProps.control.height),
69
+ sizes.map(
70
+ (size) =>
71
+ resolveKnobs(defaultKnobs, undefined, "light", { sizeToken: sizeTokens[size] }).knobProps
72
+ .control.height,
73
+ ),
64
74
  );
65
75
  expect(controlHeights.size).toBeGreaterThan(0);
66
76
  for (const size of sizes) {
@@ -75,63 +85,44 @@ describe("width: remapped to the per-kind table, never the control ladder", () =
75
85
  });
76
86
 
77
87
  describe("radius: CONTAINER-CAP at all five stops", () => {
78
- it("mirrors the resolver's panel padding — the drift guard on this module's copy", () => {
79
- // `capContainerRadius` at `full` returns the container's own padding px,
80
- // so this equality pins our mirror to `panelPaddingPxMap` in resolveKnobs.
81
- for (const space of spaces) {
82
- expect(capContainerRadius("full", space)).toBe(nodePanelPaddingPx[space]);
83
- }
88
+ it("caps at the panelPadding role, the same px capContainerRadius uses", () => {
89
+ expect(capContainerRadius("full")).toBe(panelPaddingPx());
84
90
  });
85
91
 
86
- it("paints exactly what capContainerRadius resolves, at every stop and space", () => {
92
+ it("paints exactly what capContainerRadius resolves, at every stop", () => {
87
93
  for (const borderRadius of radiusStops) {
88
- for (const space of spaces) {
89
- expect(graphNodeRadius({ borderRadius, space })).toBe(
90
- capContainerRadius(borderRadius, space),
91
- );
92
- }
94
+ expect(graphNodeRadius({ borderRadius })).toBe(capContainerRadius(borderRadius));
93
95
  }
94
96
  });
95
97
 
96
- it("agrees with the DG-RAD doctrine table at every stop and space", () => {
98
+ it("agrees with the DG-RAD doctrine table at every stop", () => {
97
99
  for (const borderRadius of radiusStops) {
98
- for (const space of spaces) {
99
- expect(graphNodeRadiusPx({ borderRadius, space })).toBe(
100
- resolveRadiusForPart({
101
- radiusClass: "CONTAINER-CAP",
102
- stop: borderRadius,
103
- paddingPx: nodePanelPaddingPx[space],
104
- }).px,
105
- );
106
- }
100
+ expect(graphNodeRadiusPx({ borderRadius })).toBe(
101
+ resolveRadiusForPart({
102
+ radiusClass: "CONTAINER-CAP",
103
+ stop: borderRadius,
104
+ paddingPx: panelPaddingPx(),
105
+ }).px,
106
+ );
107
107
  }
108
108
  });
109
109
 
110
110
  it("never exceeds the node's own padding — the cap is what stops a child clip", () => {
111
111
  for (const borderRadius of radiusStops) {
112
- for (const space of spaces) {
113
- expect(graphNodeRadiusPx({ borderRadius, space })).toBeLessThanOrEqual(
114
- nodePanelPaddingPx[space],
115
- );
116
- }
112
+ expect(graphNodeRadiusPx({ borderRadius })).toBeLessThanOrEqual(panelPaddingPx());
117
113
  }
118
114
  });
119
115
 
120
116
  it("caps rather than passes through where the token would overflow the padding", () => {
121
- // `full` (50px) exceeds every padding stop, so every space caps.
122
- for (const space of spaces) {
123
- expect(graphNodeRadius({ borderRadius: "full", space })).toBe(nodePanelPaddingPx[space]);
124
- }
125
- // `large` (16px) fits inside `large` padding (32px) and keeps its token.
126
- expect(typeof graphNodeRadius({ borderRadius: "large", space: "large" })).toBe("string");
127
- // ...and does not fit inside `small` padding (13px), so it caps.
128
- expect(graphNodeRadius({ borderRadius: "large", space: "small" })).toBe(13);
117
+ // `full` (44px) exceeds the 16px panelPadding role, so it caps.
118
+ expect(graphNodeRadius({ borderRadius: "full" })).toBe(panelPaddingPx());
119
+ // `medium` (14px) fits inside it and keeps its token.
120
+ expect(typeof graphNodeRadius({ borderRadius: "medium" })).toBe("string");
121
+ expect(graphNodeRadius({ borderRadius: "large" })).toBe(panelPaddingPx());
129
122
  });
130
123
 
131
- it("is 0 at `none` in every space", () => {
132
- for (const space of spaces) {
133
- expect(graphNodeRadiusPx({ borderRadius: "none", space })).toBe(0);
134
- }
124
+ it("is 0 at `none`", () => {
125
+ expect(graphNodeRadiusPx({ borderRadius: "none" })).toBe(0);
135
126
  });
136
127
  });
137
128
 
@@ -243,3 +234,21 @@ describe("graphNodeMeasure seeds both axes before first paint", () => {
243
234
  ).toStrictEqual({ width: 300, minHeight: 200 });
244
235
  });
245
236
  });
237
+
238
+ it("container radius tokens read the active scale, including the $8 container key", () => {
239
+ const previous = getActiveScaleTokens();
240
+ try {
241
+ for (const control of [40, 36, 44]) {
242
+ const tokens = generateScale({ control, type: 15 }).tokens;
243
+ setActiveScaleTokens(tokens);
244
+ for (const key of ["0", "4", "6", "8", "12"] as const) {
245
+ expect(containerRadiusPx(`$${key}`)).toBe(tokens.radius[key]);
246
+ }
247
+ expect(containerRadiusPx(13)).toBe(13);
248
+ }
249
+ expect(() => containerRadiusPx("missing")).toThrow();
250
+ expect(nodeRadiusTokenPx.$8).toBe(20);
251
+ } finally {
252
+ setActiveScaleTokens(previous);
253
+ }
254
+ });
@@ -12,15 +12,28 @@
12
12
 
13
13
  import {
14
14
  capContainerRadius,
15
+ defaultScaleInputs,
16
+ generateScale,
17
+ getActiveScaleTokens,
15
18
  type Knobs,
16
19
  MIN_PRESS_TARGET,
20
+ panelPaddingPx,
17
21
  resolveRadiusForPart,
22
+ sizeTokenStop,
18
23
  } from "@multiplatform.one/theme";
19
24
 
20
- export type GraphNodeSizeKnob = Knobs["size"];
21
- export type GraphNodeSpaceKnob = Knobs["space"];
25
+ /**
26
+ * The row of the per-kind tables a node reads: its size token's side of the
27
+ * default control size (`sizeTokenStop`), never the control-height recipe.
28
+ */
29
+ export type GraphNodeSizeKnob = "small" | "medium" | "large";
22
30
  export type GraphNodeRadiusKnob = Knobs["borderRadius"];
23
31
 
32
+ /** The table row for a resolved size token. */
33
+ export function graphNodeSize(sizeToken: string): GraphNodeSizeKnob {
34
+ return sizeTokenStop(sizeToken);
35
+ }
36
+
24
37
  export type GraphPortSide = "left" | "right" | "top" | "bottom";
25
38
 
26
39
  /**
@@ -58,21 +71,6 @@ export const defaultNodeMeasure: Record<GraphNodeSizeKnob, GraphNodeMeasure> = {
58
71
  large: { width: 216, minHeight: 84 },
59
72
  };
60
73
 
61
- /**
62
- * Container padding in px per `space` stop.
63
- *
64
- * Mirrors `panelPaddingPxMap` in `resolveKnobs.ts`, which is private to that
65
- * module — the resolver mirrors the same numbers for the same reason (it stays
66
- * pure, so it cannot read live tokens). `graphNode.spec.ts` pins this mirror
67
- * against `capContainerRadius(..., "full")`, which returns the padding px
68
- * itself, so a change in the resolver fails the spec here instead of drifting.
69
- */
70
- export const nodePanelPaddingPx: Record<GraphNodeSpaceKnob, number> = {
71
- small: 13,
72
- medium: 18,
73
- large: 32,
74
- };
75
-
76
74
  /**
77
75
  * Vertical pitch between two ports on the same node edge.
78
76
  *
@@ -92,7 +90,7 @@ export function portSpacing(opts: { readonly coarsePointer: boolean }): number {
92
90
  /**
93
91
  * §11 Size, GraphNode frame: `remap` to `NodeKindSpec.measure.width`.
94
92
  *
95
- * The size knob reaches the width only through the per-kind table. It never
93
+ * The size token reaches the width only through the per-kind table. It never
96
94
  * reaches the control-height recipe (`knobProps.control.height`), because a
97
95
  * node is a container and a container that steps with a control ladder is a
98
96
  * control wearing a frame.
@@ -144,41 +142,27 @@ export function graphNodeMinHeight(opts: {
144
142
  */
145
143
  export function graphNodeRadius(knobs: {
146
144
  readonly borderRadius: GraphNodeRadiusKnob;
147
- readonly space: GraphNodeSpaceKnob;
148
145
  }): string | number {
149
- return capContainerRadius(knobs.borderRadius, knobs.space);
146
+ return capContainerRadius(knobs.borderRadius);
150
147
  }
151
148
 
152
- /**
153
- * Radius TOKEN → px.
154
- *
155
- * Mirrors `borderRadiusMap` / `radiusPxMap` in `resolveKnobs.ts`, both private
156
- * to that module. Needed because `capContainerRadius` returns either a token
157
- * (`"$4"`) or a px number, and a rendered node has to report ONE number.
158
- * `graphNode.spec.ts` pins the whole map against the doctrine table at every
159
- * stop and space, so a token that stops meaning what it says fails there.
160
- */
161
- export const nodeRadiusTokenPx: Readonly<Record<string, number>> = {
162
- $0: 0,
163
- $2: 5,
164
- $4: 9,
165
- $6: 16,
166
- $12: 50,
167
- };
149
+ /** The default scale's radius tokens; rendered nodes read the active scale. */
150
+ export const nodeRadiusTokenPx: Readonly<Record<string, number>> = Object.fromEntries(
151
+ Object.entries(generateScale(defaultScaleInputs).tokens.radius).map(([key, px]) => [
152
+ `$${key}`,
153
+ px,
154
+ ]),
155
+ );
168
156
 
169
- /**
170
- * px for whatever `capContainerRadius` resolved to. A number is already the
171
- * capped px; a token goes through the mirror above.
172
- */
157
+ /** px for a capped number or a radius token on the active scale. */
173
158
  export function containerRadiusPx(value: string | number): number {
174
159
  if (typeof value === "number") return value;
175
- const px = nodeRadiusTokenPx[value];
176
- if (px === undefined) {
177
- throw new Error(
178
- `containerRadiusPx: "${value}" is not a radius token this module mirrors — add it to nodeRadiusTokenPx beside the resolver's borderRadiusMap`,
179
- );
160
+ const radius = getActiveScaleTokens().radius;
161
+ const key = value.slice(1);
162
+ if (!value.startsWith("$") || !Object.hasOwn(radius, key)) {
163
+ throw new Error(`containerRadiusPx: "${value}" is not a radius token on the active scale`);
180
164
  }
181
- return px;
165
+ return radius[key as keyof typeof radius];
182
166
  }
183
167
 
184
168
  /**
@@ -191,14 +175,11 @@ export function containerRadiusPx(value: string | number): number {
191
175
  * doctrine/resolver drift is a failing test rather than a corner that quietly
192
176
  * clips a child.
193
177
  */
194
- export function graphNodeRadiusPx(knobs: {
195
- readonly borderRadius: GraphNodeRadiusKnob;
196
- readonly space: GraphNodeSpaceKnob;
197
- }): number {
178
+ export function graphNodeRadiusPx(knobs: { readonly borderRadius: GraphNodeRadiusKnob }): number {
198
179
  return resolveRadiusForPart({
199
180
  radiusClass: "CONTAINER-CAP",
200
181
  stop: knobs.borderRadius,
201
- paddingPx: nodePanelPaddingPx[knobs.space],
182
+ paddingPx: panelPaddingPx(),
202
183
  }).px;
203
184
  }
204
185
 
@@ -20,8 +20,8 @@
20
20
 
21
21
  import { useResolvedKnobs } from "@multiplatform.one/theme";
22
22
  import { YStack } from "tamagui";
23
- import type { GraphNodeMeasure, GraphNodePort, GraphNodeSizeKnob } from "./nodeMeasure";
24
- import { graphNodeMinHeight, graphNodeWidth, portSpacing } from "./nodeMeasure";
23
+ import type { GraphNodeMeasure, GraphNodePort } from "./nodeMeasure";
24
+ import { graphNodeMinHeight, graphNodeWidth, portSpacing, graphNodeSize } from "./nodeMeasure";
25
25
 
26
26
  export interface OverviewNodeProps {
27
27
  readonly id: string;
@@ -47,7 +47,7 @@ export function OverviewNode({
47
47
  // The same resolver call the detail body makes, so the fill is the fill the
48
48
  // user was just looking at rather than a second grey invented here.
49
49
  const { knobProps } = useResolvedKnobs({ component: "GraphNode", intent });
50
- const size = knobProps.size as GraphNodeSizeKnob;
50
+ const size = graphNodeSize(knobProps.sizeToken);
51
51
  const portSpacingPx = portSpacing({ coarsePointer });
52
52
 
53
53
  return (
@@ -132,7 +132,7 @@ function DriveConnection({ to }: { to: { nodeId: string; id: string } | null })
132
132
  function mountWithConnection(to: { nodeId: string; id: string } | null) {
133
133
  return renderWithProviders(
134
134
  <Preset cascade={false}>
135
- <Surface size="medium" density="comfortable">
135
+ <Surface size="$true">
136
136
  <NodeGraphWeb schema={schema()} defaultDocument={doc()}>
137
137
  <DriveConnection to={to} />
138
138
  </NodeGraphWeb>
@@ -149,7 +149,7 @@ describe("the puck reads React Flow's own connection state", () => {
149
149
  it("renders nothing while no connection is in progress", () => {
150
150
  const { container } = renderWithProviders(
151
151
  <Preset cascade={false}>
152
- <Surface size="medium" density="comfortable">
152
+ <Surface size="$true">
153
153
  <NodeGraphWeb schema={schema()} defaultDocument={doc()} />
154
154
  </Surface>
155
155
  </Preset>,
@@ -184,7 +184,7 @@ describe("the puck reads React Flow's own connection state", () => {
184
184
  it("can be turned off, for a consumer that draws its own", () => {
185
185
  const { container } = renderWithProviders(
186
186
  <Preset cascade={false}>
187
- <Surface size="medium" density="comfortable">
187
+ <Surface size="$true">
188
188
  <NodeGraphWeb schema={schema()} defaultDocument={doc()} connectionPuck={false}>
189
189
  <DriveConnection to={{ nodeId: "c", id: "in" }} />
190
190
  </NodeGraphWeb>
@@ -197,7 +197,7 @@ describe("the puck reads React Flow's own connection state", () => {
197
197
  it("mounted a second time renders a second div — which is why the canvas owns it", () => {
198
198
  const { container } = renderWithProviders(
199
199
  <Preset cascade={false}>
200
- <Surface size="medium" density="comfortable">
200
+ <Surface size="$true">
201
201
  <NodeGraphWeb schema={schema()} defaultDocument={doc()} connectionPuck={false}>
202
202
  <DriveConnection to={{ nodeId: "c", id: "in" }} />
203
203
  <ConnectionPuck document={doc()} schema={schema()} />
@@ -20,7 +20,7 @@
20
20
  * target the drop will then reject is worse than naming nothing.
21
21
  */
22
22
 
23
- import { useResolvedKnobs } from "@multiplatform.one/theme";
23
+ import { transitionProps, useResolvedKnobs } from "@multiplatform.one/theme";
24
24
  import { SizableText, XStack } from "tamagui";
25
25
  import { isLegalTarget } from "../interaction/legalTargets";
26
26
  import type {
@@ -165,7 +165,7 @@ export function ConnectionPuck<N extends NodeKindDefs, E extends EdgeKindDefs>({
165
165
  // saying something true, what you are dragging, just not the thing the
166
166
  // user is looking for.
167
167
  opacity={label.legal ? 1 : 0.7}
168
- transition={knobProps.transition}
168
+ {...transitionProps(knobProps.motion.overlay)}
169
169
  >
170
170
  <SizableText {...knobProps.label} numberOfLines={1}>
171
171
  {label.text}
@@ -42,9 +42,7 @@ afterEach(() => {
42
42
  function mount(element: ReactElement) {
43
43
  return renderWithProviders(
44
44
  <Preset cascade={false}>
45
- <Surface size="medium" density="comfortable">
46
- {element}
47
- </Surface>
45
+ <Surface size="$true">{element}</Surface>
48
46
  </Preset>,
49
47
  );
50
48
  }
@@ -337,9 +335,7 @@ describe("onlyRenderVisibleElements", () => {
337
335
 
338
336
  view.rerender(
339
337
  <Preset cascade={false}>
340
- <Surface size="medium" density="comfortable">
341
- {canvas(false)}
342
- </Surface>
338
+ <Surface size="$true">{canvas(false)}</Surface>
343
339
  </Preset>,
344
340
  );
345
341
 
@@ -186,24 +186,20 @@ const fuzzCombos: readonly { readonly label: string; readonly knobs: Partial<Kno
186
186
  borderRadius: "medium",
187
187
  borderWidth: "none",
188
188
  elevation: "none",
189
- space: "large",
190
- size: "medium",
191
189
  textAccent: "medium",
192
190
  headingFont: "geometric",
193
191
  bodyFont: "sans-serif",
194
192
  fontWeight: "bold",
195
- animation: "tooltip",
193
+ animation: "slow",
196
194
  },
197
195
  },
198
196
  {
199
- label: "combo 2 · radius none · size small · animation none",
197
+ label: "combo 2 · radius none · animation none",
200
198
  knobs: {
201
199
  fillStyle: "filled",
202
200
  borderRadius: "none",
203
201
  borderWidth: "none",
204
202
  elevation: "small",
205
- space: "small",
206
- size: "small",
207
203
  textAccent: "medium",
208
204
  headingFont: "rounded",
209
205
  bodyFont: "cursive",
@@ -212,14 +208,12 @@ const fuzzCombos: readonly { readonly label: string; readonly knobs: Partial<Kno
212
208
  },
213
209
  },
214
210
  {
215
- label: "combo 3 · radius full · size large · elevation large",
211
+ label: "combo 3 · radius full · elevation large",
216
212
  knobs: {
217
213
  fillStyle: "filled",
218
214
  borderRadius: "full",
219
215
  borderWidth: "small",
220
216
  elevation: "large",
221
- space: "medium",
222
- size: "large",
223
217
  textAccent: "high",
224
218
  headingFont: "mono",
225
219
  bodyFont: "serif",
@@ -67,7 +67,7 @@ import type {
67
67
  } from "../model/index";
68
68
  import { portConnectionRadius } from "../nodes/handleGeometry";
69
69
  import { defaultOverviewZoom } from "../nodes/lod";
70
- import type { GraphNodeSizeKnob } from "../nodes/nodeMeasure";
70
+ import { graphNodeSize } from "../nodes/nodeMeasure";
71
71
  import { GraphVars, graphRootClassName } from "../theme/GraphVarsStyle";
72
72
  import { GraphActionsProvider } from "./GraphActions";
73
73
  import type { GraphActions } from "./GraphActions";
@@ -336,7 +336,7 @@ function NodeGraphCanvas<N extends NodeKindDefs, E extends EdgeKindDefs>(
336
336
  // `useResolvedKnobs`, not from here; this is the `size` the measure seed
337
337
  // reads and the pointer class the var layer needs.
338
338
  const { knobProps } = useResolvedKnobs();
339
- const size = knobProps.size as GraphNodeSizeKnob;
339
+ const size = graphNodeSize(knobProps.sizeToken);
340
340
 
341
341
  const envelope = useKnobEnvelope({
342
342
  knobs,
@@ -18,12 +18,14 @@ import {
18
18
  type Knobs,
19
19
  type ResolvedKnobs,
20
20
  defaultKnobs,
21
+ cssAnimationConfig,
21
22
  isResolvableThemeValue,
22
23
  resolveKnobs,
23
24
  } from "@multiplatform.one/theme";
24
25
  import {
25
26
  type GraphThemeAccess,
26
27
  edgeStrokeWidthPx,
28
+ elevationBoxShadow,
27
29
  graphCssVars,
28
30
  graphTransitionCss,
29
31
  graphVarValues,
@@ -85,6 +87,7 @@ function enclosingSelector(css: string, index: number): string {
85
87
  // ── A theme fixture with HEX raws, so the hex ban is not vacuous ──────────────
86
88
 
87
89
  const fixtureRaw: Record<string, string> = {
90
+ shadowColor: "rgba(0,0,0,0.12)",
88
91
  color: "hsl(0, 0%, 9.0%)",
89
92
  color1: "#ffffff",
90
93
  color4: "hsl(0, 0%, 94.1%)",
@@ -102,8 +105,12 @@ const fixtureTheme: GraphThemeAccess = {
102
105
  raw: (key) => fixtureRaw[key],
103
106
  };
104
107
 
105
- function resolveFor(knobs: Partial<Knobs>, scheme: "light" | "dark" = "light"): ResolvedKnobs {
106
- return resolveKnobs({ ...defaultKnobs, ...knobs } as Knobs, undefined, scheme);
108
+ function resolveFor(
109
+ knobs: Partial<Knobs>,
110
+ scheme: "light" | "dark" = "light",
111
+ sizeToken?: string,
112
+ ): ResolvedKnobs {
113
+ return resolveKnobs({ ...defaultKnobs, ...knobs } as Knobs, undefined, scheme, { sizeToken });
107
114
  }
108
115
 
109
116
  const defaultResolved = resolveFor({});
@@ -164,10 +171,10 @@ describe("every mapped value resolves", () => {
164
171
  const schemes = ["light", "dark"] as const;
165
172
  const knobSweep: Partial<Knobs>[] = [
166
173
  {},
167
- { fillStyle: "outlined", borderWidth: "none", elevation: "none", size: "small" },
168
- { borderWidth: "small", elevation: "medium", space: "small", size: "large" },
169
- { borderWidth: "large", elevation: "large", space: "large", animation: "none" },
170
- { animation: "bouncy", density: "compact", textAccent: "low" },
174
+ { fillStyle: "outlined", borderWidth: "none", elevation: "none" },
175
+ { borderWidth: "small", elevation: "medium" },
176
+ { borderWidth: "large", elevation: "large", animation: "none" },
177
+ { animation: "bouncy", textAccent: "low" },
171
178
  ];
172
179
 
173
180
  for (const scheme of schemes) {
@@ -243,19 +250,20 @@ describe("the knob laws this layer owns", () => {
243
250
  }
244
251
  });
245
252
 
246
- it("moves the painted handle with `size` and never paints it at 44", () => {
247
- const painted = (["small", "medium", "large"] as const).map(
248
- (size) => graphCssVars(resolveFor({ size }), fixtureTheme)["--mpo-graph-handle-size"],
253
+ it("moves the painted handle with the size token and never paints it at 44", () => {
254
+ const painted = (["$3", "$true", "$5"] as const).map(
255
+ (sizeToken) =>
256
+ graphCssVars(resolveFor({}, "light", sizeToken), fixtureTheme)["--mpo-graph-handle-size"],
249
257
  );
250
258
  expect(painted).toEqual(["8px", "10px", "12px"]);
251
259
  expect(painted).not.toContain("44px");
252
260
  });
253
261
 
254
262
  it("floors the handle HIT at 44 on coarse and at max(painted, 24) on fine", () => {
255
- const coarse = graphCssVars(resolveFor({ size: "small" }), fixtureTheme, {
263
+ const coarse = graphCssVars(resolveFor({}, "light", "$3"), fixtureTheme, {
256
264
  pointer: "coarse",
257
265
  });
258
- const fine = graphCssVars(resolveFor({ size: "small" }), fixtureTheme, { pointer: "fine" });
266
+ const fine = graphCssVars(resolveFor({}, "light", "$3"), fixtureTheme, { pointer: "fine" });
259
267
  expect(coarse["--mpo-graph-handle-hit"]).toBe("44px");
260
268
  expect(fine["--mpo-graph-handle-hit"]).toBe("24px");
261
269
  });
@@ -267,15 +275,16 @@ describe("the knob laws this layer owns", () => {
267
275
  it("emits a complete CSS transition shorthand, or `none`", () => {
268
276
  expect(
269
277
  graphCssVars(resolveFor({ animation: "quick" }), fixtureTheme)["--mpo-graph-transition"],
270
- ).toBe("all ease-in 100ms");
278
+ ).toBe(`opacity ${cssAnimationConfig.quick}`);
271
279
  expect(
272
280
  graphCssVars(resolveFor({ animation: "none" }), fixtureTheme)["--mpo-graph-transition"],
273
281
  ).toBe("none");
274
282
  });
275
283
 
276
284
  it("emits no `$token` — a CSS variable whose value is a token is dead", () => {
277
- for (const knobs of [{}, { space: "small" as const }, { space: "large" as const }]) {
278
- for (const [name, value] of Object.entries(graphCssVars(resolveFor(knobs), fixtureTheme))) {
285
+ for (const sizeToken of [undefined, "$3", "$5"]) {
286
+ const emitted = graphCssVars(resolveFor({}, "light", sizeToken), fixtureTheme);
287
+ for (const [name, value] of Object.entries(emitted)) {
279
288
  expect(value.includes("$"), `${name}: ${value}`).toBe(false);
280
289
  }
281
290
  }
@@ -294,25 +303,10 @@ describe("the mirrored token tables", () => {
294
303
  }
295
304
  });
296
305
 
297
- it("mirrors the theme package's own CSS animation timings", () => {
298
- // Read as TEXT, not imported. `animations/index` is a platform twin and
299
- // resolves to the react-native spring table off the web, and the package's
300
- // exports map does not carry this subpath through the workspace alias —
301
- // both of which would make an import prove something other than the claim.
302
- const source = readFileSync(
303
- path.resolve(packageRoot, "../theme/src/theme/animations/css.ts"),
304
- "utf8",
305
- );
306
- const block = /export const animationConfig = \{([\s\S]*?)\n\} as const;/.exec(source);
307
- expect(block, "animationConfig is no longer an object literal in css.ts").not.toBeNull();
308
- const installed: Record<string, string> = {};
309
- for (const entry of (block as RegExpExecArray)[1].matchAll(/(\w+):\s*"([^"]+)"/g)) {
310
- installed[entry[1]] = entry[2];
311
- }
312
- expect(Object.keys(installed).length).toBeGreaterThan(0);
313
- expect(Object.keys(graphTransitionCss).sort()).toEqual(Object.keys(installed).sort());
314
- for (const [name, timing] of Object.entries(installed)) {
315
- expect(graphTransitionCss[name], name).toBe(`all ${timing}`);
306
+ it("uses the theme CSS table for explicit guide opacity transitions", () => {
307
+ expect(Object.keys(graphTransitionCss)).toEqual(Object.keys(cssAnimationConfig));
308
+ for (const [name, timing] of Object.entries(cssAnimationConfig)) {
309
+ expect(graphTransitionCss[name], name).toBe(`opacity ${timing}`);
316
310
  }
317
311
  });
318
312
 
@@ -320,3 +314,41 @@ describe("the mirrored token tables", () => {
320
314
  expect(Object.keys(edgeStrokeWidthPx).sort()).toEqual(["0", "0.5", "1", "2"].sort());
321
315
  });
322
316
  });
317
+
318
+ describe("size-formula shadow bridge", () => {
319
+ it("resolves the shadow variable into CSS and its colour list", () => {
320
+ const resolved = resolveKnobs({ ...defaultKnobs, elevation: "medium" });
321
+ expect(elevationBoxShadow(resolved, fixtureTheme)).toEqual({
322
+ css: "0px 8px 15px rgba(0,0,0,0.12)",
323
+ colors: [fixtureRaw.shadowColor],
324
+ });
325
+ const native = {
326
+ ...resolved,
327
+ knobProps: {
328
+ ...resolved.knobProps,
329
+ elevationChrome: { ...resolved.knobProps.elevationChrome, style: undefined },
330
+ },
331
+ };
332
+ expect(elevationBoxShadow(native, fixtureTheme)).toEqual(
333
+ elevationBoxShadow(resolved, fixtureTheme),
334
+ );
335
+ });
336
+
337
+ it("paints the controls with the elevated pair, so dark separators stay off the fill", () => {
338
+ for (const scheme of ["light", "dark"] as const) {
339
+ const resolved = resolveFor({}, scheme);
340
+ const vars = graphVarValues(resolved, fixtureTheme);
341
+ const surface = resolved.knobProps.elevatedSurface;
342
+ expect(vars["--xy-controls-button-background-color"]).toMatchObject({
343
+ color: fixtureTheme.css(String(surface.backgroundColor).slice(1)),
344
+ });
345
+ expect(vars["--xy-controls-button-border-color"]).toMatchObject({
346
+ color: fixtureTheme.css(String(surface.borderColor).slice(1)),
347
+ });
348
+ }
349
+ const dark = graphVarValues(resolveFor({}, "dark"), fixtureTheme);
350
+ expect(dark["--xy-controls-button-border-color"]).not.toEqual(
351
+ dark["--xy-controls-button-background-color"],
352
+ );
353
+ });
354
+ });