@dataverse-kit/surface-kit 0.4.1 → 0.5.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 +29 -0
- package/dist/core.cjs +38 -2
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +47 -1
- package/dist/core.d.ts +47 -1
- package/dist/core.mjs +33 -1
- package/dist/core.mjs.map +1 -1
- package/dist/index.cjs +38 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +34 -1
- package/dist/index.mjs.map +1 -1
- package/dist/v9.cjs +1283 -153
- package/dist/v9.cjs.map +1 -1
- package/dist/v9.d.cts +213 -10
- package/dist/v9.d.ts +213 -10
- package/dist/v9.mjs +1284 -144
- package/dist/v9.mjs.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -55,5 +55,34 @@ export function AccountForm() {
|
|
|
55
55
|
| `fromFormDefinition` | Adapter that builds a surface tree from a (duck-typed) form definition. |
|
|
56
56
|
| `createDynamicsTheme` | Canonical Dynamics token theme factory. |
|
|
57
57
|
| `./core`, `./responsive` (subpaths) | Pure-TS sizing scalars and breakpoint/style builders — framework-agnostic, shared with the export-engine codegen. |
|
|
58
|
+
| `./v9`: `CommandSurface look="ce"` | The Dynamics command bar card: back / pop-out, abutting commands (16px icons), menu and split commands, ⋮ overflow, outlined far items (Share). Every command within 0.2px of the org. Pass 16px icons for commands, 20px for far items; overflow folding is explicit (`overflowItems`). |
|
|
59
|
+
| `./v9`: `FormHeader` + `PivotSurface look="ce"` | The Dynamics record header (avatar, title + status, entity · form switcher, header fields, owner, expand) inside the header card with the tab strip; the selected tab's content renders below the card. `PivotSurface`'s ce look is opt-in (`look="ce"`), and it needs a width-constrained parent (`minWidth: 0` on a grid/flex item) so a long header truncates inside the card. Measured against the org to 0.1px at full width. |
|
|
60
|
+
| `./v9`: `FieldRow`, `TabColumns`, `FormSurface look="ce"` | Fluent v9 layouts for forms that look identical to Dynamics CE. Under `look="ce"` they follow the model-driven form measured on a live org: 184px label column, 44/48px rows, the label moving above the control on narrow rows, and tab columns by declared share with a 340px floor. Pair with `dynamicsCeTheme` and stock Fluent controls using `appearance="filled-darker"`. Evidence: `apps/ce-parity/reference/`. |
|
|
61
|
+
|
|
62
|
+
### Dynamics-identical form (v9)
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { FluentProvider, Input } from '@fluentui/react-components';
|
|
66
|
+
import { FormSurface, TabColumns, Section, FieldRow, dynamicsCeTheme } from '@dataverse-kit/surface-kit/v9';
|
|
67
|
+
|
|
68
|
+
<FluentProvider theme={dynamicsCeTheme}>
|
|
69
|
+
<FormSurface look="ce">
|
|
70
|
+
<TabColumns widths={[33, 42, 25]}>
|
|
71
|
+
<Section title="ACCOUNT INFORMATION">
|
|
72
|
+
<FieldRow label="Account Name" htmlFor="name" required>
|
|
73
|
+
<Input id="name" appearance="filled-darker" placeholder="---" />
|
|
74
|
+
</FieldRow>
|
|
75
|
+
</Section>
|
|
76
|
+
<Section>{/* … */}</Section>
|
|
77
|
+
<Section>{/* … */}</Section>
|
|
78
|
+
</TabColumns>
|
|
79
|
+
</FormSurface>
|
|
80
|
+
</FluentProvider>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
> Put `FieldRow`s in a `Section`: under `look="ce"` it is the measured Dynamics section (shadow4
|
|
84
|
+
> card, 8px radius, title as given, gap-free body that pulls rows up so the first sits at 8px). For a
|
|
85
|
+
> multi-column section pass `internalColumns={2}` and one child per column (equal columns, 21px
|
|
86
|
+
> gutter). Verified against the org in a real browser: `apps/ce-parity` *Tab layouts* stories.
|
|
58
87
|
|
|
59
88
|
> See [`CLAUDE.md`](./CLAUDE.md) for the full architecture / contributor reference.
|
package/dist/core.cjs
CHANGED
|
@@ -21,6 +21,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var core_exports = {};
|
|
22
22
|
__export(core_exports, {
|
|
23
23
|
CALLOUT_DEFAULT_WIDTH: () => CALLOUT_DEFAULT_WIDTH,
|
|
24
|
+
CE_LABEL_ABOVE_BELOW_ROW_WIDTH: () => CE_LABEL_ABOVE_BELOW_ROW_WIDTH,
|
|
25
|
+
CE_TAB_COLUMN_MIN_WIDTH: () => CE_TAB_COLUMN_MIN_WIDTH,
|
|
24
26
|
DIALOG_SIZE_PX: () => DIALOG_SIZE_PX,
|
|
25
27
|
MODAL_AUTOSIZE_MIN_WIDTH: () => MODAL_AUTOSIZE_MIN_WIDTH,
|
|
26
28
|
PANEL_DEFAULT_WIDTH: () => PANEL_DEFAULT_WIDTH,
|
|
@@ -31,8 +33,10 @@ __export(core_exports, {
|
|
|
31
33
|
dialogWidthCss: () => dialogWidthCss,
|
|
32
34
|
dialogWidthPx: () => dialogWidthPx,
|
|
33
35
|
gridTemplateForColumns: () => gridTemplateForColumns,
|
|
36
|
+
normalizeColumnShares: () => normalizeColumnShares,
|
|
34
37
|
panelCustomWidthPx: () => panelCustomWidthPx,
|
|
35
|
-
panelKindFromWidth: () => panelKindFromWidth
|
|
38
|
+
panelKindFromWidth: () => panelKindFromWidth,
|
|
39
|
+
resolveTabColumnWidths: () => resolveTabColumnWidths
|
|
36
40
|
});
|
|
37
41
|
module.exports = __toCommonJS(core_exports);
|
|
38
42
|
|
|
@@ -81,9 +85,39 @@ function clampSpan(span, columns) {
|
|
|
81
85
|
if (!Number.isFinite(span)) return 1;
|
|
82
86
|
return Math.max(1, Math.min(Math.round(span), columns));
|
|
83
87
|
}
|
|
88
|
+
|
|
89
|
+
// src/core/ceLayout.ts
|
|
90
|
+
var CE_TAB_COLUMN_MIN_WIDTH = 340;
|
|
91
|
+
function normalizeColumnShares(shares) {
|
|
92
|
+
const clean = shares.map((s) => Number.isFinite(s) && s > 0 ? s : 0);
|
|
93
|
+
const total = clean.reduce((a, b) => a + b, 0);
|
|
94
|
+
if (total <= 0) return shares.map(() => 100 / (shares.length || 1));
|
|
95
|
+
return clean.map((s) => s / total * 100);
|
|
96
|
+
}
|
|
97
|
+
function resolveTabColumnWidths(shares, available, minWidth = CE_TAB_COLUMN_MIN_WIDTH) {
|
|
98
|
+
if (shares.length === 0) return [];
|
|
99
|
+
shares = normalizeColumnShares(shares);
|
|
100
|
+
const clamped = /* @__PURE__ */ new Set();
|
|
101
|
+
let widths = [];
|
|
102
|
+
for (let pass = 0; pass <= shares.length; pass++) {
|
|
103
|
+
const free = available - clamped.size * minWidth;
|
|
104
|
+
const total = shares.reduce((sum, s, i) => clamped.has(i) ? sum : sum + Math.max(0, s), 0);
|
|
105
|
+
widths = shares.map((s, i) => {
|
|
106
|
+
if (clamped.has(i)) return minWidth;
|
|
107
|
+
return total > 0 ? Math.max(0, s) / total * free : free / (shares.length - clamped.size);
|
|
108
|
+
});
|
|
109
|
+
const violators = widths.map((w, i) => !clamped.has(i) && w < minWidth ? i : -1).filter((i) => i >= 0);
|
|
110
|
+
if (violators.length === 0) break;
|
|
111
|
+
violators.forEach((i) => clamped.add(i));
|
|
112
|
+
}
|
|
113
|
+
return widths;
|
|
114
|
+
}
|
|
115
|
+
var CE_LABEL_ABOVE_BELOW_ROW_WIDTH = 340;
|
|
84
116
|
// Annotate the CommonJS export names for ESM import in node:
|
|
85
117
|
0 && (module.exports = {
|
|
86
118
|
CALLOUT_DEFAULT_WIDTH,
|
|
119
|
+
CE_LABEL_ABOVE_BELOW_ROW_WIDTH,
|
|
120
|
+
CE_TAB_COLUMN_MIN_WIDTH,
|
|
87
121
|
DIALOG_SIZE_PX,
|
|
88
122
|
MODAL_AUTOSIZE_MIN_WIDTH,
|
|
89
123
|
PANEL_DEFAULT_WIDTH,
|
|
@@ -94,7 +128,9 @@ function clampSpan(span, columns) {
|
|
|
94
128
|
dialogWidthCss,
|
|
95
129
|
dialogWidthPx,
|
|
96
130
|
gridTemplateForColumns,
|
|
131
|
+
normalizeColumnShares,
|
|
97
132
|
panelCustomWidthPx,
|
|
98
|
-
panelKindFromWidth
|
|
133
|
+
panelKindFromWidth,
|
|
134
|
+
resolveTabColumnWidths
|
|
99
135
|
});
|
|
100
136
|
//# sourceMappingURL=core.cjs.map
|
package/dist/core.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/index.ts","../src/core/sizePresets.ts","../src/core/columns.ts"],"sourcesContent":["export * from './sizePresets';\nexport * from './columns';\n","// Pure-TS sizing scalars for overlay surfaces. The SINGLE SOURCE of the numbers\n// that both surface-kit's runtime overlays AND the export-engine codegen use, so a\n// Dialog is 1200px wide in exactly one place. Framework-agnostic (no react/@fluentui).\n//\n// Values mirror export-engine/src/generators/FormCodeGenerator.ts (v8 branches):\n// dialog: default width 1200; height <=100 -> `${h}vh` else '95vh'\n// panel: kind = (width ?? 480) > 500 ? 'medium' : 'custom'; custom width = width ?? 480\n// callout (form-type): default width 320; directionalHint 'bottomLeftEdge'\n// v1c wires export-engine to import these; a byte-identical emission test guards drift.\n\n// --- Dialog --------------------------------------------------------------------\n\nexport type DialogSizePreset = 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen';\n\n// NOTE: only the lg = 1200 default and an explicit `width` are the shared-with-codegen\n// contract (export-engine does `width ?? 1200`). The sm/md/xl/fullscreen presets are\n// surface-kit runtime conveniences with no codegen counterpart — v1c does NOT consume them.\nexport const DIALOG_SIZE_PX = {\n sm: 600,\n md: 900,\n lg: 1200,\n xl: 1600,\n} as const;\n\n/** Resolved dialog width in px. Explicit `width` wins; else the preset (default lg = 1200, the codegen default). */\nexport function dialogWidthPx(opts: { width?: number; size?: DialogSizePreset } = {}): number {\n if (typeof opts.width === 'number') return opts.width;\n const size = opts.size ?? 'lg';\n // fullscreen is clamped by 95vw at the CSS layer; fall back to the largest preset for the px hint.\n if (size === 'fullscreen') return DIALOG_SIZE_PX.xl;\n return DIALOG_SIZE_PX[size];\n}\n\n/** Dialog width as a CSS value — '95vw' for the fullscreen preset (no explicit width), else `${px}px`. */\nexport function dialogWidthCss(opts: { width?: number; size?: DialogSizePreset } = {}): string {\n if (opts.size === 'fullscreen' && opts.width == null) return '95vw';\n return `${dialogWidthPx(opts)}px`;\n}\n\n/** Dialog height as a CSS value. <=100 is treated as a vh percentage; otherwise the 95vh cap (codegen rule). */\nexport function dialogHeightCss(height?: number): string {\n return height != null && height <= 100 ? `${height}vh` : '95vh';\n}\n\n/** Min width for an auto-sized ModalSurface (the Track-and-Ship \"min 1000, auto\" pattern). */\nexport const MODAL_AUTOSIZE_MIN_WIDTH = 1000;\n\n// --- Panel ---------------------------------------------------------------------\n\nexport type PanelKind = 'medium' | 'custom';\nexport const PANEL_DEFAULT_WIDTH = 480;\n\n/** Codegen-parity panel kind: wide panels use Fluent `medium`, narrow ones `custom` + customWidth. */\nexport function panelKindFromWidth(width?: number): PanelKind {\n return (width ?? PANEL_DEFAULT_WIDTH) > 500 ? 'medium' : 'custom';\n}\n\n/** customWidth px for a `custom` panel. */\nexport function panelCustomWidthPx(width?: number): number {\n return width ?? PANEL_DEFAULT_WIDTH;\n}\n\n// --- Callout -------------------------------------------------------------------\n\nexport const CALLOUT_DEFAULT_WIDTH = 320;\n\n/** Form-type callout defaults (the export-engine form-callout path emits width + directionalHint only). */\nexport const calloutFormDefaults = {\n width: CALLOUT_DEFAULT_WIDTH,\n directionalHint: 'bottomLeftEdge',\n} as const;\n\nexport function calloutWidthPx(width?: number): number {\n return width ?? CALLOUT_DEFAULT_WIDTH;\n}\n","// Pure-TS column helpers shared by layouts/containers and the responsive builders.\nexport type ColumnCount = 1 | 2 | 3 | 4;\n\n/** CSS grid-template-columns for an N-column layout. Matches codegen `repeat(N, 1fr)`. */\nexport function gridTemplateForColumns(columns: ColumnCount): string {\n return `repeat(${columns}, 1fr)`;\n}\n\n/** Clamp a cell column-span to the available column count (min 1). */\nexport function clampSpan(span: number, columns: ColumnCount): number {\n if (!Number.isFinite(span)) return 1;\n return Math.max(1, Math.min(Math.round(span), columns));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACiBO,IAAM,iBAAiB;AAAA,EAC5B,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGO,SAAS,cAAc,OAAoD,CAAC,GAAW;AAC5F,MAAI,OAAO,KAAK,UAAU,SAAU,QAAO,KAAK;AAChD,QAAM,OAAO,KAAK,QAAQ;AAE1B,MAAI,SAAS,aAAc,QAAO,eAAe;AACjD,SAAO,eAAe,IAAI;AAC5B;AAGO,SAAS,eAAe,OAAoD,CAAC,GAAW;AAC7F,MAAI,KAAK,SAAS,gBAAgB,KAAK,SAAS,KAAM,QAAO;AAC7D,SAAO,GAAG,cAAc,IAAI,CAAC;AAC/B;AAGO,SAAS,gBAAgB,QAAyB;AACvD,SAAO,UAAU,QAAQ,UAAU,MAAM,GAAG,MAAM,OAAO;AAC3D;AAGO,IAAM,2BAA2B;AAKjC,IAAM,sBAAsB;AAG5B,SAAS,mBAAmB,OAA2B;AAC5D,UAAQ,SAAS,uBAAuB,MAAM,WAAW;AAC3D;AAGO,SAAS,mBAAmB,OAAwB;AACzD,SAAO,SAAS;AAClB;AAIO,IAAM,wBAAwB;AAG9B,IAAM,sBAAsB;AAAA,EACjC,OAAO;AAAA,EACP,iBAAiB;AACnB;AAEO,SAAS,eAAe,OAAwB;AACrD,SAAO,SAAS;AAClB;;;ACtEO,SAAS,uBAAuB,SAA8B;AACnE,SAAO,UAAU,OAAO;AAC1B;AAGO,SAAS,UAAU,MAAc,SAA8B;AACpE,MAAI,CAAC,OAAO,SAAS,IAAI,EAAG,QAAO;AACnC,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,MAAM,IAAI,GAAG,OAAO,CAAC;AACxD;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/index.ts","../src/core/sizePresets.ts","../src/core/columns.ts","../src/core/ceLayout.ts"],"sourcesContent":["export * from './sizePresets';\nexport * from './columns';\nexport * from './ceLayout';\n","// Pure-TS sizing scalars for overlay surfaces. The SINGLE SOURCE of the numbers\n// that both surface-kit's runtime overlays AND the export-engine codegen use, so a\n// Dialog is 1200px wide in exactly one place. Framework-agnostic (no react/@fluentui).\n//\n// Values mirror export-engine/src/generators/FormCodeGenerator.ts (v8 branches):\n// dialog: default width 1200; height <=100 -> `${h}vh` else '95vh'\n// panel: kind = (width ?? 480) > 500 ? 'medium' : 'custom'; custom width = width ?? 480\n// callout (form-type): default width 320; directionalHint 'bottomLeftEdge'\n// v1c wires export-engine to import these; a byte-identical emission test guards drift.\n\n// --- Dialog --------------------------------------------------------------------\n\nexport type DialogSizePreset = 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen';\n\n// NOTE: only the lg = 1200 default and an explicit `width` are the shared-with-codegen\n// contract (export-engine does `width ?? 1200`). The sm/md/xl/fullscreen presets are\n// surface-kit runtime conveniences with no codegen counterpart — v1c does NOT consume them.\nexport const DIALOG_SIZE_PX = {\n sm: 600,\n md: 900,\n lg: 1200,\n xl: 1600,\n} as const;\n\n/** Resolved dialog width in px. Explicit `width` wins; else the preset (default lg = 1200, the codegen default). */\nexport function dialogWidthPx(opts: { width?: number; size?: DialogSizePreset } = {}): number {\n if (typeof opts.width === 'number') return opts.width;\n const size = opts.size ?? 'lg';\n // fullscreen is clamped by 95vw at the CSS layer; fall back to the largest preset for the px hint.\n if (size === 'fullscreen') return DIALOG_SIZE_PX.xl;\n return DIALOG_SIZE_PX[size];\n}\n\n/** Dialog width as a CSS value — '95vw' for the fullscreen preset (no explicit width), else `${px}px`. */\nexport function dialogWidthCss(opts: { width?: number; size?: DialogSizePreset } = {}): string {\n if (opts.size === 'fullscreen' && opts.width == null) return '95vw';\n return `${dialogWidthPx(opts)}px`;\n}\n\n/** Dialog height as a CSS value. <=100 is treated as a vh percentage; otherwise the 95vh cap (codegen rule). */\nexport function dialogHeightCss(height?: number): string {\n return height != null && height <= 100 ? `${height}vh` : '95vh';\n}\n\n/** Min width for an auto-sized ModalSurface (the Track-and-Ship \"min 1000, auto\" pattern). */\nexport const MODAL_AUTOSIZE_MIN_WIDTH = 1000;\n\n// --- Panel ---------------------------------------------------------------------\n\nexport type PanelKind = 'medium' | 'custom';\nexport const PANEL_DEFAULT_WIDTH = 480;\n\n/** Codegen-parity panel kind: wide panels use Fluent `medium`, narrow ones `custom` + customWidth. */\nexport function panelKindFromWidth(width?: number): PanelKind {\n return (width ?? PANEL_DEFAULT_WIDTH) > 500 ? 'medium' : 'custom';\n}\n\n/** customWidth px for a `custom` panel. */\nexport function panelCustomWidthPx(width?: number): number {\n return width ?? PANEL_DEFAULT_WIDTH;\n}\n\n// --- Callout -------------------------------------------------------------------\n\nexport const CALLOUT_DEFAULT_WIDTH = 320;\n\n/** Form-type callout defaults (the export-engine form-callout path emits width + directionalHint only). */\nexport const calloutFormDefaults = {\n width: CALLOUT_DEFAULT_WIDTH,\n directionalHint: 'bottomLeftEdge',\n} as const;\n\nexport function calloutWidthPx(width?: number): number {\n return width ?? CALLOUT_DEFAULT_WIDTH;\n}\n","// Pure-TS column helpers shared by layouts/containers and the responsive builders.\nexport type ColumnCount = 1 | 2 | 3 | 4;\n\n/** CSS grid-template-columns for an N-column layout. Matches codegen `repeat(N, 1fr)`. */\nexport function gridTemplateForColumns(columns: ColumnCount): string {\n return `repeat(${columns}, 1fr)`;\n}\n\n/** Clamp a cell column-span to the available column count (min 1). */\nexport function clampSpan(span: number, columns: ColumnCount): number {\n if (!Number.isFinite(span)) return 1;\n return Math.max(1, Math.min(Math.round(span), columns));\n}\n","// Pure-TS layout rules MEASURED on a live Dynamics 365 CE model-driven form (the UCI \"new look\").\n// Evidence: dynamics-ui-kit apps/ce-parity/reference/ (captures + ce-layout-reference.json).\n// No React / Fluent imports: this file is inlined by export-engine like the rest of core/.\n\n/** The appearance family a surface renders in: the kit's existing look, or the measured CE form. */\nexport type SurfaceLook = 'default' | 'ce';\n\n/**\n * A Dynamics tab column never renders narrower than this (px). Measured: a 25% column in a\n * 1106px tab body renders 340, not 276.5.\n */\nexport const CE_TAB_COLUMN_MIN_WIDTH = 340;\n\n/**\n * Normalise declared column shares so they sum to 100: negative or non-finite shares count as 0,\n * and all-zero (or empty-of-positive) input becomes equal shares. `TabColumns` renders the\n * NORMALISED shares, and `resolveTabColumnWidths` normalises too, so the CSS and this function\n * always agree. (CSS flexbox treats a flex-grow sum below 1 differently — it leaves free space\n * unused — and never grows a flex-grow-0 item; normalising removes both divergences.)\n */\nexport function normalizeColumnShares(shares: readonly number[]): number[] {\n const clean = shares.map((s) => (Number.isFinite(s) && s > 0 ? s : 0));\n const total = clean.reduce((a, b) => a + b, 0);\n if (total <= 0) return shares.map(() => 100 / (shares.length || 1));\n return clean.map((s) => (s / total) * 100);\n}\n\n/**\n * Resolve the rendered width of each tab column, the way Dynamics does.\n *\n * Each column takes its declared share (the formxml column `width`, e.g. 33 / 42 / 25) of the\n * space left after the gaps, but never goes below `minWidth`. The shortfall of a clamped column is\n * taken from the unclamped columns in proportion to their declared shares. That is CSS flexbox\n * with `flex: <share> 1 0` and `min-width` over NORMALISED shares (see normalizeColumnShares),\n * which is how `TabColumns` renders it.\n * This function exists so the rule can be tested and used where there is no DOM (e.g. sizing\n * from a PCF `allocatedWidth`).\n *\n * Verified against every measured layout: 33/42/25 at a 1450, 1240 and 1106px row gives\n * 478.5/609/362.5, 396/504/340 and 340/426/340; 34/33/33 at 1450 gives 493/478.5/478.5.\n *\n * @param shares Declared column shares (percentages are the usual case; they are normalised).\n * @param available Row width in px AFTER subtracting the gaps between columns.\n * @param minWidth Minimum rendered column width. Defaults to the measured 340.\n * @returns Widths in px, in column order. If every column hits the floor the widths sum to more\n * than `available`; the caller wraps or scrolls (Dynamics' behaviour there is unmeasured).\n */\nexport function resolveTabColumnWidths(\n shares: readonly number[],\n available: number,\n minWidth: number = CE_TAB_COLUMN_MIN_WIDTH,\n): number[] {\n if (shares.length === 0) return [];\n shares = normalizeColumnShares(shares);\n const clamped = new Set<number>();\n let widths: number[] = [];\n // Resolve like the flexbox algorithm: distribute, freeze violators, redistribute, until none violate.\n for (let pass = 0; pass <= shares.length; pass++) {\n const free = available - clamped.size * minWidth;\n const total = shares.reduce((sum, s, i) => (clamped.has(i) ? sum : sum + Math.max(0, s)), 0);\n widths = shares.map((s, i) => {\n if (clamped.has(i)) return minWidth;\n return total > 0 ? (Math.max(0, s) / total) * free : free / (shares.length - clamped.size);\n });\n // CSS Flexbox §9.7: when the total violation is positive (only min-width constraints exist\n // here, so it always is), freeze EVERY min-violating item at once, then redistribute.\n const violators = widths.map((w, i) => (!clamped.has(i) && w < minWidth ? i : -1)).filter((i) => i >= 0);\n if (violators.length === 0) break;\n violators.forEach((i) => clamped.add(i));\n }\n return widths;\n}\n\n/**\n * Row width (px) below which a field label declared \"beside\" (formxml `Left`) is drawn ABOVE its\n * input instead.\n *\n * ★ PROVISIONAL. Measured only as a bracket: beside at a 362px row, above at a 306px row. 340 is\n * the column floor and sits inside the bracket, but the exact switch point has NOT been\n * observed. It is an open item in the reference; re-measure before relying on the exact value.\n */\nexport const CE_LABEL_ABOVE_BELOW_ROW_WIDTH = 340;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACiBO,IAAM,iBAAiB;AAAA,EAC5B,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGO,SAAS,cAAc,OAAoD,CAAC,GAAW;AAC5F,MAAI,OAAO,KAAK,UAAU,SAAU,QAAO,KAAK;AAChD,QAAM,OAAO,KAAK,QAAQ;AAE1B,MAAI,SAAS,aAAc,QAAO,eAAe;AACjD,SAAO,eAAe,IAAI;AAC5B;AAGO,SAAS,eAAe,OAAoD,CAAC,GAAW;AAC7F,MAAI,KAAK,SAAS,gBAAgB,KAAK,SAAS,KAAM,QAAO;AAC7D,SAAO,GAAG,cAAc,IAAI,CAAC;AAC/B;AAGO,SAAS,gBAAgB,QAAyB;AACvD,SAAO,UAAU,QAAQ,UAAU,MAAM,GAAG,MAAM,OAAO;AAC3D;AAGO,IAAM,2BAA2B;AAKjC,IAAM,sBAAsB;AAG5B,SAAS,mBAAmB,OAA2B;AAC5D,UAAQ,SAAS,uBAAuB,MAAM,WAAW;AAC3D;AAGO,SAAS,mBAAmB,OAAwB;AACzD,SAAO,SAAS;AAClB;AAIO,IAAM,wBAAwB;AAG9B,IAAM,sBAAsB;AAAA,EACjC,OAAO;AAAA,EACP,iBAAiB;AACnB;AAEO,SAAS,eAAe,OAAwB;AACrD,SAAO,SAAS;AAClB;;;ACtEO,SAAS,uBAAuB,SAA8B;AACnE,SAAO,UAAU,OAAO;AAC1B;AAGO,SAAS,UAAU,MAAc,SAA8B;AACpE,MAAI,CAAC,OAAO,SAAS,IAAI,EAAG,QAAO;AACnC,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,MAAM,IAAI,GAAG,OAAO,CAAC;AACxD;;;ACDO,IAAM,0BAA0B;AAShC,SAAS,sBAAsB,QAAqC;AACzE,QAAM,QAAQ,OAAO,IAAI,CAAC,MAAO,OAAO,SAAS,CAAC,KAAK,IAAI,IAAI,IAAI,CAAE;AACrE,QAAM,QAAQ,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC;AAC7C,MAAI,SAAS,EAAG,QAAO,OAAO,IAAI,MAAM,OAAO,OAAO,UAAU,EAAE;AAClE,SAAO,MAAM,IAAI,CAAC,MAAO,IAAI,QAAS,GAAG;AAC3C;AAsBO,SAAS,uBACd,QACA,WACA,WAAmB,yBACT;AACV,MAAI,OAAO,WAAW,EAAG,QAAO,CAAC;AACjC,WAAS,sBAAsB,MAAM;AACrC,QAAM,UAAU,oBAAI,IAAY;AAChC,MAAI,SAAmB,CAAC;AAExB,WAAS,OAAO,GAAG,QAAQ,OAAO,QAAQ,QAAQ;AAChD,UAAM,OAAO,YAAY,QAAQ,OAAO;AACxC,UAAM,QAAQ,OAAO,OAAO,CAAC,KAAK,GAAG,MAAO,QAAQ,IAAI,CAAC,IAAI,MAAM,MAAM,KAAK,IAAI,GAAG,CAAC,GAAI,CAAC;AAC3F,aAAS,OAAO,IAAI,CAAC,GAAG,MAAM;AAC5B,UAAI,QAAQ,IAAI,CAAC,EAAG,QAAO;AAC3B,aAAO,QAAQ,IAAK,KAAK,IAAI,GAAG,CAAC,IAAI,QAAS,OAAO,QAAQ,OAAO,SAAS,QAAQ;AAAA,IACvF,CAAC;AAGD,UAAM,YAAY,OAAO,IAAI,CAAC,GAAG,MAAO,CAAC,QAAQ,IAAI,CAAC,KAAK,IAAI,WAAW,IAAI,EAAG,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC;AACvG,QAAI,UAAU,WAAW,EAAG;AAC5B,cAAU,QAAQ,CAAC,MAAM,QAAQ,IAAI,CAAC,CAAC;AAAA,EACzC;AACA,SAAO;AACT;AAUO,IAAM,iCAAiC;","names":[]}
|
package/dist/core.d.cts
CHANGED
|
@@ -35,4 +35,50 @@ declare const calloutFormDefaults: {
|
|
|
35
35
|
};
|
|
36
36
|
declare function calloutWidthPx(width?: number): number;
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/** The appearance family a surface renders in: the kit's existing look, or the measured CE form. */
|
|
39
|
+
type SurfaceLook = 'default' | 'ce';
|
|
40
|
+
/**
|
|
41
|
+
* A Dynamics tab column never renders narrower than this (px). Measured: a 25% column in a
|
|
42
|
+
* 1106px tab body renders 340, not 276.5.
|
|
43
|
+
*/
|
|
44
|
+
declare const CE_TAB_COLUMN_MIN_WIDTH = 340;
|
|
45
|
+
/**
|
|
46
|
+
* Normalise declared column shares so they sum to 100: negative or non-finite shares count as 0,
|
|
47
|
+
* and all-zero (or empty-of-positive) input becomes equal shares. `TabColumns` renders the
|
|
48
|
+
* NORMALISED shares, and `resolveTabColumnWidths` normalises too, so the CSS and this function
|
|
49
|
+
* always agree. (CSS flexbox treats a flex-grow sum below 1 differently — it leaves free space
|
|
50
|
+
* unused — and never grows a flex-grow-0 item; normalising removes both divergences.)
|
|
51
|
+
*/
|
|
52
|
+
declare function normalizeColumnShares(shares: readonly number[]): number[];
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the rendered width of each tab column, the way Dynamics does.
|
|
55
|
+
*
|
|
56
|
+
* Each column takes its declared share (the formxml column `width`, e.g. 33 / 42 / 25) of the
|
|
57
|
+
* space left after the gaps, but never goes below `minWidth`. The shortfall of a clamped column is
|
|
58
|
+
* taken from the unclamped columns in proportion to their declared shares. That is CSS flexbox
|
|
59
|
+
* with `flex: <share> 1 0` and `min-width` over NORMALISED shares (see normalizeColumnShares),
|
|
60
|
+
* which is how `TabColumns` renders it.
|
|
61
|
+
* This function exists so the rule can be tested and used where there is no DOM (e.g. sizing
|
|
62
|
+
* from a PCF `allocatedWidth`).
|
|
63
|
+
*
|
|
64
|
+
* Verified against every measured layout: 33/42/25 at a 1450, 1240 and 1106px row gives
|
|
65
|
+
* 478.5/609/362.5, 396/504/340 and 340/426/340; 34/33/33 at 1450 gives 493/478.5/478.5.
|
|
66
|
+
*
|
|
67
|
+
* @param shares Declared column shares (percentages are the usual case; they are normalised).
|
|
68
|
+
* @param available Row width in px AFTER subtracting the gaps between columns.
|
|
69
|
+
* @param minWidth Minimum rendered column width. Defaults to the measured 340.
|
|
70
|
+
* @returns Widths in px, in column order. If every column hits the floor the widths sum to more
|
|
71
|
+
* than `available`; the caller wraps or scrolls (Dynamics' behaviour there is unmeasured).
|
|
72
|
+
*/
|
|
73
|
+
declare function resolveTabColumnWidths(shares: readonly number[], available: number, minWidth?: number): number[];
|
|
74
|
+
/**
|
|
75
|
+
* Row width (px) below which a field label declared "beside" (formxml `Left`) is drawn ABOVE its
|
|
76
|
+
* input instead.
|
|
77
|
+
*
|
|
78
|
+
* ★ PROVISIONAL. Measured only as a bracket: beside at a 362px row, above at a 306px row. 340 is
|
|
79
|
+
* the column floor and sits inside the bracket, but the exact switch point has NOT been
|
|
80
|
+
* observed. It is an open item in the reference; re-measure before relying on the exact value.
|
|
81
|
+
*/
|
|
82
|
+
declare const CE_LABEL_ABOVE_BELOW_ROW_WIDTH = 340;
|
|
83
|
+
|
|
84
|
+
export { CALLOUT_DEFAULT_WIDTH, CE_LABEL_ABOVE_BELOW_ROW_WIDTH, CE_TAB_COLUMN_MIN_WIDTH, DIALOG_SIZE_PX, type DialogSizePreset, MODAL_AUTOSIZE_MIN_WIDTH, PANEL_DEFAULT_WIDTH, type PanelKind, type SurfaceLook, calloutFormDefaults, calloutWidthPx, dialogHeightCss, dialogWidthCss, dialogWidthPx, normalizeColumnShares, panelCustomWidthPx, panelKindFromWidth, resolveTabColumnWidths };
|
package/dist/core.d.ts
CHANGED
|
@@ -35,4 +35,50 @@ declare const calloutFormDefaults: {
|
|
|
35
35
|
};
|
|
36
36
|
declare function calloutWidthPx(width?: number): number;
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
/** The appearance family a surface renders in: the kit's existing look, or the measured CE form. */
|
|
39
|
+
type SurfaceLook = 'default' | 'ce';
|
|
40
|
+
/**
|
|
41
|
+
* A Dynamics tab column never renders narrower than this (px). Measured: a 25% column in a
|
|
42
|
+
* 1106px tab body renders 340, not 276.5.
|
|
43
|
+
*/
|
|
44
|
+
declare const CE_TAB_COLUMN_MIN_WIDTH = 340;
|
|
45
|
+
/**
|
|
46
|
+
* Normalise declared column shares so they sum to 100: negative or non-finite shares count as 0,
|
|
47
|
+
* and all-zero (or empty-of-positive) input becomes equal shares. `TabColumns` renders the
|
|
48
|
+
* NORMALISED shares, and `resolveTabColumnWidths` normalises too, so the CSS and this function
|
|
49
|
+
* always agree. (CSS flexbox treats a flex-grow sum below 1 differently — it leaves free space
|
|
50
|
+
* unused — and never grows a flex-grow-0 item; normalising removes both divergences.)
|
|
51
|
+
*/
|
|
52
|
+
declare function normalizeColumnShares(shares: readonly number[]): number[];
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the rendered width of each tab column, the way Dynamics does.
|
|
55
|
+
*
|
|
56
|
+
* Each column takes its declared share (the formxml column `width`, e.g. 33 / 42 / 25) of the
|
|
57
|
+
* space left after the gaps, but never goes below `minWidth`. The shortfall of a clamped column is
|
|
58
|
+
* taken from the unclamped columns in proportion to their declared shares. That is CSS flexbox
|
|
59
|
+
* with `flex: <share> 1 0` and `min-width` over NORMALISED shares (see normalizeColumnShares),
|
|
60
|
+
* which is how `TabColumns` renders it.
|
|
61
|
+
* This function exists so the rule can be tested and used where there is no DOM (e.g. sizing
|
|
62
|
+
* from a PCF `allocatedWidth`).
|
|
63
|
+
*
|
|
64
|
+
* Verified against every measured layout: 33/42/25 at a 1450, 1240 and 1106px row gives
|
|
65
|
+
* 478.5/609/362.5, 396/504/340 and 340/426/340; 34/33/33 at 1450 gives 493/478.5/478.5.
|
|
66
|
+
*
|
|
67
|
+
* @param shares Declared column shares (percentages are the usual case; they are normalised).
|
|
68
|
+
* @param available Row width in px AFTER subtracting the gaps between columns.
|
|
69
|
+
* @param minWidth Minimum rendered column width. Defaults to the measured 340.
|
|
70
|
+
* @returns Widths in px, in column order. If every column hits the floor the widths sum to more
|
|
71
|
+
* than `available`; the caller wraps or scrolls (Dynamics' behaviour there is unmeasured).
|
|
72
|
+
*/
|
|
73
|
+
declare function resolveTabColumnWidths(shares: readonly number[], available: number, minWidth?: number): number[];
|
|
74
|
+
/**
|
|
75
|
+
* Row width (px) below which a field label declared "beside" (formxml `Left`) is drawn ABOVE its
|
|
76
|
+
* input instead.
|
|
77
|
+
*
|
|
78
|
+
* ★ PROVISIONAL. Measured only as a bracket: beside at a 362px row, above at a 306px row. 340 is
|
|
79
|
+
* the column floor and sits inside the bracket, but the exact switch point has NOT been
|
|
80
|
+
* observed. It is an open item in the reference; re-measure before relying on the exact value.
|
|
81
|
+
*/
|
|
82
|
+
declare const CE_LABEL_ABOVE_BELOW_ROW_WIDTH = 340;
|
|
83
|
+
|
|
84
|
+
export { CALLOUT_DEFAULT_WIDTH, CE_LABEL_ABOVE_BELOW_ROW_WIDTH, CE_TAB_COLUMN_MIN_WIDTH, DIALOG_SIZE_PX, type DialogSizePreset, MODAL_AUTOSIZE_MIN_WIDTH, PANEL_DEFAULT_WIDTH, type PanelKind, type SurfaceLook, calloutFormDefaults, calloutWidthPx, dialogHeightCss, dialogWidthCss, dialogWidthPx, normalizeColumnShares, panelCustomWidthPx, panelKindFromWidth, resolveTabColumnWidths };
|
package/dist/core.mjs
CHANGED
|
@@ -43,8 +43,38 @@ function clampSpan(span, columns) {
|
|
|
43
43
|
if (!Number.isFinite(span)) return 1;
|
|
44
44
|
return Math.max(1, Math.min(Math.round(span), columns));
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
// src/core/ceLayout.ts
|
|
48
|
+
var CE_TAB_COLUMN_MIN_WIDTH = 340;
|
|
49
|
+
function normalizeColumnShares(shares) {
|
|
50
|
+
const clean = shares.map((s) => Number.isFinite(s) && s > 0 ? s : 0);
|
|
51
|
+
const total = clean.reduce((a, b) => a + b, 0);
|
|
52
|
+
if (total <= 0) return shares.map(() => 100 / (shares.length || 1));
|
|
53
|
+
return clean.map((s) => s / total * 100);
|
|
54
|
+
}
|
|
55
|
+
function resolveTabColumnWidths(shares, available, minWidth = CE_TAB_COLUMN_MIN_WIDTH) {
|
|
56
|
+
if (shares.length === 0) return [];
|
|
57
|
+
shares = normalizeColumnShares(shares);
|
|
58
|
+
const clamped = /* @__PURE__ */ new Set();
|
|
59
|
+
let widths = [];
|
|
60
|
+
for (let pass = 0; pass <= shares.length; pass++) {
|
|
61
|
+
const free = available - clamped.size * minWidth;
|
|
62
|
+
const total = shares.reduce((sum, s, i) => clamped.has(i) ? sum : sum + Math.max(0, s), 0);
|
|
63
|
+
widths = shares.map((s, i) => {
|
|
64
|
+
if (clamped.has(i)) return minWidth;
|
|
65
|
+
return total > 0 ? Math.max(0, s) / total * free : free / (shares.length - clamped.size);
|
|
66
|
+
});
|
|
67
|
+
const violators = widths.map((w, i) => !clamped.has(i) && w < minWidth ? i : -1).filter((i) => i >= 0);
|
|
68
|
+
if (violators.length === 0) break;
|
|
69
|
+
violators.forEach((i) => clamped.add(i));
|
|
70
|
+
}
|
|
71
|
+
return widths;
|
|
72
|
+
}
|
|
73
|
+
var CE_LABEL_ABOVE_BELOW_ROW_WIDTH = 340;
|
|
46
74
|
export {
|
|
47
75
|
CALLOUT_DEFAULT_WIDTH,
|
|
76
|
+
CE_LABEL_ABOVE_BELOW_ROW_WIDTH,
|
|
77
|
+
CE_TAB_COLUMN_MIN_WIDTH,
|
|
48
78
|
DIALOG_SIZE_PX,
|
|
49
79
|
MODAL_AUTOSIZE_MIN_WIDTH,
|
|
50
80
|
PANEL_DEFAULT_WIDTH,
|
|
@@ -55,7 +85,9 @@ export {
|
|
|
55
85
|
dialogWidthCss,
|
|
56
86
|
dialogWidthPx,
|
|
57
87
|
gridTemplateForColumns,
|
|
88
|
+
normalizeColumnShares,
|
|
58
89
|
panelCustomWidthPx,
|
|
59
|
-
panelKindFromWidth
|
|
90
|
+
panelKindFromWidth,
|
|
91
|
+
resolveTabColumnWidths
|
|
60
92
|
};
|
|
61
93
|
//# sourceMappingURL=core.mjs.map
|
package/dist/core.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/core/sizePresets.ts","../src/core/columns.ts"],"sourcesContent":["// Pure-TS sizing scalars for overlay surfaces. The SINGLE SOURCE of the numbers\n// that both surface-kit's runtime overlays AND the export-engine codegen use, so a\n// Dialog is 1200px wide in exactly one place. Framework-agnostic (no react/@fluentui).\n//\n// Values mirror export-engine/src/generators/FormCodeGenerator.ts (v8 branches):\n// dialog: default width 1200; height <=100 -> `${h}vh` else '95vh'\n// panel: kind = (width ?? 480) > 500 ? 'medium' : 'custom'; custom width = width ?? 480\n// callout (form-type): default width 320; directionalHint 'bottomLeftEdge'\n// v1c wires export-engine to import these; a byte-identical emission test guards drift.\n\n// --- Dialog --------------------------------------------------------------------\n\nexport type DialogSizePreset = 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen';\n\n// NOTE: only the lg = 1200 default and an explicit `width` are the shared-with-codegen\n// contract (export-engine does `width ?? 1200`). The sm/md/xl/fullscreen presets are\n// surface-kit runtime conveniences with no codegen counterpart — v1c does NOT consume them.\nexport const DIALOG_SIZE_PX = {\n sm: 600,\n md: 900,\n lg: 1200,\n xl: 1600,\n} as const;\n\n/** Resolved dialog width in px. Explicit `width` wins; else the preset (default lg = 1200, the codegen default). */\nexport function dialogWidthPx(opts: { width?: number; size?: DialogSizePreset } = {}): number {\n if (typeof opts.width === 'number') return opts.width;\n const size = opts.size ?? 'lg';\n // fullscreen is clamped by 95vw at the CSS layer; fall back to the largest preset for the px hint.\n if (size === 'fullscreen') return DIALOG_SIZE_PX.xl;\n return DIALOG_SIZE_PX[size];\n}\n\n/** Dialog width as a CSS value — '95vw' for the fullscreen preset (no explicit width), else `${px}px`. */\nexport function dialogWidthCss(opts: { width?: number; size?: DialogSizePreset } = {}): string {\n if (opts.size === 'fullscreen' && opts.width == null) return '95vw';\n return `${dialogWidthPx(opts)}px`;\n}\n\n/** Dialog height as a CSS value. <=100 is treated as a vh percentage; otherwise the 95vh cap (codegen rule). */\nexport function dialogHeightCss(height?: number): string {\n return height != null && height <= 100 ? `${height}vh` : '95vh';\n}\n\n/** Min width for an auto-sized ModalSurface (the Track-and-Ship \"min 1000, auto\" pattern). */\nexport const MODAL_AUTOSIZE_MIN_WIDTH = 1000;\n\n// --- Panel ---------------------------------------------------------------------\n\nexport type PanelKind = 'medium' | 'custom';\nexport const PANEL_DEFAULT_WIDTH = 480;\n\n/** Codegen-parity panel kind: wide panels use Fluent `medium`, narrow ones `custom` + customWidth. */\nexport function panelKindFromWidth(width?: number): PanelKind {\n return (width ?? PANEL_DEFAULT_WIDTH) > 500 ? 'medium' : 'custom';\n}\n\n/** customWidth px for a `custom` panel. */\nexport function panelCustomWidthPx(width?: number): number {\n return width ?? PANEL_DEFAULT_WIDTH;\n}\n\n// --- Callout -------------------------------------------------------------------\n\nexport const CALLOUT_DEFAULT_WIDTH = 320;\n\n/** Form-type callout defaults (the export-engine form-callout path emits width + directionalHint only). */\nexport const calloutFormDefaults = {\n width: CALLOUT_DEFAULT_WIDTH,\n directionalHint: 'bottomLeftEdge',\n} as const;\n\nexport function calloutWidthPx(width?: number): number {\n return width ?? CALLOUT_DEFAULT_WIDTH;\n}\n","// Pure-TS column helpers shared by layouts/containers and the responsive builders.\nexport type ColumnCount = 1 | 2 | 3 | 4;\n\n/** CSS grid-template-columns for an N-column layout. Matches codegen `repeat(N, 1fr)`. */\nexport function gridTemplateForColumns(columns: ColumnCount): string {\n return `repeat(${columns}, 1fr)`;\n}\n\n/** Clamp a cell column-span to the available column count (min 1). */\nexport function clampSpan(span: number, columns: ColumnCount): number {\n if (!Number.isFinite(span)) return 1;\n return Math.max(1, Math.min(Math.round(span), columns));\n}\n"],"mappings":";AAiBO,IAAM,iBAAiB;AAAA,EAC5B,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGO,SAAS,cAAc,OAAoD,CAAC,GAAW;AAC5F,MAAI,OAAO,KAAK,UAAU,SAAU,QAAO,KAAK;AAChD,QAAM,OAAO,KAAK,QAAQ;AAE1B,MAAI,SAAS,aAAc,QAAO,eAAe;AACjD,SAAO,eAAe,IAAI;AAC5B;AAGO,SAAS,eAAe,OAAoD,CAAC,GAAW;AAC7F,MAAI,KAAK,SAAS,gBAAgB,KAAK,SAAS,KAAM,QAAO;AAC7D,SAAO,GAAG,cAAc,IAAI,CAAC;AAC/B;AAGO,SAAS,gBAAgB,QAAyB;AACvD,SAAO,UAAU,QAAQ,UAAU,MAAM,GAAG,MAAM,OAAO;AAC3D;AAGO,IAAM,2BAA2B;AAKjC,IAAM,sBAAsB;AAG5B,SAAS,mBAAmB,OAA2B;AAC5D,UAAQ,SAAS,uBAAuB,MAAM,WAAW;AAC3D;AAGO,SAAS,mBAAmB,OAAwB;AACzD,SAAO,SAAS;AAClB;AAIO,IAAM,wBAAwB;AAG9B,IAAM,sBAAsB;AAAA,EACjC,OAAO;AAAA,EACP,iBAAiB;AACnB;AAEO,SAAS,eAAe,OAAwB;AACrD,SAAO,SAAS;AAClB;;;ACtEO,SAAS,uBAAuB,SAA8B;AACnE,SAAO,UAAU,OAAO;AAC1B;AAGO,SAAS,UAAU,MAAc,SAA8B;AACpE,MAAI,CAAC,OAAO,SAAS,IAAI,EAAG,QAAO;AACnC,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,MAAM,IAAI,GAAG,OAAO,CAAC;AACxD;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/core/sizePresets.ts","../src/core/columns.ts","../src/core/ceLayout.ts"],"sourcesContent":["// Pure-TS sizing scalars for overlay surfaces. The SINGLE SOURCE of the numbers\n// that both surface-kit's runtime overlays AND the export-engine codegen use, so a\n// Dialog is 1200px wide in exactly one place. Framework-agnostic (no react/@fluentui).\n//\n// Values mirror export-engine/src/generators/FormCodeGenerator.ts (v8 branches):\n// dialog: default width 1200; height <=100 -> `${h}vh` else '95vh'\n// panel: kind = (width ?? 480) > 500 ? 'medium' : 'custom'; custom width = width ?? 480\n// callout (form-type): default width 320; directionalHint 'bottomLeftEdge'\n// v1c wires export-engine to import these; a byte-identical emission test guards drift.\n\n// --- Dialog --------------------------------------------------------------------\n\nexport type DialogSizePreset = 'sm' | 'md' | 'lg' | 'xl' | 'fullscreen';\n\n// NOTE: only the lg = 1200 default and an explicit `width` are the shared-with-codegen\n// contract (export-engine does `width ?? 1200`). The sm/md/xl/fullscreen presets are\n// surface-kit runtime conveniences with no codegen counterpart — v1c does NOT consume them.\nexport const DIALOG_SIZE_PX = {\n sm: 600,\n md: 900,\n lg: 1200,\n xl: 1600,\n} as const;\n\n/** Resolved dialog width in px. Explicit `width` wins; else the preset (default lg = 1200, the codegen default). */\nexport function dialogWidthPx(opts: { width?: number; size?: DialogSizePreset } = {}): number {\n if (typeof opts.width === 'number') return opts.width;\n const size = opts.size ?? 'lg';\n // fullscreen is clamped by 95vw at the CSS layer; fall back to the largest preset for the px hint.\n if (size === 'fullscreen') return DIALOG_SIZE_PX.xl;\n return DIALOG_SIZE_PX[size];\n}\n\n/** Dialog width as a CSS value — '95vw' for the fullscreen preset (no explicit width), else `${px}px`. */\nexport function dialogWidthCss(opts: { width?: number; size?: DialogSizePreset } = {}): string {\n if (opts.size === 'fullscreen' && opts.width == null) return '95vw';\n return `${dialogWidthPx(opts)}px`;\n}\n\n/** Dialog height as a CSS value. <=100 is treated as a vh percentage; otherwise the 95vh cap (codegen rule). */\nexport function dialogHeightCss(height?: number): string {\n return height != null && height <= 100 ? `${height}vh` : '95vh';\n}\n\n/** Min width for an auto-sized ModalSurface (the Track-and-Ship \"min 1000, auto\" pattern). */\nexport const MODAL_AUTOSIZE_MIN_WIDTH = 1000;\n\n// --- Panel ---------------------------------------------------------------------\n\nexport type PanelKind = 'medium' | 'custom';\nexport const PANEL_DEFAULT_WIDTH = 480;\n\n/** Codegen-parity panel kind: wide panels use Fluent `medium`, narrow ones `custom` + customWidth. */\nexport function panelKindFromWidth(width?: number): PanelKind {\n return (width ?? PANEL_DEFAULT_WIDTH) > 500 ? 'medium' : 'custom';\n}\n\n/** customWidth px for a `custom` panel. */\nexport function panelCustomWidthPx(width?: number): number {\n return width ?? PANEL_DEFAULT_WIDTH;\n}\n\n// --- Callout -------------------------------------------------------------------\n\nexport const CALLOUT_DEFAULT_WIDTH = 320;\n\n/** Form-type callout defaults (the export-engine form-callout path emits width + directionalHint only). */\nexport const calloutFormDefaults = {\n width: CALLOUT_DEFAULT_WIDTH,\n directionalHint: 'bottomLeftEdge',\n} as const;\n\nexport function calloutWidthPx(width?: number): number {\n return width ?? CALLOUT_DEFAULT_WIDTH;\n}\n","// Pure-TS column helpers shared by layouts/containers and the responsive builders.\nexport type ColumnCount = 1 | 2 | 3 | 4;\n\n/** CSS grid-template-columns for an N-column layout. Matches codegen `repeat(N, 1fr)`. */\nexport function gridTemplateForColumns(columns: ColumnCount): string {\n return `repeat(${columns}, 1fr)`;\n}\n\n/** Clamp a cell column-span to the available column count (min 1). */\nexport function clampSpan(span: number, columns: ColumnCount): number {\n if (!Number.isFinite(span)) return 1;\n return Math.max(1, Math.min(Math.round(span), columns));\n}\n","// Pure-TS layout rules MEASURED on a live Dynamics 365 CE model-driven form (the UCI \"new look\").\n// Evidence: dynamics-ui-kit apps/ce-parity/reference/ (captures + ce-layout-reference.json).\n// No React / Fluent imports: this file is inlined by export-engine like the rest of core/.\n\n/** The appearance family a surface renders in: the kit's existing look, or the measured CE form. */\nexport type SurfaceLook = 'default' | 'ce';\n\n/**\n * A Dynamics tab column never renders narrower than this (px). Measured: a 25% column in a\n * 1106px tab body renders 340, not 276.5.\n */\nexport const CE_TAB_COLUMN_MIN_WIDTH = 340;\n\n/**\n * Normalise declared column shares so they sum to 100: negative or non-finite shares count as 0,\n * and all-zero (or empty-of-positive) input becomes equal shares. `TabColumns` renders the\n * NORMALISED shares, and `resolveTabColumnWidths` normalises too, so the CSS and this function\n * always agree. (CSS flexbox treats a flex-grow sum below 1 differently — it leaves free space\n * unused — and never grows a flex-grow-0 item; normalising removes both divergences.)\n */\nexport function normalizeColumnShares(shares: readonly number[]): number[] {\n const clean = shares.map((s) => (Number.isFinite(s) && s > 0 ? s : 0));\n const total = clean.reduce((a, b) => a + b, 0);\n if (total <= 0) return shares.map(() => 100 / (shares.length || 1));\n return clean.map((s) => (s / total) * 100);\n}\n\n/**\n * Resolve the rendered width of each tab column, the way Dynamics does.\n *\n * Each column takes its declared share (the formxml column `width`, e.g. 33 / 42 / 25) of the\n * space left after the gaps, but never goes below `minWidth`. The shortfall of a clamped column is\n * taken from the unclamped columns in proportion to their declared shares. That is CSS flexbox\n * with `flex: <share> 1 0` and `min-width` over NORMALISED shares (see normalizeColumnShares),\n * which is how `TabColumns` renders it.\n * This function exists so the rule can be tested and used where there is no DOM (e.g. sizing\n * from a PCF `allocatedWidth`).\n *\n * Verified against every measured layout: 33/42/25 at a 1450, 1240 and 1106px row gives\n * 478.5/609/362.5, 396/504/340 and 340/426/340; 34/33/33 at 1450 gives 493/478.5/478.5.\n *\n * @param shares Declared column shares (percentages are the usual case; they are normalised).\n * @param available Row width in px AFTER subtracting the gaps between columns.\n * @param minWidth Minimum rendered column width. Defaults to the measured 340.\n * @returns Widths in px, in column order. If every column hits the floor the widths sum to more\n * than `available`; the caller wraps or scrolls (Dynamics' behaviour there is unmeasured).\n */\nexport function resolveTabColumnWidths(\n shares: readonly number[],\n available: number,\n minWidth: number = CE_TAB_COLUMN_MIN_WIDTH,\n): number[] {\n if (shares.length === 0) return [];\n shares = normalizeColumnShares(shares);\n const clamped = new Set<number>();\n let widths: number[] = [];\n // Resolve like the flexbox algorithm: distribute, freeze violators, redistribute, until none violate.\n for (let pass = 0; pass <= shares.length; pass++) {\n const free = available - clamped.size * minWidth;\n const total = shares.reduce((sum, s, i) => (clamped.has(i) ? sum : sum + Math.max(0, s)), 0);\n widths = shares.map((s, i) => {\n if (clamped.has(i)) return minWidth;\n return total > 0 ? (Math.max(0, s) / total) * free : free / (shares.length - clamped.size);\n });\n // CSS Flexbox §9.7: when the total violation is positive (only min-width constraints exist\n // here, so it always is), freeze EVERY min-violating item at once, then redistribute.\n const violators = widths.map((w, i) => (!clamped.has(i) && w < minWidth ? i : -1)).filter((i) => i >= 0);\n if (violators.length === 0) break;\n violators.forEach((i) => clamped.add(i));\n }\n return widths;\n}\n\n/**\n * Row width (px) below which a field label declared \"beside\" (formxml `Left`) is drawn ABOVE its\n * input instead.\n *\n * ★ PROVISIONAL. Measured only as a bracket: beside at a 362px row, above at a 306px row. 340 is\n * the column floor and sits inside the bracket, but the exact switch point has NOT been\n * observed. It is an open item in the reference; re-measure before relying on the exact value.\n */\nexport const CE_LABEL_ABOVE_BELOW_ROW_WIDTH = 340;\n"],"mappings":";AAiBO,IAAM,iBAAiB;AAAA,EAC5B,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AAAA,EACJ,IAAI;AACN;AAGO,SAAS,cAAc,OAAoD,CAAC,GAAW;AAC5F,MAAI,OAAO,KAAK,UAAU,SAAU,QAAO,KAAK;AAChD,QAAM,OAAO,KAAK,QAAQ;AAE1B,MAAI,SAAS,aAAc,QAAO,eAAe;AACjD,SAAO,eAAe,IAAI;AAC5B;AAGO,SAAS,eAAe,OAAoD,CAAC,GAAW;AAC7F,MAAI,KAAK,SAAS,gBAAgB,KAAK,SAAS,KAAM,QAAO;AAC7D,SAAO,GAAG,cAAc,IAAI,CAAC;AAC/B;AAGO,SAAS,gBAAgB,QAAyB;AACvD,SAAO,UAAU,QAAQ,UAAU,MAAM,GAAG,MAAM,OAAO;AAC3D;AAGO,IAAM,2BAA2B;AAKjC,IAAM,sBAAsB;AAG5B,SAAS,mBAAmB,OAA2B;AAC5D,UAAQ,SAAS,uBAAuB,MAAM,WAAW;AAC3D;AAGO,SAAS,mBAAmB,OAAwB;AACzD,SAAO,SAAS;AAClB;AAIO,IAAM,wBAAwB;AAG9B,IAAM,sBAAsB;AAAA,EACjC,OAAO;AAAA,EACP,iBAAiB;AACnB;AAEO,SAAS,eAAe,OAAwB;AACrD,SAAO,SAAS;AAClB;;;ACtEO,SAAS,uBAAuB,SAA8B;AACnE,SAAO,UAAU,OAAO;AAC1B;AAGO,SAAS,UAAU,MAAc,SAA8B;AACpE,MAAI,CAAC,OAAO,SAAS,IAAI,EAAG,QAAO;AACnC,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,MAAM,IAAI,GAAG,OAAO,CAAC;AACxD;;;ACDO,IAAM,0BAA0B;AAShC,SAAS,sBAAsB,QAAqC;AACzE,QAAM,QAAQ,OAAO,IAAI,CAAC,MAAO,OAAO,SAAS,CAAC,KAAK,IAAI,IAAI,IAAI,CAAE;AACrE,QAAM,QAAQ,MAAM,OAAO,CAAC,GAAG,MAAM,IAAI,GAAG,CAAC;AAC7C,MAAI,SAAS,EAAG,QAAO,OAAO,IAAI,MAAM,OAAO,OAAO,UAAU,EAAE;AAClE,SAAO,MAAM,IAAI,CAAC,MAAO,IAAI,QAAS,GAAG;AAC3C;AAsBO,SAAS,uBACd,QACA,WACA,WAAmB,yBACT;AACV,MAAI,OAAO,WAAW,EAAG,QAAO,CAAC;AACjC,WAAS,sBAAsB,MAAM;AACrC,QAAM,UAAU,oBAAI,IAAY;AAChC,MAAI,SAAmB,CAAC;AAExB,WAAS,OAAO,GAAG,QAAQ,OAAO,QAAQ,QAAQ;AAChD,UAAM,OAAO,YAAY,QAAQ,OAAO;AACxC,UAAM,QAAQ,OAAO,OAAO,CAAC,KAAK,GAAG,MAAO,QAAQ,IAAI,CAAC,IAAI,MAAM,MAAM,KAAK,IAAI,GAAG,CAAC,GAAI,CAAC;AAC3F,aAAS,OAAO,IAAI,CAAC,GAAG,MAAM;AAC5B,UAAI,QAAQ,IAAI,CAAC,EAAG,QAAO;AAC3B,aAAO,QAAQ,IAAK,KAAK,IAAI,GAAG,CAAC,IAAI,QAAS,OAAO,QAAQ,OAAO,SAAS,QAAQ;AAAA,IACvF,CAAC;AAGD,UAAM,YAAY,OAAO,IAAI,CAAC,GAAG,MAAO,CAAC,QAAQ,IAAI,CAAC,KAAK,IAAI,WAAW,IAAI,EAAG,EAAE,OAAO,CAAC,MAAM,KAAK,CAAC;AACvG,QAAI,UAAU,WAAW,EAAG;AAC5B,cAAU,QAAQ,CAAC,MAAM,QAAQ,IAAI,CAAC,CAAC;AAAA,EACzC;AACA,SAAO;AACT;AAUO,IAAM,iCAAiC;","names":[]}
|
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,8 @@ __export(src_exports, {
|
|
|
34
34
|
BREAKPOINTS: () => BREAKPOINTS,
|
|
35
35
|
CALLOUT_DEFAULT_WIDTH: () => CALLOUT_DEFAULT_WIDTH,
|
|
36
36
|
CARD_REFLOW_WIDTH: () => CARD_REFLOW_WIDTH,
|
|
37
|
+
CE_LABEL_ABOVE_BELOW_ROW_WIDTH: () => CE_LABEL_ABOVE_BELOW_ROW_WIDTH,
|
|
38
|
+
CE_TAB_COLUMN_MIN_WIDTH: () => CE_TAB_COLUMN_MIN_WIDTH,
|
|
37
39
|
CalloutSurface: () => CalloutSurface,
|
|
38
40
|
ColumnLayout: () => ColumnLayout,
|
|
39
41
|
CommandSurface: () => CommandSurface,
|
|
@@ -59,6 +61,7 @@ __export(src_exports, {
|
|
|
59
61
|
TwoColumnLayout: () => TwoColumnLayout,
|
|
60
62
|
calloutFormDefaults: () => calloutFormDefaults,
|
|
61
63
|
calloutWidthPx: () => calloutWidthPx,
|
|
64
|
+
ceForm: () => import_design_tokens.ceForm,
|
|
62
65
|
clampSpan: () => clampSpan,
|
|
63
66
|
colors: () => import_design_tokens.colors,
|
|
64
67
|
createDynamicsTheme: () => createDynamicsTheme,
|
|
@@ -71,11 +74,13 @@ __export(src_exports, {
|
|
|
71
74
|
fromFormDefinition: () => fromFormDefinition,
|
|
72
75
|
gridTemplateForColumns: () => gridTemplateForColumns,
|
|
73
76
|
layout: () => import_design_tokens.layout,
|
|
77
|
+
normalizeColumnShares: () => normalizeColumnShares,
|
|
74
78
|
panelCustomWidthPx: () => panelCustomWidthPx,
|
|
75
79
|
panelKindFromWidth: () => panelKindFromWidth,
|
|
76
80
|
radius: () => import_design_tokens.radius,
|
|
77
81
|
resolveBreakpoint: () => resolveBreakpoint,
|
|
78
82
|
resolveResponsiveValue: () => resolveResponsiveValue,
|
|
83
|
+
resolveTabColumnWidths: () => resolveTabColumnWidths,
|
|
79
84
|
scaleSpacing: () => scaleSpacing,
|
|
80
85
|
sectionStackingStyle: () => sectionStackingStyle,
|
|
81
86
|
spacing: () => import_design_tokens.spacing,
|
|
@@ -135,6 +140,34 @@ function clampSpan(span, columns) {
|
|
|
135
140
|
return Math.max(1, Math.min(Math.round(span), columns));
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
// src/core/ceLayout.ts
|
|
144
|
+
var CE_TAB_COLUMN_MIN_WIDTH = 340;
|
|
145
|
+
function normalizeColumnShares(shares) {
|
|
146
|
+
const clean = shares.map((s) => Number.isFinite(s) && s > 0 ? s : 0);
|
|
147
|
+
const total = clean.reduce((a, b) => a + b, 0);
|
|
148
|
+
if (total <= 0) return shares.map(() => 100 / (shares.length || 1));
|
|
149
|
+
return clean.map((s) => s / total * 100);
|
|
150
|
+
}
|
|
151
|
+
function resolveTabColumnWidths(shares, available, minWidth = CE_TAB_COLUMN_MIN_WIDTH) {
|
|
152
|
+
if (shares.length === 0) return [];
|
|
153
|
+
shares = normalizeColumnShares(shares);
|
|
154
|
+
const clamped = /* @__PURE__ */ new Set();
|
|
155
|
+
let widths = [];
|
|
156
|
+
for (let pass = 0; pass <= shares.length; pass++) {
|
|
157
|
+
const free = available - clamped.size * minWidth;
|
|
158
|
+
const total = shares.reduce((sum, s, i) => clamped.has(i) ? sum : sum + Math.max(0, s), 0);
|
|
159
|
+
widths = shares.map((s, i) => {
|
|
160
|
+
if (clamped.has(i)) return minWidth;
|
|
161
|
+
return total > 0 ? Math.max(0, s) / total * free : free / (shares.length - clamped.size);
|
|
162
|
+
});
|
|
163
|
+
const violators = widths.map((w, i) => !clamped.has(i) && w < minWidth ? i : -1).filter((i) => i >= 0);
|
|
164
|
+
if (violators.length === 0) break;
|
|
165
|
+
violators.forEach((i) => clamped.add(i));
|
|
166
|
+
}
|
|
167
|
+
return widths;
|
|
168
|
+
}
|
|
169
|
+
var CE_LABEL_ABOVE_BELOW_ROW_WIDTH = 340;
|
|
170
|
+
|
|
138
171
|
// src/responsive/breakpoints.ts
|
|
139
172
|
var BREAKPOINTS = {
|
|
140
173
|
xs: 0,
|
|
@@ -1170,6 +1203,8 @@ function fromFormDefinition(def) {
|
|
|
1170
1203
|
BREAKPOINTS,
|
|
1171
1204
|
CALLOUT_DEFAULT_WIDTH,
|
|
1172
1205
|
CARD_REFLOW_WIDTH,
|
|
1206
|
+
CE_LABEL_ABOVE_BELOW_ROW_WIDTH,
|
|
1207
|
+
CE_TAB_COLUMN_MIN_WIDTH,
|
|
1173
1208
|
CalloutSurface,
|
|
1174
1209
|
ColumnLayout,
|
|
1175
1210
|
CommandSurface,
|
|
@@ -1195,6 +1230,7 @@ function fromFormDefinition(def) {
|
|
|
1195
1230
|
TwoColumnLayout,
|
|
1196
1231
|
calloutFormDefaults,
|
|
1197
1232
|
calloutWidthPx,
|
|
1233
|
+
ceForm,
|
|
1198
1234
|
clampSpan,
|
|
1199
1235
|
colors,
|
|
1200
1236
|
createDynamicsTheme,
|
|
@@ -1207,11 +1243,13 @@ function fromFormDefinition(def) {
|
|
|
1207
1243
|
fromFormDefinition,
|
|
1208
1244
|
gridTemplateForColumns,
|
|
1209
1245
|
layout,
|
|
1246
|
+
normalizeColumnShares,
|
|
1210
1247
|
panelCustomWidthPx,
|
|
1211
1248
|
panelKindFromWidth,
|
|
1212
1249
|
radius,
|
|
1213
1250
|
resolveBreakpoint,
|
|
1214
1251
|
resolveResponsiveValue,
|
|
1252
|
+
resolveTabColumnWidths,
|
|
1215
1253
|
scaleSpacing,
|
|
1216
1254
|
sectionStackingStyle,
|
|
1217
1255
|
spacing,
|