@dataverse-kit/surface-kit 0.4.0 → 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 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
- export { CALLOUT_DEFAULT_WIDTH, DIALOG_SIZE_PX, type DialogSizePreset, MODAL_AUTOSIZE_MIN_WIDTH, PANEL_DEFAULT_WIDTH, type PanelKind, calloutFormDefaults, calloutWidthPx, dialogHeightCss, dialogWidthCss, dialogWidthPx, panelCustomWidthPx, panelKindFromWidth };
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
- export { CALLOUT_DEFAULT_WIDTH, DIALOG_SIZE_PX, type DialogSizePreset, MODAL_AUTOSIZE_MIN_WIDTH, PANEL_DEFAULT_WIDTH, type PanelKind, calloutFormDefaults, calloutWidthPx, dialogHeightCss, dialogWidthCss, dialogWidthPx, panelCustomWidthPx, panelKindFromWidth };
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,