inkbridge 0.1.0-beta.2 → 0.1.0-beta.21

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 (178) hide show
  1. package/README.md +108 -25
  2. package/bin/inkbridge.mjs +354 -83
  3. package/code.js +40 -11802
  4. package/manifest.json +1 -0
  5. package/package.json +74 -23
  6. package/scanner/adapter-utils-regression.ts +159 -0
  7. package/scanner/aspect-percent-position-regression.ts +237 -0
  8. package/scanner/aspect-ratio-regression.ts +90 -0
  9. package/scanner/blob-placement-regression.ts +2 -2
  10. package/scanner/block-cache-regression.ts +195 -0
  11. package/scanner/bundle-size-regression.ts +50 -0
  12. package/scanner/child-sizing-matrix-regression.ts +303 -0
  13. package/scanner/cli.ts +342 -13
  14. package/scanner/component-scanner.ts +2108 -174
  15. package/scanner/component-sections-regression.ts +198 -0
  16. package/scanner/compound-classes-lookup-regression.ts +163 -0
  17. package/scanner/css-token-reader-regression.ts +7 -6
  18. package/scanner/css-token-reader.ts +152 -31
  19. package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
  20. package/scanner/cva-master-icon-regression.ts +315 -0
  21. package/scanner/data-attr-prop-alias-regression.ts +129 -0
  22. package/scanner/explicit-size-root-regression.ts +102 -0
  23. package/scanner/font-family-extract-regression.ts +113 -0
  24. package/scanner/font-style-resolver-regression.ts +1 -1
  25. package/scanner/framework-adapter-shadcn-regression.ts +480 -0
  26. package/scanner/full-width-matrix-regression.ts +338 -0
  27. package/scanner/grid-cols-extraction-regression.ts +110 -0
  28. package/scanner/image-src-collector-regression.ts +204 -0
  29. package/scanner/inline-flex-regression.ts +235 -0
  30. package/scanner/input-range-regression.ts +217 -0
  31. package/scanner/instance-rendering-regression.ts +224 -0
  32. package/scanner/jsx-prop-unresolved-regression.ts +178 -0
  33. package/scanner/jsx-text-regression.ts +178 -0
  34. package/scanner/layout-alignment-regression.ts +108 -0
  35. package/scanner/layout-flex-regression.ts +90 -0
  36. package/scanner/layout-mode-regression.ts +71 -0
  37. package/scanner/layout-sizing-regression.ts +227 -0
  38. package/scanner/layout-spacing-regression.ts +135 -0
  39. package/scanner/local-const-className-regression.ts +331 -0
  40. package/scanner/percent-position-regression.ts +105 -0
  41. package/scanner/provider-cascade-regression.ts +224 -0
  42. package/scanner/provider-flatten-regression.ts +235 -0
  43. package/scanner/radial-gradient-regression.ts +1 -1
  44. package/scanner/render-prop-parser-regression.ts +161 -0
  45. package/scanner/ring-utility-regression.ts +153 -0
  46. package/scanner/sandbox-spread-regression.ts +125 -0
  47. package/scanner/selection-pressed-regression.ts +241 -0
  48. package/scanner/size-full-normalization-regression.ts +127 -0
  49. package/scanner/state-classification-regression.ts +175 -0
  50. package/scanner/story-diagnostics-regression.ts +216 -0
  51. package/scanner/story-dimensioning-regression.ts +298 -0
  52. package/scanner/story-render-strategy-regression.ts +205 -0
  53. package/scanner/stretch-to-parent-width-regression.ts +147 -0
  54. package/scanner/svg-fill-parent-regression.ts +98 -0
  55. package/scanner/svg-group-inheritance-regression.ts +166 -0
  56. package/scanner/svg-marker-inline-regression.ts +211 -0
  57. package/scanner/svg-marker-regression.ts +116 -0
  58. package/scanner/tailwind-parser.ts +46 -4
  59. package/scanner/text-resize-matrix-regression.ts +173 -0
  60. package/scanner/transform-math-regression.ts +1 -1
  61. package/scanner/types.ts +26 -2
  62. package/src/cache/frame-cache.ts +150 -0
  63. package/src/cache/index.ts +2 -0
  64. package/src/{component-defs.ts → components/component-defs.ts} +25 -10
  65. package/src/{component-gen.ts → components/component-gen.ts} +43 -116
  66. package/src/components/component-instance.ts +386 -0
  67. package/src/components/component-library.ts +44 -0
  68. package/src/components/component-lookup.ts +161 -0
  69. package/src/components/index.ts +7 -0
  70. package/src/components/scanner-types.ts +39 -0
  71. package/src/components/symbol-instance-policy.ts +312 -0
  72. package/src/design-system/block-cache.ts +130 -0
  73. package/src/design-system/component-sections.ts +107 -0
  74. package/src/design-system/cva-inference.ts +187 -0
  75. package/src/design-system/cva-master.ts +427 -0
  76. package/src/design-system/cva-utils.ts +29 -0
  77. package/src/design-system/design-system.ts +334 -0
  78. package/src/design-system/frame-stabilizers.ts +191 -0
  79. package/src/design-system/frame-utils.ts +46 -0
  80. package/src/design-system/generated-node.ts +84 -0
  81. package/src/design-system/icon-rendering.ts +229 -0
  82. package/src/design-system/index.ts +13 -0
  83. package/src/design-system/instance-rendering.ts +307 -0
  84. package/src/design-system/master-shared.ts +133 -0
  85. package/src/design-system/node-helpers.ts +237 -0
  86. package/src/design-system/node-variants.ts +196 -0
  87. package/src/design-system/non-cva-master.ts +104 -0
  88. package/src/design-system/portal-handling.ts +138 -0
  89. package/src/design-system/preview-builder.ts +738 -0
  90. package/src/{render-context.ts → design-system/render-context.ts} +32 -6
  91. package/src/design-system/render-prop-parser.ts +50 -0
  92. package/src/design-system/responsive-resolver.ts +180 -0
  93. package/src/design-system/selectable-state.ts +157 -0
  94. package/src/design-system/state-master.ts +267 -0
  95. package/src/design-system/state-utils.ts +15 -0
  96. package/src/design-system/story-builder-context.ts +40 -0
  97. package/src/design-system/story-builder.ts +1322 -0
  98. package/src/design-system/story-diagnostics.ts +80 -0
  99. package/src/design-system/story-dimensioning.ts +272 -0
  100. package/src/design-system/story-frames.ts +400 -0
  101. package/src/design-system/story-instance.ts +333 -0
  102. package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
  103. package/src/design-system/story-render-strategy.ts +150 -0
  104. package/src/design-system/story-tree-search.ts +110 -0
  105. package/src/design-system/symbol-fallback.ts +89 -0
  106. package/src/design-system/symbol-source.ts +172 -0
  107. package/src/design-system/table-helpers.ts +56 -0
  108. package/src/design-system/tag-predicates.ts +99 -0
  109. package/src/design-system/theme-context.ts +52 -0
  110. package/src/design-system/typography.ts +100 -0
  111. package/src/design-system/ui-builder.ts +2676 -0
  112. package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
  113. package/src/effects/icon-builder.ts +1074 -0
  114. package/src/effects/index.ts +5 -0
  115. package/src/effects/portal-panel.ts +369 -0
  116. package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
  117. package/src/framework-adapters/index.ts +47 -0
  118. package/src/framework-adapters/shadcn.ts +541 -0
  119. package/src/{github.ts → github/github.ts} +46 -21
  120. package/src/github/index.ts +1 -0
  121. package/src/layout/deferred-layout.ts +1556 -0
  122. package/src/layout/index.ts +24 -0
  123. package/src/layout/layout-parser.ts +375 -0
  124. package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
  125. package/src/layout/parser/alignment.ts +54 -0
  126. package/src/layout/parser/flex.ts +59 -0
  127. package/src/layout/parser/index.ts +65 -0
  128. package/src/layout/parser/ir.ts +80 -0
  129. package/src/layout/parser/layout-mode.ts +57 -0
  130. package/src/layout/parser/sizing.ts +241 -0
  131. package/src/layout/parser/spacing-scale.ts +78 -0
  132. package/src/layout/parser/spacing.ts +134 -0
  133. package/src/layout/ring-utils.ts +120 -0
  134. package/src/layout/size-utils.ts +143 -0
  135. package/src/layout/text-resize-decision.ts +51 -0
  136. package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
  137. package/src/main.ts +444 -162
  138. package/src/{config.ts → plugin/config.ts} +12 -12
  139. package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
  140. package/src/plugin/image-src-collector.ts +52 -0
  141. package/src/plugin/index.ts +3 -0
  142. package/src/plugin/packs/index.ts +2 -0
  143. package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
  144. package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
  145. package/src/render-engine-version.ts +2 -0
  146. package/src/tailwind/adapter-utils.ts +137 -0
  147. package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
  148. package/src/tailwind/index.ts +8 -0
  149. package/src/tailwind/jsx-utils.ts +319 -0
  150. package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
  151. package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
  152. package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
  153. package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
  154. package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
  155. package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
  156. package/src/text/index.ts +4 -0
  157. package/src/{inline-text.ts → text/inline-text.ts} +13 -13
  158. package/src/{text-builder.ts → text/text-builder.ts} +24 -7
  159. package/src/{text-line.ts → text/text-line.ts} +2 -2
  160. package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
  161. package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
  162. package/src/{colors.ts → tokens/colors.ts} +13 -6
  163. package/src/tokens/index.ts +6 -0
  164. package/src/{token-source.ts → tokens/token-source.ts} +4 -1
  165. package/src/{tokens.ts → tokens/tokens.ts} +116 -20
  166. package/src/{variables.ts → tokens/variables.ts} +447 -102
  167. package/templates/patch-tokens-route.ts +25 -6
  168. package/templates/scan-components-route.ts +26 -5
  169. package/ui.html +485 -37
  170. package/src/component-lookup.ts +0 -82
  171. package/src/design-system.ts +0 -59
  172. package/src/icon-builder.ts +0 -607
  173. package/src/layout-parser.ts +0 -667
  174. package/src/story-builder.ts +0 -1706
  175. package/src/ui-builder.ts +0 -1996
  176. /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
  177. /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
  178. /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
@@ -0,0 +1,80 @@
1
+ import type { ComponentDef, ComponentStory, LayoutInfo } from '../components';
2
+ import { RENDER_ENGINE_VERSION } from '../render-engine-version';
3
+ import { hasNodeChildren } from './frame-utils';
4
+ import { normalizeLayoutClasses } from './story-layout';
5
+
6
+ /**
7
+ * Per-story inspection metadata written to the Story Layout frame as Figma
8
+ * plugin data. Surfaced by the `Debug Selection` plugin command — read from
9
+ * `inkbridge:story-scan` (scanner output snapshot) and `inkbridge:story-render`
10
+ * (render-time decisions) so you can answer "why did this story render the
11
+ * way it did?" without reattaching a debugger.
12
+ *
13
+ * Pure side-effect: writes string-encoded summaries; tolerates `setPluginData`
14
+ * unavailability or throws (the diagnostics path must never crash a render).
15
+ */
16
+
17
+ function getStoryTreeRootTag(story: ComponentStory): string {
18
+ const root = story && story.jsxTree;
19
+ if (!root || root.type !== 'element') return '';
20
+ return String(root.tagName || '');
21
+ }
22
+
23
+ function getStoryTreeRootChildCount(story: ComponentStory): number {
24
+ const root = story && story.jsxTree;
25
+ if (!root || !Array.isArray(root.children)) return 0;
26
+ return root.children.length;
27
+ }
28
+
29
+ function summarizeLayoutClasses(classes: string[] | undefined, maxLength: number): string {
30
+ const joined = Array.isArray(classes) ? classes.join(' ') : '';
31
+ if (joined.length <= maxLength) return joined;
32
+ return joined.slice(0, maxLength);
33
+ }
34
+
35
+ export function setStoryRenderDiagnostics(
36
+ layout: LayoutInfo,
37
+ def: ComponentDef,
38
+ story: ComponentStory,
39
+ mode: string,
40
+ added: number,
41
+ usedStoryTree: boolean
42
+ ): void {
43
+ if (!layout || typeof layout.setPluginData !== 'function') return;
44
+ try {
45
+ const storyName = String(story && story.name ? story.name : '');
46
+ const defName = String(def && def.name ? def.name : '');
47
+ const instances = Array.isArray(story && story.instances) ? story.instances.length : 0;
48
+ const layoutClasses = normalizeLayoutClasses(story && story.layoutClasses);
49
+ const scanSummary = [
50
+ 'jsx=' + (story && story.jsxTree ? '1' : '0'),
51
+ 'root=' + getStoryTreeRootTag(story),
52
+ 'rootChildren=' + String(getStoryTreeRootChildCount(story)),
53
+ 'instances=' + String(instances),
54
+ 'layout=' + summarizeLayoutClasses(layoutClasses, 120),
55
+ ].join(';');
56
+ const renderSummary = [
57
+ 'mode=' + mode,
58
+ 'added=' + String(added),
59
+ 'treeSelected=' + (usedStoryTree ? '1' : '0'),
60
+ 'children=' + (hasNodeChildren(layout) ? String(layout.children.length) : '0'),
61
+ 'engine=' + RENDER_ENGINE_VERSION,
62
+ ].join(';');
63
+
64
+ layout.setPluginData('inkbridge:story-name', storyName);
65
+ layout.setPluginData('inkbridge:story-def', defName);
66
+ layout.setPluginData('inkbridge:story-scan', scanSummary);
67
+ layout.setPluginData('inkbridge:story-render', renderSummary);
68
+ } catch (_err) {
69
+ // Ignore diagnostics write failures.
70
+ }
71
+ }
72
+
73
+ // Exported for the regression fixture so the pure helpers can be tested in
74
+ // isolation. Not used outside this module's `setStoryRenderDiagnostics` in
75
+ // runtime code; the test boundary is the only consumer.
76
+ export const _internal = {
77
+ getStoryTreeRootTag,
78
+ getStoryTreeRootChildCount,
79
+ summarizeLayoutClasses,
80
+ };
@@ -0,0 +1,272 @@
1
+ import {
2
+ extractLeadingContainerMaxWidthFromTree,
3
+ splitClassName,
4
+ treeHasFullWidth,
5
+ type JsxElement,
6
+ type JsxNode,
7
+ } from '../tailwind';
8
+ import type { ComponentStory, LayoutInfo } from '../components';
9
+ import {
10
+ extractFixedWidth,
11
+ extractGridBreakpointWidth,
12
+ extractGridColumns,
13
+ extractMaxWidth,
14
+ } from '../layout';
15
+ import {
16
+ defaultGridWidth,
17
+ extractGridBreakpointWidthFromTree,
18
+ extractGridColumnsFromTree,
19
+ } from './story-layout';
20
+ import { resolvePortalAwareStoryTree } from './portal-handling';
21
+ import type { StoryBuilderContext } from './story-builder-context';
22
+
23
+ // Components whose root is `md:hidden` (or larger-breakpoint-hidden) are
24
+ // mobile-only designs. At the default ~900px width their flex/justify-between
25
+ // layout spreads content across an empty frame. Render such components at a
26
+ // mobile width so the design is legible.
27
+ export const MOBILE_ONLY_DEFAULT_WIDTH = 360;
28
+
29
+ // Viewport-anchored stories (Sheet drawers, full-page shells, dialogs) use
30
+ // `h-full` / `h-screen` to anchor against a viewport that doesn't exist in
31
+ // Figma auto-layout. Without a synthetic height the container hugs content
32
+ // and descendant `flex-1` siblings collapse to 0. Detect the pattern at the
33
+ // story root and pin a reasonable design height so the vertical grow cascade
34
+ // lights up. Two preset heights cover the common shapes.
35
+ export const VIEWPORT_HEIGHTS = {
36
+ DRAWER: 700,
37
+ FULL_PAGE: 900,
38
+ } as const;
39
+
40
+ /**
41
+ * Walks the leading single-child spine of a JSX tree and collects classes so a
42
+ * decorator-wrapped component (common in Storybook stories) still exposes its
43
+ * intended root behaviour. Stops after 4 levels or at the first multi-child
44
+ * fan-out — those branches are the body, not the spine.
45
+ */
46
+ export function collectLeadingSpineClasses(node: JsxNode, out: string[]): void {
47
+ let current: JsxNode | undefined = node;
48
+ let depth = 0;
49
+ while (current && current.type === 'element' && depth < 4) {
50
+ const el = current as JsxElement;
51
+ const cls = el.props && el.props.className;
52
+ if (typeof cls === 'string') {
53
+ const list = splitClassName(cls);
54
+ for (let i = 0; i < list.length; i++) out.push(list[i]);
55
+ }
56
+ const children = el.children || [];
57
+ if (children.length !== 1 || !children[0] || children[0].type !== 'element') break;
58
+ current = children[0];
59
+ depth++;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Depth-limited descendant scan for a single Tailwind class. Used by the
65
+ * viewport-pattern detector to confirm a descendant `flex-1` exists before
66
+ * pinning a drawer height — without `flex-1` the height pin would just
67
+ * stretch a fixed-height container with no grow cascade to fill it.
68
+ */
69
+ export function treeHasDescendantClass(node: JsxNode, cls: string, depth: number = 0): boolean {
70
+ if (!node || typeof node !== 'object' || depth > 6) return false;
71
+ if (node.type === 'element') {
72
+ const el = node as JsxElement;
73
+ if (el.props && typeof el.props.className === 'string') {
74
+ const list = splitClassName(el.props.className);
75
+ if (list.indexOf(cls) !== -1) return true;
76
+ }
77
+ const children = Array.isArray(el.children) ? el.children : [];
78
+ for (let i = 0; i < children.length; i++) {
79
+ if (treeHasDescendantClass(children[i], cls, depth + 1)) return true;
80
+ }
81
+ }
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Detects portal-mounted nodes that anchor to viewport height. Required because
87
+ * Sheet/Drawer content is rendered into a portal subtree — its `h-full` /
88
+ * `inset-y-0` classes don't show up on the story's own spine.
89
+ */
90
+ export function treeHasPortalWithFullHeight(node: JsxNode, depth: number = 0): boolean {
91
+ if (!node || typeof node !== 'object' || depth > 8) return false;
92
+ if (node.type === 'element') {
93
+ const el = node as JsxElement;
94
+ if (el.props && el.props.__fromPortal) {
95
+ const cls = el.props.className;
96
+ if (typeof cls === 'string') {
97
+ const list = splitClassName(cls);
98
+ if (list.indexOf('h-full') !== -1 || list.indexOf('h-screen') !== -1 || list.indexOf('inset-y-0') !== -1) {
99
+ if (treeHasDescendantClass(el, 'flex-1')) return true;
100
+ }
101
+ }
102
+ }
103
+ const children = Array.isArray(el.children) ? el.children : [];
104
+ for (let i = 0; i < children.length; i++) {
105
+ if (treeHasPortalWithFullHeight(children[i], depth + 1)) return true;
106
+ }
107
+ }
108
+ return false;
109
+ }
110
+
111
+ /**
112
+ * Returns a synthetic viewport height when the story root needs a fixed
113
+ * primary axis to make `h-full` / `flex-1` children render correctly:
114
+ * - `h-screen` / `min-h-screen` on the spine → FULL_PAGE (900)
115
+ * - `h-full` + vertical flex + descendant `flex-1` on the spine → DRAWER (700)
116
+ * - A portal-mounted node with `h-full` (or `inset-y-0`) + descendant
117
+ * `flex-1` (Sheet / Drawer content) → DRAWER (700)
118
+ * Returns null when the story does not need viewport anchoring.
119
+ */
120
+ export function detectViewportHeightPattern(story: ComponentStory, layoutClasses: string[]): number | null {
121
+ // Portal-filter the tree the same way the renderer does: closed stories drop
122
+ // the portal subtree, so a `Sheet` default (closed) shouldn't be detected as
123
+ // a drawer — only its `defaultOpen` sibling should.
124
+ const tree = story && story.jsxTree ? resolvePortalAwareStoryTree(story.jsxTree) : null;
125
+
126
+ const spineClasses: string[] = [];
127
+ if (tree) collectLeadingSpineClasses(tree, spineClasses);
128
+ for (let i = 0; i < layoutClasses.length; i++) spineClasses.push(layoutClasses[i]);
129
+
130
+ for (let i = 0; i < spineClasses.length; i++) {
131
+ const cls = spineClasses[i];
132
+ if (cls === 'h-screen' || cls === 'min-h-screen') return VIEWPORT_HEIGHTS.FULL_PAGE;
133
+ }
134
+
135
+ const spineHasHFull = spineClasses.indexOf('h-full') !== -1;
136
+ const spineHasFlexCol = spineClasses.indexOf('flex-col') !== -1;
137
+ const treeHasFlexOne = tree ? treeHasDescendantClass(tree, 'flex-1') : false;
138
+ if (spineHasHFull && spineHasFlexCol && treeHasFlexOne) return VIEWPORT_HEIGHTS.DRAWER;
139
+
140
+ if (tree && treeHasPortalWithFullHeight(tree)) return VIEWPORT_HEIGHTS.DRAWER;
141
+
142
+ return null;
143
+ }
144
+
145
+ /**
146
+ * Returns the mobile width when the story root carries a `{bp}:hidden` class —
147
+ * the design is intended to render only at mobile width and would look broken
148
+ * at the default 900px.
149
+ */
150
+ export function mobileOnlyRootWidth(story: ComponentStory, layoutClasses: string[]): number | null {
151
+ const rootClasses: string[] = [];
152
+ if (story && story.jsxTree) collectLeadingSpineClasses(story.jsxTree, rootClasses);
153
+ for (let i = 0; i < layoutClasses.length; i++) rootClasses.push(layoutClasses[i]);
154
+ for (let i = 0; i < rootClasses.length; i++) {
155
+ const cls = rootClasses[i];
156
+ // Match `sm:hidden`, `md:hidden`, `lg:hidden`, `xl:hidden`, `2xl:hidden`.
157
+ if (/^(sm|md|lg|xl|2xl):hidden$/.test(cls)) return MOBILE_ONLY_DEFAULT_WIDTH;
158
+ }
159
+ return null;
160
+ }
161
+
162
+ /**
163
+ * Resolves the desktop width of the Story Layout frame and applies it. Picks
164
+ * between an explicit fixed-width class, a tree-derived max-width, grid-cols
165
+ * minimums, mobile-only overrides, and the generic 900px fallback in priority
166
+ * order. The `viewportWidth` parameter overrides everything (used by
167
+ * responsive previews to size at a specific breakpoint).
168
+ */
169
+ export function resolveStoryLayoutWidth(
170
+ story: ComponentStory,
171
+ layout: LayoutInfo,
172
+ layoutClasses: string[],
173
+ ctx: StoryBuilderContext,
174
+ viewportWidth?: number
175
+ ): number {
176
+ const fixedWidth = extractFixedWidth(layoutClasses);
177
+ const maxWidthFromLayout = extractMaxWidth(layoutClasses);
178
+ const maxWidthFromTree = extractLeadingContainerMaxWidthFromTree(story.jsxTree);
179
+ const constrainedMaxWidth = (
180
+ maxWidthFromLayout != null && maxWidthFromTree != null
181
+ ? Math.min(maxWidthFromLayout, maxWidthFromTree)
182
+ : (maxWidthFromTree != null ? maxWidthFromTree : maxWidthFromLayout)
183
+ );
184
+ const mobileOnlyWidth = mobileOnlyRootWidth(story, layoutClasses);
185
+ const fallbackWidth = !fixedWidth && story.jsxTree && treeHasFullWidth(story.jsxTree, null, {
186
+ getComponentDefByName: ctx.getComponentDefByName,
187
+ normalizeComponentDef: ctx.normalizeComponentDef,
188
+ hasWidthHintInClasses: ctx.hasWidthHintInClasses,
189
+ propsContainWidthHint: ctx.propsContainWidthHint,
190
+ })
191
+ ? (constrainedMaxWidth || 900)
192
+ : null;
193
+ let effectiveWidth = fixedWidth || fallbackWidth;
194
+ const treeGridCols = story.jsxTree ? extractGridColumnsFromTree(story.jsxTree) : null;
195
+ const treeGridMinWidth = story.jsxTree ? extractGridBreakpointWidthFromTree(story.jsxTree) : null;
196
+ const layoutGridCols = extractGridColumns(layoutClasses);
197
+ const layoutGridMinWidth = extractGridBreakpointWidth(layoutClasses);
198
+ const gridCols = layoutGridCols || treeGridCols;
199
+ if (!effectiveWidth && gridCols) {
200
+ effectiveWidth = defaultGridWidth(gridCols);
201
+ } else if (effectiveWidth && layoutGridCols) {
202
+ const minGridWidth = defaultGridWidth(layoutGridCols);
203
+ if (effectiveWidth < minGridWidth) {
204
+ effectiveWidth = minGridWidth;
205
+ }
206
+ }
207
+ const gridMinWidth = Math.max(layoutGridMinWidth || 0, treeGridMinWidth || 0);
208
+ if (gridMinWidth > 0) {
209
+ if (!effectiveWidth || effectiveWidth < gridMinWidth) {
210
+ effectiveWidth = gridMinWidth;
211
+ }
212
+ }
213
+ if (!effectiveWidth) {
214
+ effectiveWidth = mobileOnlyWidth != null ? mobileOnlyWidth : 900;
215
+ } else if (mobileOnlyWidth != null && !fixedWidth && constrainedMaxWidth == null && !gridCols && !gridMinWidth) {
216
+ // Mobile-only components with only `w-full` on a decorator would otherwise
217
+ // inherit the generic 900px fallback via treeHasFullWidth. Clamp them to
218
+ // the mobile default so the design renders at its intended width.
219
+ effectiveWidth = mobileOnlyWidth;
220
+ }
221
+ if (viewportWidth != null && Number.isFinite(viewportWidth) && viewportWidth > 0) {
222
+ effectiveWidth = viewportWidth;
223
+ }
224
+ if (effectiveWidth) {
225
+ layout.resize(effectiveWidth, layout.height);
226
+ if (layout.layoutMode === 'HORIZONTAL') {
227
+ layout.primaryAxisSizingMode = 'FIXED';
228
+ } else {
229
+ layout.counterAxisSizingMode = 'FIXED';
230
+ }
231
+ if (!ctx.hasExplicitHeight(layoutClasses)) {
232
+ // Keep natural content-driven height in responsive previews.
233
+ // Forcing a transient 1px height can leave complex portal content
234
+ // (e.g. Dialog panels) visually clipped after layout settles.
235
+ if (layout.layoutMode === 'HORIZONTAL') {
236
+ layout.counterAxisSizingMode = 'AUTO';
237
+ } else {
238
+ layout.primaryAxisSizingMode = 'AUTO';
239
+ }
240
+ }
241
+ }
242
+ return effectiveWidth;
243
+ }
244
+
245
+ /**
246
+ * Pin the Story Layout's primary-axis height for viewport-anchored stories so
247
+ * `h-full` / `flex-1` descendants render at their intended sizes. Mirror of
248
+ * `resolveStoryLayoutWidth` on the primary axis.
249
+ */
250
+ export function resolveStoryLayoutHeight(
251
+ story: ComponentStory,
252
+ layout: LayoutInfo,
253
+ layoutClasses: string[],
254
+ ctx: StoryBuilderContext
255
+ ): number | null {
256
+ if (ctx.hasExplicitHeight(layoutClasses)) return null;
257
+ const height = detectViewportHeightPattern(story, layoutClasses);
258
+ if (height == null) return null;
259
+ try {
260
+ layout.resize(layout.width, height);
261
+ if (layout.layoutMode === 'VERTICAL') {
262
+ layout.primaryAxisSizingMode = 'FIXED';
263
+ } else if (layout.layoutMode === 'HORIZONTAL') {
264
+ layout.counterAxisSizingMode = 'FIXED';
265
+ } else if ('primaryAxisSizingMode' in layout) {
266
+ layout.primaryAxisSizingMode = 'FIXED';
267
+ }
268
+ } catch (_err) {
269
+ // ignore resize errors
270
+ }
271
+ return height;
272
+ }