@oxide/design-system 6.7.0-canary.8a33f9c → 6.7.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.
package/README.md CHANGED
@@ -20,20 +20,11 @@ Releases are managed via GitHub Actions workflows triggered from the Actions tab
20
20
  tag. Install it with `npm install @oxide/design-system@canary` to test changes before
21
21
  merging.
22
22
 
23
- ## Figma Plugins
24
-
25
- Figma plugins live in `plugins/`. Each is a standalone npm project with its own
26
- `package.json` and `node_modules` — `cd plugins/<name> && npm install && npm run build`
27
- (or use the `dual-grid:*` / `token-sync:*` scripts at the root). Load a built plugin in
28
- Figma via **Plugins → Development → Import plugin from manifest…**.
29
-
30
- - `plugins/token-sync` — reads the CSS files in `styles/` and compares them against
31
- Figma variables; changes can be applied from the plugin UI.
32
- - `plugins/dual-grid` — generates a synchronised column + cell grid, ASCII grid, and
33
- type specimens on a frame.
34
-
35
23
  ## Syncing with Figma
36
24
 
25
+ The Token Sync Figma plugin reads the CSS files in `styles/` directly and compares them
26
+ against Figma variables. Changes can be applied from the plugin UI.
27
+
37
28
  To regenerate colour palettes, run `npm run color-gen:apply`. This updates the `--color-*`
38
29
  variables in `styles/main.css` and writes the accent override files.
39
30
 
@@ -81,7 +72,7 @@ This is type-checked, and will throw an error if the corresponding icon doesn't
81
72
 
82
73
  ## Usage
83
74
 
84
- This package provides four main entry points:
75
+ This package provides two main entry points:
85
76
 
86
77
  ### UI Components (`@oxide/design-system/ui`)
87
78
 
@@ -126,81 +117,3 @@ content: [
126
117
  'node_modules/@oxide/design-system/components/**/*.{ts,tsx,jsx,js}',
127
118
  ],
128
119
  ```
129
-
130
- ### Grid math (`@oxide/design-system/grid`)
131
-
132
- The maths behind the dual-grid Figma plugin: computes an ASCII cell grid aligned with a
133
- column grid, where margin, column and gutter widths are all whole multiples of the cell
134
- size. Pure TypeScript with no dependencies. See `plugins/dual-grid/README.md` for the
135
- derivation.
136
-
137
- `computeGrid` takes the layout column count, the frame size, and either pixel targets
138
- (`mode: 'auto'` — a solver finds the closest whole-cell fit) or explicit cell counts
139
- (`mode: 'manual'`):
140
-
141
- ```ts
142
- import { computeGrid, gridLineSegments, snapLineHeight } from '@oxide/design-system/grid'
143
-
144
- const grid = computeGrid({
145
- mode: 'auto', // or 'manual' with marginCells / gutterCells
146
- columns: 12, // layout columns
147
- width: 1920, // frame size, px
148
- height: 1080,
149
- targetMarginPx: 50, // the solver aims at these…
150
- targetGutterPx: 20,
151
- targetCellColumns: 89, // …and at this ASCII column count
152
- // optional: cellAspect, snapRows, pixelSnap, snapTolerancePx, aspectTolerancePct
153
- })
154
-
155
- grid.N // ASCII grid columns
156
- grid.rows // ASCII grid rows
157
- grid.u // cell width, px
158
- grid.cellH // cell height, px
159
- grid.solvedM // margin in cells
160
- grid.solvedG // gutter in cells
161
- grid.margin // margin in px (effectiveMargin includes the pixel-snap remainder)
162
- grid.gutterWidth // gutter in px
163
- grid.columnWidth // column width in px
164
-
165
- // The cell grid as drawable [x1, y1, x2, y2] line segments, with lines near
166
- // the frame edge culled.
167
- const segments = gridLineSegments(grid, 1920, 1080, { edgeCull: true })
168
-
169
- // A line height snapped to whole cell rows, so text set at it keeps its
170
- // baselines on the grid. Pick the font size as a share of the result.
171
- const lh = snapLineHeight(grid, 1080 * 0.12) // { cells: 3, px: 129.6 }
172
- const fontSize = lh.px * 0.92
173
- ```
174
-
175
- To place text by coordinate (canvas, SVG, Figma), `baselineOffset(metrics, fontSize,
176
- lineHeight)` gives the distance from the top of the line box to the first baseline, so
177
- `y = grid.rowOffset + row * grid.cellH - baselineOffset(...)` seats it on a cell row.
178
- In CSS, pair with the capsize entry point below instead.
179
-
180
- ### Capsize (`@oxide/design-system/capsize`)
181
-
182
- A [capsize](https://seek-oss.github.io/capsize/)-style React hook that trims the space
183
- above the cap height and below the baseline, so the element's box runs exactly from cap
184
- top to baseline. Anchor the trimmed box's bottom edge to a grid row and the text sits on
185
- the baseline grid — no offset arithmetic needed:
186
-
187
- ```tsx
188
- import { computeGrid, snapLineHeight } from '@oxide/design-system/grid'
189
- import { useCapsize } from '@oxide/design-system/capsize'
190
-
191
- const grid = computeGrid({ ... })
192
- const lh = snapLineHeight(grid, height * 0.12)
193
-
194
- const { styles, className } = useCapsize({
195
- fontFamily: 'suisse-intl', // or pass `metrics` for other fonts
196
- fontSize: lh.px * 0.92,
197
- lineHeight: lh.px,
198
- })
199
-
200
- // bottom-0 lands on the container edge — a grid line — and capsize makes the
201
- // element's bottom edge the baseline.
202
- <h1 className={cn('absolute bottom-0', className)} style={styles}>…</h1>
203
- ```
204
-
205
- The pure computation is also exported as `capsize(options)` (returns `styles`,
206
- `className` and the `cssText` for the trim pseudo-elements) for non-React or SSR use.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxide/design-system",
3
- "version": "6.7.0-canary.8a33f9c",
3
+ "version": "6.7.0",
4
4
  "description": "Home of reusable design assets and token for oxide internal sites",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -8,8 +8,6 @@
8
8
  "./styles/*": "./styles/*",
9
9
  "./components/*.css": "./dist/*.css",
10
10
  "./asciidoc": "./dist/components/src/asciidoc/index.js",
11
- "./capsize": "./dist/components/src/capsize/index.js",
12
- "./grid": "./dist/components/src/grid/index.js",
13
11
  "./syntax": "./dist/components/src/syntax/index.js",
14
12
  "./ui": "./dist/components/src/ui/index.js",
15
13
  "./icons": "./dist/icons/index.js",
@@ -27,10 +25,6 @@
27
25
  "color-gen:dev": "vite --config color-gen/vite.config.ts color-gen",
28
26
  "color-gen:apply": "tsx color-gen/apply-colors.ts",
29
27
  "preview:dev": "vite --config preview/vite.config.ts",
30
- "dual-grid:build": "npm --prefix plugins/dual-grid run build",
31
- "dual-grid:test": "npm --prefix plugins/dual-grid run test",
32
- "token-sync:dev": "npm --prefix plugins/token-sync run dev",
33
- "token-sync:build": "npm --prefix plugins/token-sync run build",
34
28
  "design-md": "node scripts/design-md.mjs",
35
29
  "design-md:check": "node scripts/design-md.mjs --check"
36
30
  },
@@ -1,22 +0,0 @@
1
- import { FontMetrics } from '../grid/index.js';
2
-
3
- interface CapsizeFontMetrics extends FontMetrics {
4
- capHeight: number;
5
- }
6
- declare const FONT_METRICS: Record<string, CapsizeFontMetrics>;
7
- interface CapsizeOptions {
8
- fontFamily?: string;
9
- metrics?: CapsizeFontMetrics;
10
- fontSize: number | string;
11
- lineHeight?: number | string;
12
- rootSize?: number;
13
- }
14
- interface CapsizeResult {
15
- styles: React.CSSProperties;
16
- className: string;
17
- cssText?: string;
18
- }
19
- declare function capsize({ fontFamily, metrics, fontSize, lineHeight, rootSize, }: CapsizeOptions): CapsizeResult;
20
- declare const useCapsize: (options: CapsizeOptions) => CapsizeResult;
21
-
22
- export { type CapsizeFontMetrics, type CapsizeOptions, type CapsizeResult, FONT_METRICS, capsize, useCapsize };
@@ -1,125 +0,0 @@
1
- // components/src/capsize/index.ts
2
- import { useEffect, useMemo } from "react";
3
- var FONT_METRICS = {
4
- "suisse-intl": {
5
- capHeight: 725,
6
- ascent: 986,
7
- descent: -311,
8
- lineGap: 0,
9
- unitsPerEm: 1e3
10
- }
11
- };
12
- function isRelativeValue(value) {
13
- const isPercentValue = value.endsWith("%");
14
- const isUnitlessValue = /\d$/.test(value);
15
- return isPercentValue || isUnitlessValue;
16
- }
17
- function getRelativeValue(value) {
18
- const isPercentValue = value.endsWith("%");
19
- return isPercentValue ? Number.parseInt(value.replace("%", ""), 10) / 100 : Number.parseFloat(value);
20
- }
21
- function normalizeValue(value, root, fs) {
22
- if (typeof value === "number") return value;
23
- if (value.endsWith("px")) return Number.parseFloat(value.replace("px", ""));
24
- if (value.endsWith("rem")) return root * Number.parseFloat(value.replace("rem", ""));
25
- if (isRelativeValue(value) && fs !== void 0) {
26
- return fs * getRelativeValue(value);
27
- }
28
- return Number.parseInt(value, 10);
29
- }
30
- function round(value) {
31
- return Number.parseFloat(value.toFixed(4)).toString();
32
- }
33
- function lineHeightProperties(lineHeight, rootSize = 16, fontSizePx) {
34
- if (lineHeight === void 0) return {};
35
- const lineHeightStr = typeof lineHeight === "number" ? `${lineHeight}px` : lineHeight;
36
- const lineHeightActual = isRelativeValue(lineHeightStr) ? `calc(${getRelativeValue(lineHeightStr).toString()} * var(--font-size-px))` : normalizeValue(lineHeightStr, rootSize, fontSizePx).toString();
37
- return {
38
- "--line-height-offset": `calc((((var(--line-height-scale) * var(--font-size-px)) - ${lineHeightActual}) / 2) / var(--font-size-px))`
39
- };
40
- }
41
- function capsize({
42
- fontFamily = "sans",
43
- metrics,
44
- fontSize,
45
- lineHeight,
46
- rootSize = 16
47
- }) {
48
- const fontMetrics = metrics || FONT_METRICS[fontFamily];
49
- if (!fontMetrics) {
50
- return {
51
- styles: {
52
- fontSize: typeof fontSize === "number" ? `${fontSize}px` : fontSize,
53
- lineHeight: typeof lineHeight === "number" ? `${lineHeight}px` : lineHeight
54
- },
55
- className: ""
56
- };
57
- }
58
- const { ascent, descent, lineGap, unitsPerEm, capHeight } = fontMetrics;
59
- const ascentScale = ascent / unitsPerEm;
60
- const descentScale = Math.abs(descent) / unitsPerEm;
61
- const capHeightScale = capHeight / unitsPerEm;
62
- const lineGapScale = lineGap / unitsPerEm;
63
- const lineHeightScale = (ascent + lineGap + Math.abs(descent)) / unitsPerEm;
64
- const fontSizeActual = normalizeValue(fontSize, rootSize);
65
- const lineHeightProps = lineHeightProperties(lineHeight, rootSize, fontSizeActual);
66
- const fontSizeKey = typeof fontSize === "number" ? fontSize : fontSizeActual;
67
- const className = `capsize-${fontFamily}-${Math.round(fontSizeKey)}`;
68
- const styles = {
69
- fontSize: typeof fontSize === "number" ? `${fontSize}px` : fontSize,
70
- lineHeight: typeof lineHeight === "number" ? `${lineHeight}px` : lineHeight,
71
- "--font-size-px": String(fontSizeActual),
72
- "--ascent-scale": round(ascentScale),
73
- "--descent-scale": round(descentScale),
74
- "--cap-height-scale": round(capHeightScale),
75
- "--line-gap-scale": round(lineGapScale),
76
- "--line-height-scale": round(lineHeightScale),
77
- ...lineHeightProps
78
- };
79
- return {
80
- styles,
81
- className,
82
- cssText: `
83
- .${className}::before {
84
- display: table;
85
- content: "";
86
- margin-bottom: calc(((var(--ascent-scale) - var(--cap-height-scale) + var(--line-gap-scale) / 2) - var(--line-height-offset)) * -1em);
87
- }
88
- .${className}::after {
89
- display: table;
90
- content: "";
91
- margin-top: calc(((var(--descent-scale) + var(--line-gap-scale) / 2) - var(--line-height-offset)) * -1em);
92
- }
93
- `
94
- };
95
- }
96
- var useCapsize = (options) => {
97
- const { fontFamily = "sans", metrics, fontSize, lineHeight, rootSize = 16 } = options;
98
- const result = useMemo(
99
- () => capsize({ fontFamily, metrics, fontSize, lineHeight, rootSize }),
100
- [fontFamily, metrics, fontSize, lineHeight, rootSize]
101
- );
102
- useEffect(() => {
103
- if (!result.cssText) return;
104
- const styleId = "capsize-styles";
105
- let styleElement = document.getElementById(styleId);
106
- if (!styleElement) {
107
- styleElement = document.createElement("style");
108
- styleElement.id = styleId;
109
- document.head.appendChild(styleElement);
110
- }
111
- if (!styleElement.textContent?.includes(result.className)) {
112
- styleElement.textContent += result.cssText;
113
- }
114
- }, [result.cssText, result.className]);
115
- return {
116
- styles: result.styles,
117
- className: result.className
118
- };
119
- };
120
- export {
121
- FONT_METRICS,
122
- capsize,
123
- useCapsize
124
- };
125
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../components/src/capsize/index.ts"],"sourcesContent":["/*\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, you can obtain one at https://mozilla.org/MPL/2.0/.\n *\n * Copyright Oxide Computer Company\n */\nimport { useEffect, useMemo } from 'react'\n\nimport type { FontMetrics } from '../grid'\n\n// Capsize-style leading trim: ::before/::after pseudo-elements pull the space\n// above the cap height and below the baseline out of the first and last line,\n// so the element's box runs exactly from cap top to baseline. Anchoring the\n// trimmed box's bottom edge to a grid row therefore seats the baseline on the\n// grid — pair with snapLineHeight from @oxide/design-system/grid.\n\n// Grid FontMetrics plus the cap height, which the top trim needs.\n// Generate values on https://seek-oss.github.io/capsize/\nexport interface CapsizeFontMetrics extends FontMetrics {\n capHeight: number\n}\n\nexport const FONT_METRICS: Record<string, CapsizeFontMetrics> = {\n 'suisse-intl': {\n capHeight: 725,\n ascent: 986,\n descent: -311,\n lineGap: 0,\n unitsPerEm: 1000,\n },\n}\n\nexport interface CapsizeOptions {\n // Key into FONT_METRICS; ignored when metrics is given. Unknown families\n // fall through with untrimmed font-size/line-height styles.\n fontFamily?: string\n metrics?: CapsizeFontMetrics\n fontSize: number | string\n lineHeight?: number | string\n rootSize?: number\n}\n\nexport interface CapsizeResult {\n styles: React.CSSProperties\n className: string\n // Rules for the trim pseudo-elements; useCapsize injects these, non-React\n // consumers add them to a stylesheet themselves.\n cssText?: string\n}\n\nfunction isRelativeValue(value: string) {\n const isPercentValue = value.endsWith('%')\n const isUnitlessValue = /\\d$/.test(value)\n return isPercentValue || isUnitlessValue\n}\n\nfunction getRelativeValue(value: string) {\n const isPercentValue = value.endsWith('%')\n return isPercentValue\n ? Number.parseInt(value.replace('%', ''), 10) / 100\n : Number.parseFloat(value)\n}\n\nfunction normalizeValue(value: string | number, root: number, fs?: number): number {\n if (typeof value === 'number') return value\n\n if (value.endsWith('px')) return Number.parseFloat(value.replace('px', ''))\n if (value.endsWith('rem')) return root * Number.parseFloat(value.replace('rem', ''))\n\n if (isRelativeValue(value) && fs !== undefined) {\n return fs * getRelativeValue(value)\n }\n\n return Number.parseInt(value, 10)\n}\n\nfunction round(value: number): string {\n return Number.parseFloat(value.toFixed(4)).toString()\n}\n\n// With a fixed line height the em box is centred in the line box; this is the\n// resulting half leading in em, which both trims subtract.\nfunction lineHeightProperties(\n lineHeight?: string | number,\n rootSize = 16,\n fontSizePx?: number,\n) {\n if (lineHeight === undefined) return {}\n\n const lineHeightStr = typeof lineHeight === 'number' ? `${lineHeight}px` : lineHeight\n\n const lineHeightActual = isRelativeValue(lineHeightStr)\n ? `calc(${getRelativeValue(lineHeightStr).toString()} * var(--font-size-px))`\n : normalizeValue(lineHeightStr, rootSize, fontSizePx).toString()\n\n return {\n '--line-height-offset': `calc((((var(--line-height-scale) * var(--font-size-px)) - ${lineHeightActual}) / 2) / var(--font-size-px))`,\n }\n}\n\n// The pure computation behind useCapsize.\nexport function capsize({\n fontFamily = 'sans',\n metrics,\n fontSize,\n lineHeight,\n rootSize = 16,\n}: CapsizeOptions): CapsizeResult {\n const fontMetrics = metrics || FONT_METRICS[fontFamily]\n\n if (!fontMetrics) {\n return {\n styles: {\n fontSize: typeof fontSize === 'number' ? `${fontSize}px` : fontSize,\n lineHeight: typeof lineHeight === 'number' ? `${lineHeight}px` : lineHeight,\n },\n className: '',\n }\n }\n\n const { ascent, descent, lineGap, unitsPerEm, capHeight } = fontMetrics\n\n const ascentScale = ascent / unitsPerEm\n const descentScale = Math.abs(descent) / unitsPerEm\n const capHeightScale = capHeight / unitsPerEm\n const lineGapScale = lineGap / unitsPerEm\n const lineHeightScale = (ascent + lineGap + Math.abs(descent)) / unitsPerEm\n\n const fontSizeActual = normalizeValue(fontSize, rootSize)\n\n const lineHeightProps = lineHeightProperties(lineHeight, rootSize, fontSizeActual)\n\n const fontSizeKey = typeof fontSize === 'number' ? fontSize : fontSizeActual\n const className = `capsize-${fontFamily}-${Math.round(fontSizeKey)}`\n\n const styles = {\n fontSize: typeof fontSize === 'number' ? `${fontSize}px` : fontSize,\n lineHeight: typeof lineHeight === 'number' ? `${lineHeight}px` : lineHeight,\n '--font-size-px': String(fontSizeActual),\n '--ascent-scale': round(ascentScale),\n '--descent-scale': round(descentScale),\n '--cap-height-scale': round(capHeightScale),\n '--line-gap-scale': round(lineGapScale),\n '--line-height-scale': round(lineHeightScale),\n ...lineHeightProps,\n }\n\n return {\n styles,\n className,\n cssText: `\n .${className}::before {\n display: table;\n content: \"\";\n margin-bottom: calc(((var(--ascent-scale) - var(--cap-height-scale) + var(--line-gap-scale) / 2) - var(--line-height-offset)) * -1em);\n }\n .${className}::after {\n display: table;\n content: \"\";\n margin-top: calc(((var(--descent-scale) + var(--line-gap-scale) / 2) - var(--line-height-offset)) * -1em);\n }\n `,\n }\n}\n\nexport const useCapsize = (options: CapsizeOptions): CapsizeResult => {\n const { fontFamily = 'sans', metrics, fontSize, lineHeight, rootSize = 16 } = options\n\n const result = useMemo(\n () => capsize({ fontFamily, metrics, fontSize, lineHeight, rootSize }),\n [fontFamily, metrics, fontSize, lineHeight, rootSize],\n )\n\n // Inject CSS for the trim pseudo-elements, one shared style tag, one rule\n // per class.\n useEffect(() => {\n if (!result.cssText) return\n\n const styleId = 'capsize-styles'\n let styleElement = document.getElementById(styleId) as HTMLStyleElement\n\n if (!styleElement) {\n styleElement = document.createElement('style')\n styleElement.id = styleId\n document.head.appendChild(styleElement)\n }\n\n if (!styleElement.textContent?.includes(result.className)) {\n styleElement.textContent += result.cssText\n }\n }, [result.cssText, result.className])\n\n return {\n styles: result.styles,\n className: result.className,\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,109 +0,0 @@
1
- type GridSpec = {
2
- columns: number;
3
- width: number;
4
- height: number;
5
- targetCellColumns?: number;
6
- cellAspect?: number;
7
- snapRows?: boolean;
8
- pixelSnap?: boolean;
9
- snapTolerancePx?: number;
10
- aspectTolerancePct?: number;
11
- } & ({
12
- mode: 'manual';
13
- marginCells: number;
14
- gutterCells: number;
15
- } | {
16
- mode: 'auto';
17
- targetMarginPx: number;
18
- targetGutterPx: number;
19
- });
20
- interface GridResult {
21
- N: number;
22
- u: number;
23
- solvedM: number;
24
- solvedK: number;
25
- solvedG: number;
26
- solved: boolean;
27
- offsetX: number;
28
- cellsPerGutter: number;
29
- columnWidth: number;
30
- columnCells: number;
31
- gutterWidth: number;
32
- margin: number;
33
- effectiveMargin: number;
34
- contentWidth: number;
35
- rows: number;
36
- cellH: number;
37
- rowOffset: number;
38
- hRemainder: number;
39
- vRemainder: number;
40
- actualAspect: number;
41
- deltaPct: number;
42
- warn: boolean;
43
- cellLadder: number[];
44
- cellColumnsTaken: number;
45
- }
46
- interface Solution {
47
- m: number;
48
- k: number;
49
- g: number;
50
- marginPx: number;
51
- gutterPx: number;
52
- u: number;
53
- N: number;
54
- err: number;
55
- }
56
- interface CellOption {
57
- u: number;
58
- k: number;
59
- N: number;
60
- bleed: number;
61
- }
62
- interface GridExtent {
63
- startX: number;
64
- startY: number;
65
- cols: number;
66
- rows: number;
67
- width: number;
68
- height: number;
69
- }
70
- type Segment = [number, number, number, number];
71
- declare const DEFAULT_TARGET_CELL_COLUMNS = 89;
72
- declare const DEFAULT_CELL_ASPECT = 2;
73
- declare const DEFAULT_SNAP_TOLERANCE_PX = 0.5;
74
- declare const DEFAULT_ASPECT_TOL_PCT = 5;
75
- declare const DEFAULT_EDGE_CULL_PCT = 20;
76
- declare const SOLVE_MAX_GUTTER = 8;
77
- declare const SOLVE_MAX_MARGIN = 24;
78
- declare const SOLVE_MAX_COLUMN = 48;
79
- declare const GUTTER_WEIGHT = 8;
80
- declare const COLUMNS_WEIGHT = 16;
81
- declare const LADDER_MIN_CELL = 2;
82
- declare const EDGE_CULL_EPSILON = 0.01;
83
- declare function snapUnit(value: number, pixelSnap: boolean, tolPx: number): number;
84
- declare function betterFit(a: Solution, b: Solution): boolean;
85
- declare function solveMultiples(W: number, c: number, targetMargin: number, targetGutter: number, targetN: number, pixelSnap: boolean, snapTolPx: number): Solution;
86
- declare function round2(n: number): number;
87
- declare function cellLadder(W: number, c: number, m: number, g: number, pixelSnap: boolean, snapTolPx: number): CellOption[];
88
- declare function pickCellOption(ladder: CellOption[], targetN: number): CellOption | null;
89
- declare function computeGrid(spec: GridSpec): GridResult;
90
- declare function snapLineHeight(g: GridResult, targetPx: number): {
91
- cells: number;
92
- px: number;
93
- };
94
- interface FontMetrics {
95
- ascent: number;
96
- descent: number;
97
- lineGap: number;
98
- unitsPerEm: number;
99
- }
100
- declare function baselineOffset(m: FontMetrics, fontSize: number, lineHeight: number): number;
101
- declare function gridExtent(g: GridResult): GridExtent;
102
- declare function gridLineSegments(g: GridResult, width: number, height: number, opts?: {
103
- density?: number;
104
- edgeCull?: boolean;
105
- edgeCullPct?: number;
106
- }): Segment[];
107
- declare function parseAspect(ratio: string): number;
108
-
109
- export { COLUMNS_WEIGHT, type CellOption, DEFAULT_ASPECT_TOL_PCT, DEFAULT_CELL_ASPECT, DEFAULT_EDGE_CULL_PCT, DEFAULT_SNAP_TOLERANCE_PX, DEFAULT_TARGET_CELL_COLUMNS, EDGE_CULL_EPSILON, type FontMetrics, GUTTER_WEIGHT, type GridExtent, type GridResult, type GridSpec, LADDER_MIN_CELL, SOLVE_MAX_COLUMN, SOLVE_MAX_GUTTER, SOLVE_MAX_MARGIN, type Segment, type Solution, baselineOffset, betterFit, cellLadder, computeGrid, gridExtent, gridLineSegments, parseAspect, pickCellOption, round2, snapLineHeight, snapUnit, solveMultiples };
@@ -1,278 +0,0 @@
1
- // components/src/grid/index.ts
2
- var DEFAULT_TARGET_CELL_COLUMNS = 89;
3
- var DEFAULT_CELL_ASPECT = 2;
4
- var DEFAULT_SNAP_TOLERANCE_PX = 0.5;
5
- var DEFAULT_ASPECT_TOL_PCT = 5;
6
- var DEFAULT_EDGE_CULL_PCT = 20;
7
- var SOLVE_MAX_GUTTER = 8;
8
- var SOLVE_MAX_MARGIN = 24;
9
- var SOLVE_MAX_COLUMN = 48;
10
- var GUTTER_WEIGHT = 8;
11
- var COLUMNS_WEIGHT = 16;
12
- var LADDER_MIN_CELL = 2;
13
- var EDGE_CULL_EPSILON = 0.01;
14
- function snapUnit(value, pixelSnap, tolPx) {
15
- if (!pixelSnap) return value;
16
- const snapped = Math.max(1, Math.round(value));
17
- return Math.abs(value - snapped) <= tolPx + 1e-9 ? snapped : value;
18
- }
19
- function betterFit(a, b) {
20
- if (a.err < b.err - 1e-6) return true;
21
- if (a.err > b.err + 1e-6) return false;
22
- return a.N < b.N;
23
- }
24
- function solveMultiples(W, c, targetMargin, targetGutter, targetN, pixelSnap, snapTolPx) {
25
- const relErr = (got, want) => Math.abs(got - want) / Math.max(1, want);
26
- let best = null;
27
- for (let g = 1; g <= SOLVE_MAX_GUTTER; g++) {
28
- for (let m = 0; m <= SOLVE_MAX_MARGIN; m++) {
29
- for (let k = 1; k <= SOLVE_MAX_COLUMN; k++) {
30
- const N = 2 * m + c * k + (c - 1) * g;
31
- const u = snapUnit(W / N, pixelSnap, snapTolPx);
32
- const gutterPx = g * u;
33
- const marginPx = m * u + (W - N * u) / 2;
34
- const err = GUTTER_WEIGHT * relErr(gutterPx, targetGutter) + COLUMNS_WEIGHT * relErr(N, targetN) + relErr(marginPx, targetMargin);
35
- const candidate = { m, k, g, marginPx, gutterPx, u, N, err };
36
- if (!best || betterFit(candidate, best)) best = candidate;
37
- }
38
- }
39
- }
40
- return best || {
41
- m: 3,
42
- k: 6,
43
- g: 1,
44
- marginPx: 0,
45
- gutterPx: 0,
46
- u: 0,
47
- N: 0,
48
- err: Infinity
49
- };
50
- }
51
- function round2(n) {
52
- return Math.round(n * 100) / 100;
53
- }
54
- function cellLadder(W, c, m, g, pixelSnap, snapTolPx) {
55
- const byCell = /* @__PURE__ */ new Map();
56
- for (let k = 1; k <= SOLVE_MAX_COLUMN; k++) {
57
- const N = 2 * m + c * k + (c - 1) * g;
58
- const u = snapUnit(W / N, pixelSnap, snapTolPx);
59
- if (u < LADDER_MIN_CELL) continue;
60
- const bleed = Math.abs(W - N * u);
61
- const key = round2(u);
62
- const prev = byCell.get(key);
63
- if (!prev || bleed < prev.bleed) byCell.set(key, { u, k, N, bleed });
64
- }
65
- const out = [];
66
- byCell.forEach((v) => out.push(v));
67
- return out.sort((a, b) => a.u - b.u);
68
- }
69
- function pickCellOption(ladder, targetN) {
70
- let best = null;
71
- let bestErr = Infinity;
72
- for (const opt of ladder) {
73
- const err = Math.abs(opt.N - targetN);
74
- if (err < bestErr - 1e-9 || best && err < bestErr + 1e-9 && opt.N < best.N) {
75
- bestErr = err;
76
- best = opt;
77
- }
78
- }
79
- return best;
80
- }
81
- function computeGrid(spec) {
82
- const W = spec.width;
83
- const H = spec.height;
84
- const c = Math.max(1, Math.round(spec.columns));
85
- const a = spec.cellAspect !== void 0 ? spec.cellAspect : DEFAULT_CELL_ASPECT;
86
- const snapRows = spec.snapRows !== void 0 ? spec.snapRows : true;
87
- const pixelSnap = spec.pixelSnap === true;
88
- const snapTol = spec.snapTolerancePx !== void 0 ? spec.snapTolerancePx : DEFAULT_SNAP_TOLERANCE_PX;
89
- const aspectTolPct = spec.aspectTolerancePct !== void 0 ? spec.aspectTolerancePct : DEFAULT_ASPECT_TOL_PCT;
90
- const targetN = Math.max(
91
- 1,
92
- Math.round(
93
- spec.targetCellColumns !== void 0 ? spec.targetCellColumns : DEFAULT_TARGET_CELL_COLUMNS
94
- )
95
- );
96
- let solved = null;
97
- let gm;
98
- let m;
99
- if (spec.mode === "auto") {
100
- solved = solveMultiples(
101
- W,
102
- c,
103
- Math.max(0, spec.targetMarginPx),
104
- Math.max(1, spec.targetGutterPx),
105
- targetN,
106
- pixelSnap,
107
- snapTol
108
- );
109
- gm = solved.g;
110
- m = solved.m;
111
- } else {
112
- gm = Math.max(1, Math.round(spec.gutterCells));
113
- m = Math.max(0, Math.round(spec.marginCells));
114
- }
115
- const ladder = solved ? [] : cellLadder(W, c, m, gm, pixelSnap, snapTol);
116
- const chosen = solved ? null : pickCellOption(ladder, targetN);
117
- const k = solved ? solved.k : chosen ? chosen.k : 1;
118
- const N = 2 * m + c * k + (c - 1) * gm;
119
- const uExact = W / N;
120
- const u = snapUnit(uExact, pixelSnap, snapTol);
121
- const columnWidth = k * u;
122
- const gutterWidth = gm * u;
123
- const margin = m * u;
124
- const columnCells = k;
125
- const contentWidth = c * columnWidth + (c - 1) * gutterWidth;
126
- const hRemainder = W - N * u;
127
- const effectiveMargin = margin + hRemainder / 2;
128
- const offsetX = 0;
129
- let rows;
130
- let cellH;
131
- if (snapRows) {
132
- rows = Math.max(1, Math.round(H / (a * uExact)));
133
- const band = pixelSnap ? Math.min(50, aspectTolPct) / 100 : 0;
134
- if (band > 0) {
135
- const lo = Math.max(1, Math.ceil(H / (a * u * (1 + band))));
136
- const hi = Math.max(lo, Math.floor(H / (a * u * (1 - band))));
137
- let bestR = 0;
138
- let bestErr = Infinity;
139
- for (let r = lo; r <= hi; r++) {
140
- const snapped = Math.max(1, Math.round(H / r));
141
- if (Math.abs(H / r - snapped) > snapTol + 1e-9) continue;
142
- const err = Math.abs(snapped / u / a - 1);
143
- if (err > band + 1e-9) continue;
144
- if (err < bestErr - 1e-12 || err < bestErr + 1e-12 && Math.abs(r - rows) < Math.abs(bestR - rows)) {
145
- bestErr = err;
146
- bestR = r;
147
- }
148
- }
149
- if (bestR) rows = bestR;
150
- }
151
- cellH = snapUnit(H / rows, pixelSnap, snapTol);
152
- } else {
153
- cellH = snapUnit(a * u, pixelSnap, snapTol);
154
- rows = Math.max(1, Math.ceil(H / cellH - 1e-9));
155
- }
156
- const vRemainder = H - rows * cellH;
157
- const rowOffset = vRemainder / 2;
158
- const actualAspect = cellH / u;
159
- const deltaPct = (actualAspect / a - 1) * 100;
160
- return {
161
- N,
162
- u,
163
- solvedM: m,
164
- solvedK: k,
165
- solvedG: gm,
166
- solved: !!solved,
167
- offsetX,
168
- cellsPerGutter: gm,
169
- columnWidth,
170
- columnCells,
171
- gutterWidth,
172
- margin,
173
- effectiveMargin,
174
- contentWidth,
175
- rows,
176
- cellH,
177
- rowOffset,
178
- hRemainder,
179
- vRemainder,
180
- actualAspect,
181
- deltaPct,
182
- warn: Math.abs(deltaPct) > 1,
183
- // Ascending in N, which is *descending* in cell size — the ladder is built
184
- // u-ascending, so this reverses it.
185
- cellLadder: ladder.map((o) => o.N).sort((x, y) => x - y),
186
- // The count actually taken: the nearest ladder rung in manual mode, the
187
- // solver's N in auto.
188
- cellColumnsTaken: solved ? N : chosen ? chosen.N : targetN
189
- };
190
- }
191
- function snapLineHeight(g, targetPx) {
192
- const cells = Math.max(1, Math.round(targetPx / g.cellH));
193
- return { cells, px: cells * g.cellH };
194
- }
195
- function baselineOffset(m, fontSize, lineHeight) {
196
- const emBox = fontSize * (m.ascent + m.lineGap + Math.abs(m.descent)) / m.unitsPerEm;
197
- const halfLeading = (lineHeight - emBox) / 2;
198
- return halfLeading + fontSize * (m.ascent + m.lineGap / 2) / m.unitsPerEm;
199
- }
200
- function gridExtent(g) {
201
- const extraCols = g.hRemainder > 0 ? Math.ceil(g.hRemainder / 2 / g.u) : 0;
202
- const extraRows = g.vRemainder > 0 ? Math.ceil(g.vRemainder / 2 / g.cellH) : 0;
203
- const startX = g.hRemainder / 2 - extraCols * g.u;
204
- const startY = g.vRemainder / 2 - extraRows * g.cellH;
205
- const cols = g.N + 2 * extraCols;
206
- const rows = g.rows + 2 * extraRows;
207
- return {
208
- startX,
209
- startY,
210
- cols,
211
- rows,
212
- width: cols * g.u,
213
- height: rows * g.cellH
214
- };
215
- }
216
- function gridLineSegments(g, width, height, opts) {
217
- const o = opts || {};
218
- const d = o.density !== void 0 ? o.density : 1;
219
- const cullOn = o.edgeCull !== void 0 ? o.edgeCull : true;
220
- const cullPct = o.edgeCullPct !== void 0 ? o.edgeCullPct : DEFAULT_EDGE_CULL_PCT;
221
- const cull = Math.min(50, cullPct) / 100;
222
- const ext = gridExtent(g);
223
- const right = ext.startX + ext.width;
224
- const bottom = ext.startY + ext.height;
225
- const stepX = g.u / d;
226
- const stepY = g.cellH / d;
227
- const tolX = stepX * cull;
228
- const tolY = stepY * cull;
229
- const lastCol = Math.floor(ext.cols * d + 1e-9);
230
- const lastRow = Math.floor(ext.rows * d + 1e-9);
231
- const segments = [];
232
- for (let i = 0; i <= lastCol; i++) {
233
- const x = ext.startX + i * stepX;
234
- if (x < EDGE_CULL_EPSILON || x > width - EDGE_CULL_EPSILON) continue;
235
- if (cullOn && (x < tolX || width - x < tolX)) continue;
236
- segments.push([x, ext.startY, x, bottom]);
237
- }
238
- for (let j = 0; j <= lastRow; j++) {
239
- const y = ext.startY + j * stepY;
240
- if (y < EDGE_CULL_EPSILON || y > height - EDGE_CULL_EPSILON) continue;
241
- if (cullOn && (y < tolY || height - y < tolY)) continue;
242
- segments.push([ext.startX, y, right, y]);
243
- }
244
- return segments;
245
- }
246
- function parseAspect(ratio) {
247
- const parts = ratio.split(":");
248
- const w = Number(parts[0]);
249
- const h = Number(parts[1]);
250
- return w > 0 && h > 0 ? w / h : 16 / 9;
251
- }
252
- export {
253
- COLUMNS_WEIGHT,
254
- DEFAULT_ASPECT_TOL_PCT,
255
- DEFAULT_CELL_ASPECT,
256
- DEFAULT_EDGE_CULL_PCT,
257
- DEFAULT_SNAP_TOLERANCE_PX,
258
- DEFAULT_TARGET_CELL_COLUMNS,
259
- EDGE_CULL_EPSILON,
260
- GUTTER_WEIGHT,
261
- LADDER_MIN_CELL,
262
- SOLVE_MAX_COLUMN,
263
- SOLVE_MAX_GUTTER,
264
- SOLVE_MAX_MARGIN,
265
- baselineOffset,
266
- betterFit,
267
- cellLadder,
268
- computeGrid,
269
- gridExtent,
270
- gridLineSegments,
271
- parseAspect,
272
- pickCellOption,
273
- round2,
274
- snapLineHeight,
275
- snapUnit,
276
- solveMultiples
277
- };
278
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../components/src/grid/index.ts"],"sourcesContent":["/*\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, you can obtain one at https://mozilla.org/MPL/2.0/.\n *\n * Copyright Oxide Computer Company\n */\n\n// Dual-grid math: overlays an aligned column grid and ASCII cell grid on a\n// frame. Margin, column and gutter widths are whole multiples of the cell\n// width `u`, so every edge lands on a cell line:\n//\n// N = 2m + c·k + (c−1)·g (cells across the frame)\n// u = W / N (cell size = base unit)\n// margin = m·u column = k·u gutter = g·u\n//\n// See plugins/dual-grid/README.md for the derivation, solver weight\n// comparisons and measured error tables.\n//\n// This module is consumed both as `@oxide/design-system/grid` and bundled\n// directly into the dual-grid Figma plugin. Keep it dependency-free, with no\n// Figma types and no syntax newer than ES2018 (the plugin engine's ceiling).\n\nexport type GridSpec = {\n columns: number // c — layout column count\n width: number // frame width, px\n height: number // frame height, px\n // Rough target for the number of ASCII cell columns N. Only certain counts\n // are reachable (N steps by c with m and g fixed), so the nearest is taken.\n targetCellColumns?: number\n cellAspect?: number // a — cell height as a multiple of cell width\n // Snap the row count so rows fill the height exactly; off, the cell aspect\n // is kept exact and the last row runs off the bottom edge instead.\n snapRows?: boolean\n // Round the cell size to whole pixels when within snapTolerancePx. The\n // leftover (W − N·u) is split between the margins. Off, the cell divides\n // the frame exactly and there is no bleed.\n pixelSnap?: boolean\n // Maximum rounding distance per cell in pixels. Total horizontal bleed is\n // bounded by N times this tolerance. 0 disables snapping; 0.5 always snaps.\n snapTolerancePx?: number\n // With snapRows and pixelSnap on, allow this percentage of aspect deviation\n // to find an integer row height. 0 keeps the original row count.\n aspectTolerancePct?: number\n} & (\n | {\n // Margin and gutter are given directly as cell counts; column width is\n // derived from targetCellColumns via the ladder.\n mode: 'manual'\n marginCells: number // m — margin width in cells\n gutterCells: number // g — cells per gutter\n }\n | {\n // The solver finds the whole-cell multiples (m, k, g) whose margin,\n // gutter and column count come closest to the pixel targets.\n mode: 'auto'\n targetMarginPx: number\n targetGutterPx: number\n }\n)\n\nexport interface GridResult {\n N: number // whole cells that fit across the frame\n u: number // cell size = base unit\n solvedM: number // multiples actually used (from the spec or the solver)\n solvedK: number\n solvedG: number\n solved: boolean // whether the solver chose them\n offsetX: number // phase of the cell grid, so x = margin lands on a boundary\n cellsPerGutter: number\n columnWidth: number\n columnCells: number // columnWidth in whole cells (= k)\n gutterWidth: number\n margin: number\n effectiveMargin: number\n contentWidth: number\n rows: number\n cellH: number\n rowOffset: number\n hRemainder: number // leftover width from snapping u, split between the margins\n vRemainder: number // leftover height — split top/bottom (snapRows) or a partial row at the bottom\n actualAspect: number\n deltaPct: number\n warn: boolean\n cellLadder: number[] // reachable cell-column counts, ascending (manual only)\n cellColumnsTaken: number // the rung actually taken (manual), or N (auto)\n}\n\nexport interface Solution {\n m: number\n k: number\n g: number\n marginPx: number\n gutterPx: number\n u: number\n N: number\n err: number\n}\n\nexport interface CellOption {\n u: number // exact cell size\n k: number // cells per column that produces it\n N: number\n bleed: number // px left over after rounding u — padding that lands in the margins\n}\n\n// The full-bleed extent of the cell grid: the N × rows grid plus enough whole\n// cells on every side to cover the snap's remainder margins, so the grid\n// fills the frame edge to edge. Whole units keep everything aligned; the\n// overhang is clipped by the frame.\nexport interface GridExtent {\n startX: number\n startY: number\n cols: number\n rows: number\n width: number\n height: number\n}\n\nexport type Segment = [number, number, number, number] // x1, y1, x2, y2\n\n// 89 = 2(3) + 12(6) + 11(1), i.e. k=6 — an exact rung for the default margin,\n// gutter and column count at 1920 wide, giving a 21.57px cell, 25 rows and a\n// 129px column.\nexport const DEFAULT_TARGET_CELL_COLUMNS = 89\nexport const DEFAULT_CELL_ASPECT = 2\n// Half a pixel accepts any rounding distance. Lower tolerances preserve more\n// fractional sizes and limit bleed; 0.1 caps it at N/10 pixels.\nexport const DEFAULT_SNAP_TOLERANCE_PX = 0.5\n// Allow 5% aspect deviation when searching for an integer row height.\n// For example, 24 rows of 45px fill a 1080px frame exactly.\nexport const DEFAULT_ASPECT_TOL_PCT = 5\n// 20% of a step: wide enough to catch the few-px inset the pixel snap leaves\n// (3px on a 22px cell is 13.6%), narrow enough that a real interior line — a\n// full step from the edge — can never be caught.\nexport const DEFAULT_EDGE_CULL_PCT = 20\n\n// Search all ~10k combinations on each edit to find the best match within these bounds.\nexport const SOLVE_MAX_GUTTER = 8\nexport const SOLVE_MAX_MARGIN = 24\nexport const SOLVE_MAX_COLUMN = 48\n// Prioritise column count to preserve cell density. Gutter width is g*W/N with\n// integer g, so the two targets may conflict. Report gutter mismatches rather\n// than substantially changing N. Margin has weight 1 and is quantised in\n// cell-width increments.\nexport const GUTTER_WEIGHT = 8\nexport const COLUMNS_WEIGHT = 16\n\n// Cell sizes below this are not a usable design grid, and enumerating them only\n// crowds the options.\nexport const LADDER_MIN_CELL = 2\n\n// Lines on or beyond the frame boundary (to within this epsilon) are always\n// culled as doubled frame edges, whatever the edge-cull options say.\nexport const EDGE_CULL_EPSILON = 0.01\n\n// Round to whole pixels only within tolerance. computeGrid, the solver and the\n// ladder must all use this function so previews and generated grids agree.\nexport function snapUnit(value: number, pixelSnap: boolean, tolPx: number): number {\n if (!pixelSnap) return value\n const snapped = Math.max(1, Math.round(value))\n // The epsilon keeps an exactly-on-tolerance value (and a tolerance of 0.5,\n // where |value − round(value)| can equal 0.5 up to float error) snapping.\n return Math.abs(value - snapped) <= tolPx + 1e-9 ? snapped : value\n}\n\n// Treat errors within 1e-6 as ties and prefer the coarser grid.\nexport function betterFit(a: Solution, b: Solution): boolean {\n if (a.err < b.err - 1e-6) return true\n if (a.err > b.err + 1e-6) return false\n return a.N < b.N\n}\n\n// Finds the whole-cell multiples (m, k, g) whose margin, gutter and column count\n// come closest to the three requested values.\n//\n// Whole-cell multiples preserve alignment. Inputs are weighted targets, not\n// hard constraints, so a grid is returned even when no exact match exists.\nexport function solveMultiples(\n W: number,\n c: number,\n targetMargin: number,\n targetGutter: number,\n targetN: number,\n pixelSnap: boolean,\n snapTolPx: number,\n): Solution {\n const relErr = (got: number, want: number) => Math.abs(got - want) / Math.max(1, want)\n\n let best: Solution | null = null\n\n for (let g = 1; g <= SOLVE_MAX_GUTTER; g++) {\n for (let m = 0; m <= SOLVE_MAX_MARGIN; m++) {\n for (let k = 1; k <= SOLVE_MAX_COLUMN; k++) {\n const N = 2 * m + c * k + (c - 1) * g\n // Must snap exactly as computeGrid will, or the solved fit drifts once\n // the pixel snap rounds the cell size.\n const u = snapUnit(W / N, pixelSnap, snapTolPx)\n\n // Include half the rounding remainder in the margin to account for centring.\n const gutterPx = g * u\n const marginPx = m * u + (W - N * u) / 2\n\n const err =\n GUTTER_WEIGHT * relErr(gutterPx, targetGutter) +\n COLUMNS_WEIGHT * relErr(N, targetN) +\n relErr(marginPx, targetMargin)\n const candidate: Solution = { m, k, g, marginPx, gutterPx, u, N, err }\n\n // Prefer the closest fit, then the coarsest grid on a tie.\n if (!best || betterFit(candidate, best)) best = candidate\n }\n }\n }\n\n return (\n best || {\n m: 3,\n k: 6,\n g: 1,\n marginPx: 0,\n gutterPx: 0,\n u: 0,\n N: 0,\n err: Infinity,\n }\n )\n}\n\n// Two decimals is the resolution at which ladder rungs are considered distinct\n// (it is also the resolution the plugin panel displays).\nexport function round2(n: number): number {\n return Math.round(n * 100) / 100\n}\n\n// The manual-mode cell ladder. With c, m and g fixed, k is the only free\n// variable in N = 2m + ck + (c−1)g. Possible counts step by c, giving a\n// discrete set of cell sizes u = W/N. Aspect deviation is reported, not used\n// to reject sizes.\nexport function cellLadder(\n W: number,\n c: number,\n m: number,\n g: number,\n pixelSnap: boolean,\n snapTolPx: number,\n): CellOption[] {\n const byCell = new Map<number, CellOption>()\n for (let k = 1; k <= SOLVE_MAX_COLUMN; k++) {\n const N = 2 * m + c * k + (c - 1) * g\n const u = snapUnit(W / N, pixelSnap, snapTolPx)\n if (u < LADDER_MIN_CELL) continue\n\n const bleed = Math.abs(W - N * u)\n // Rounding u to whole pixels collapses several adjacent k onto the same cell\n // size; keep whichever leaves the least bleed, since that is the one that\n // fills the frame most cleanly.\n const key = round2(u)\n const prev = byCell.get(key)\n if (!prev || bleed < prev.bleed) byCell.set(key, { u, k, N, bleed })\n }\n const out: CellOption[] = []\n byCell.forEach((v) => out.push(v))\n return out.sort((a, b) => a.u - b.u)\n}\n\n// Nearest available column count. Ties prefer fewer columns, matching the solver.\nexport function pickCellOption(ladder: CellOption[], targetN: number): CellOption | null {\n let best: CellOption | null = null\n let bestErr = Infinity\n for (const opt of ladder) {\n const err = Math.abs(opt.N - targetN)\n if (err < bestErr - 1e-9 || (best && err < bestErr + 1e-9 && opt.N < best.N)) {\n bestErr = err\n best = opt\n }\n }\n return best\n}\n\n// The mathematical core. Dimensions are measured in cells; cell size is\n// derived from the frame width. All pixel sizes are reported back in the\n// result.\nexport function computeGrid(spec: GridSpec): GridResult {\n const W = spec.width\n const H = spec.height\n const c = Math.max(1, Math.round(spec.columns))\n const a = spec.cellAspect !== undefined ? spec.cellAspect : DEFAULT_CELL_ASPECT\n const snapRows = spec.snapRows !== undefined ? spec.snapRows : true\n const pixelSnap = spec.pixelSnap === true\n const snapTol =\n spec.snapTolerancePx !== undefined ? spec.snapTolerancePx : DEFAULT_SNAP_TOLERANCE_PX\n // The aspect band as a share, capped at 50%. 0 disables the row-count search.\n const aspectTolPct =\n spec.aspectTolerancePct !== undefined ? spec.aspectTolerancePct : DEFAULT_ASPECT_TOL_PCT\n const targetN = Math.max(\n 1,\n Math.round(\n spec.targetCellColumns !== undefined\n ? spec.targetCellColumns\n : DEFAULT_TARGET_CELL_COLUMNS,\n ),\n )\n\n // In auto mode the multiples come from the solver rather than the spec.\n // Manual: the gutter sets the scale, the margin is measured against it, and the\n // column width is derived. Rounding keeps every part a whole number of cells,\n // which is what holds the two grids in phase.\n let solved: Solution | null = null\n let gm: number\n let m: number\n if (spec.mode === 'auto') {\n solved = solveMultiples(\n W,\n c,\n Math.max(0, spec.targetMarginPx),\n Math.max(1, spec.targetGutterPx),\n targetN,\n pixelSnap,\n snapTol,\n )\n gm = solved.g\n m = solved.m\n } else {\n gm = Math.max(1, Math.round(spec.gutterCells))\n m = Math.max(0, Math.round(spec.marginCells))\n }\n // Manual: the column count is picked straight off the ladder. The ladder depends\n // on c, m, g, the pixel snap and its tolerance, so it is rebuilt whenever any of\n // those move and the request is re-snapped by *count* — 197 cells stays ≈197\n // across a margin change rather than sliding to whatever sits at the same\n // ladder index.\n const ladder = solved ? [] : cellLadder(W, c, m, gm, pixelSnap, snapTol)\n const chosen = solved ? null : pickCellOption(ladder, targetN)\n const k = solved ? solved.k : chosen ? chosen.k : 1\n\n const N = 2 * m + c * k + (c - 1) * gm\n const uExact = W / N\n const u = snapUnit(uExact, pixelSnap, snapTol)\n\n const columnWidth = k * u\n const gutterWidth = gm * u\n const margin = m * u\n const columnCells = k\n const contentWidth = c * columnWidth + (c - 1) * gutterWidth\n\n // Rounding u leaves a remainder; split evenly it shifts both grids by the same\n // amount, so they stay in phase.\n const hRemainder = W - N * u\n const effectiveMargin = margin + hRemainder / 2\n const offsetX = 0 // centring handles the phase; no offset needed\n\n let rows: number\n let cellH: number\n if (snapRows) {\n // Counted off the exact cell, not the snapped one, so snapping u to a whole\n // pixel cannot flip the row count the fractional grid chose (at the borderline\n // it otherwise does: u 22.07 → 24 rows, u 22 → 25).\n rows = Math.max(1, Math.round(H / (a * uExact)))\n // Search row counts within the aspect tolerance for a height that snaps.\n // Prefer the closest aspect ratio; a divisor of H gives an exact vertical fit.\n const band = pixelSnap ? Math.min(50, aspectTolPct) / 100 : 0\n if (band > 0) {\n const lo = Math.max(1, Math.ceil(H / (a * u * (1 + band))))\n const hi = Math.max(lo, Math.floor(H / (a * u * (1 - band))))\n let bestR = 0\n let bestErr = Infinity\n for (let r = lo; r <= hi; r++) {\n const snapped = Math.max(1, Math.round(H / r))\n if (Math.abs(H / r - snapped) > snapTol + 1e-9) continue\n const err = Math.abs(snapped / u / a - 1)\n if (err > band + 1e-9) continue\n // Closest to the aspect ask wins; ties go to the natural row count.\n if (\n err < bestErr - 1e-12 ||\n (err < bestErr + 1e-12 && Math.abs(r - rows) < Math.abs(bestR - rows))\n ) {\n bestErr = err\n bestR = r\n }\n }\n if (bestR) rows = bestR\n }\n // H/rows divides H exactly; the whole-pixel snap trades that for a small\n // remainder, split top and bottom like the horizontal one.\n cellH = snapUnit(H / rows, pixelSnap, snapTol)\n } else {\n // Keep the aspect exact and let the last row run off the bottom edge rather\n // than stopping the grid short — the overhang is clipped by the frame, the\n // same way the side edges already bleed. Snapping is the opposite trade:\n // exact fit, approximate aspect.\n cellH = snapUnit(a * u, pixelSnap, snapTol)\n rows = Math.max(1, Math.ceil(H / cellH - 1e-9))\n }\n const vRemainder = H - rows * cellH\n // Same trick vertically: centre the leftover so the rows stay in phase.\n const rowOffset = vRemainder / 2\n\n const actualAspect = cellH / u\n const deltaPct = (actualAspect / a - 1) * 100\n\n return {\n N,\n u,\n solvedM: m,\n solvedK: k,\n solvedG: gm,\n solved: !!solved,\n offsetX,\n cellsPerGutter: gm,\n columnWidth,\n columnCells,\n gutterWidth,\n margin,\n effectiveMargin,\n contentWidth,\n rows,\n cellH,\n rowOffset,\n hRemainder,\n vRemainder,\n actualAspect,\n deltaPct,\n warn: Math.abs(deltaPct) > 1,\n // Ascending in N, which is *descending* in cell size — the ladder is built\n // u-ascending, so this reverses it.\n cellLadder: ladder.map((o) => o.N).sort((x, y) => x - y),\n // The count actually taken: the nearest ladder rung in manual mode, the\n // solver's N in auto.\n cellColumnsTaken: solved ? N : chosen ? chosen.N : targetN,\n }\n}\n\n// Snaps a target line height to a whole number of cell rows. Text set at the\n// returned height advances by whole cells, so with the first baseline seated\n// on a cell line every following baseline lands on the grid too. Font size is\n// the consumer's choice — typically a share of the line height.\nexport function snapLineHeight(\n g: GridResult,\n targetPx: number,\n): { cells: number; px: number } {\n const cells = Math.max(1, Math.round(targetPx / g.cellH))\n return { cells, px: cells * g.cellH }\n}\n\n// Vertical font metrics in font units, as reported by capsize or fonttools;\n// descent is negative.\nexport interface FontMetrics {\n ascent: number\n descent: number\n lineGap: number\n unitsPerEm: number\n}\n\n// Distance from the top of the line box to the first baseline. With a fixed\n// line height, renderers centre the font's em box in the line box (half\n// leading), so the baseline sits one ascent (plus half the line gap) below\n// that. To seat a text block's first baseline on cell row `r`:\n//\n// y = g.rowOffset + r * g.cellH − baselineOffset(m, fontSize, lineHeight)\nexport function baselineOffset(\n m: FontMetrics,\n fontSize: number,\n lineHeight: number,\n): number {\n const emBox = (fontSize * (m.ascent + m.lineGap + Math.abs(m.descent))) / m.unitsPerEm\n const halfLeading = (lineHeight - emBox) / 2\n return halfLeading + (fontSize * (m.ascent + m.lineGap / 2)) / m.unitsPerEm\n}\n\n// Extends the grid by whole cells past its own bounds so the rounding remainder\n// at the frame edges is covered too; the frame clips the overhang.\nexport function gridExtent(g: GridResult): GridExtent {\n const extraCols = g.hRemainder > 0 ? Math.ceil(g.hRemainder / 2 / g.u) : 0\n const extraRows = g.vRemainder > 0 ? Math.ceil(g.vRemainder / 2 / g.cellH) : 0\n const startX = g.hRemainder / 2 - extraCols * g.u\n const startY = g.vRemainder / 2 - extraRows * g.cellH\n const cols = g.N + 2 * extraCols\n const rows = g.rows + 2 * extraRows\n return {\n startX,\n startY,\n cols,\n rows,\n width: cols * g.u,\n height: rows * g.cellH,\n }\n}\n\n// The cell grid as drawable line segments over the full-bleed extent. Density\n// subdivides (2, 3) or coarsens (0.5, 0.25) the cell. Lines on or beyond the\n// frame boundary are always omitted; edge cull additionally removes interior\n// lines within edgeCullPct of a step from the boundary, to avoid darkening the\n// frame edge. The percentage is capped at half a step so it can never reach\n// two adjacent lines.\nexport function gridLineSegments(\n g: GridResult,\n width: number,\n height: number,\n opts?: { density?: number; edgeCull?: boolean; edgeCullPct?: number },\n): Segment[] {\n const o = opts || {}\n const d = o.density !== undefined ? o.density : 1\n const cullOn = o.edgeCull !== undefined ? o.edgeCull : true\n const cullPct = o.edgeCullPct !== undefined ? o.edgeCullPct : DEFAULT_EDGE_CULL_PCT\n const cull = Math.min(50, cullPct) / 100\n\n const ext = gridExtent(g)\n const right = ext.startX + ext.width\n const bottom = ext.startY + ext.height\n\n const stepX = g.u / d\n const stepY = g.cellH / d\n\n const tolX = stepX * cull\n const tolY = stepY * cull\n\n // Coarse densities can leave a fractional line count; the loop floor just\n // stops at the last coarse line inside the extent.\n const lastCol = Math.floor(ext.cols * d + 1e-9)\n const lastRow = Math.floor(ext.rows * d + 1e-9)\n\n const segments: Segment[] = []\n for (let i = 0; i <= lastCol; i++) {\n const x = ext.startX + i * stepX\n if (x < EDGE_CULL_EPSILON || x > width - EDGE_CULL_EPSILON) continue\n if (cullOn && (x < tolX || width - x < tolX)) continue\n segments.push([x, ext.startY, x, bottom])\n }\n for (let j = 0; j <= lastRow; j++) {\n const y = ext.startY + j * stepY\n if (y < EDGE_CULL_EPSILON || y > height - EDGE_CULL_EPSILON) continue\n if (cullOn && (y < tolY || height - y < tolY)) continue\n segments.push([ext.startX, y, right, y])\n }\n return segments\n}\n\nexport function parseAspect(ratio: string): number {\n const parts = ratio.split(':')\n const w = Number(parts[0])\n const h = Number(parts[1])\n return w > 0 && h > 0 ? w / h : 16 / 9\n}\n"],"mappings":"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","names":[]}