@echelon-foundry/design-system 0.12.0 → 0.13.0-main.263.1

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/CHANGELOG.md CHANGED
@@ -7,6 +7,55 @@ published as GitHub release tarballs
7
7
  and to npm. Earlier releases are described in `docs/CONSUMING-FORMA.md` and
8
8
  `HANDOFF.md`.
9
9
 
10
+ ## 0.13.0
11
+
12
+ Minor release: layout reliability (LRP-001). The source document is `.praxis/inbox/documents/2026-10-30-LRP-001-layout-reliability.md`; the baseline and the Indy Init report are in `docs/reliability/`. `@echelon-foundry/forma-conformance` 0.3.0 ships the new geometry checks.
13
+
14
+ **Behaviour change: a token too long for its line now breaks inside Forma containers.** Before this release, a token without spaces kept its full width and escaped the card, dialog or field around it. This affected URLs, SHAs, paths and identifiers in a `span`, `strong`, `em`, `div` or `li`. Such a token now breaks at the container's edge. Words that fit are never split: containers pass on `overflow-wrap: break-word`, which leaves a word's minimum width alone, not `anywhere`. Content with an explicit `white-space` (`nowrap`, `pre`) keeps its wrapping, as do code blocks, truncating selects, character-grid runs and buttons. No class, token, element or attribute was removed or renamed (QD-004).
15
+
16
+ ### Fixed
17
+ - **Text containment.** `overflow-wrap: break-word` is now inherited from these Forma containers, so arbitrary application text inside them breaks only when it would overflow:
18
+ - `.ef-surface`, `.ef-stack`, `.ef-cluster`, `.ef-actions`, `.ef-grid`, `.ef-responsive-grid`, `.ef-rail`, `.ef-mosaic`, `.ef-priority-stack`, `.ef-focus-stage`, `.ef-sidebar`, `.ef-switcher`, `.ef-split-pane`;
19
+ - `.ef-dialog`, `.ef-popover`, `.ef-menu`, `.ef-form`, `.ef-field`, `.ef-validation-summary`, `.ef-record-header`.
20
+
21
+ At 320px on 0.12.0, the fixtures measured escapes of up to 716px.
22
+ - **Flex rows.** Children of `.ef-cluster` and `.ef-actions` are capped at the row's width (`max-inline-size: 100%`). This also caps their automatic flex minimum, so a long label no longer pushes the row past its container; it breaks inside the row. Items that fit keep their content size, and no control is shrunk below its label. A `dialog` or `[popover]` authored inside the row is exempt. A full-bleed child that uses negative margins is now capped at the row's width.
23
+ - **Record header.** The type label can break when it is wider than a narrow header (320px at 200% text).
24
+ - **Command palette.** A command's label is capped at its button's width, so a long label no longer overflows the palette.
25
+ - **WebKit horizontal scrolling in forms.** A `select` with a long option inside `.ef-form-row` added the option's width to the page's horizontal scroll in WebKit/Safari. It now gets layout containment, which keeps that width inside the select; the subgrid alignment is kept.
26
+ - **Focus on date and time pickers.** In Chromium, keyboard focus on the picker button of a date, time, datetime-local, month or week input painted no focus indicator. It now shows the Forma focus ring. The rule applies only to that case (`:focus-within:not(:focus)`), so pointer focus on the input itself keeps the native behaviour.
27
+ - **Focus scrolling.** Focus-visible elements have `scroll-margin: 0.5rem`, so tabbing to an element at the viewport edge brings its whole focus ring into view. Chromium and WebKit honour this; Firefox does not.
28
+ - Side effect: in WebKit, tabbing into a mandatory scroll-snap container offsets the snap position by that margin.
29
+
30
+ ### Added
31
+ - **Design-token validator (`tools/TokenValidator`).** It is written in F#, has no dependencies, and ships in the package. It validates `var(--ef-*)` references and `--ef-*` declarations against the Forma installation itself. That installation's token sources, built CSS and color contract are the source of truth. It reports:
32
+ - `unknown-token`, even when the reference has a fallback;
33
+ - `deprecated-token` (DTCG `$deprecated`);
34
+ - `namespace`: `--forma-*`, `--ef_*`, a dropped segment such as `--ef-spacing-4`, or an application minting a new `--ef-*` name;
35
+ - `internal-variable`;
36
+ - custom-property `cycle`, judged per rule and at-rule context.
37
+
38
+ Strings and comments are inert. With `--self` it also checks Forma's own `invalid-alias`, alias `cycle`, `theme-missing` (token themes and brand manifests) and `missing-definition`. Application-owned custom properties are never reported.
39
+ - Forma itself: `npm run tokens:validate`, which is now part of `check` and `release:check`. These scripts therefore need the .NET SDK.
40
+ - Applications: `dotnet run --project node_modules/@echelon-foundry/design-system/tools/TokenValidator/TokenValidator.fsproj -- --forma node_modules/@echelon-foundry/design-system src/`. See `docs/CONSUMING-FORMA.md`, "Design-token validation".
41
+ - **`@echelon-foundry/forma-conformance` 0.3.0**, which also publishes the 0.11.0 corner-containment helpers:
42
+ - `measureContainment`, `expectContained` and `evaluateContainment` (ULI-L05 `contain.element`) check that every descendant box and text line stays inside a component's content box.
43
+ - `measureFocusVisibility`, `expectFocusVisible` and `evaluateFocusVisibility` (WCAG 2.4.7/2.4.11 `focus.unclipped`) check that a visible outline or outward box-shadow is painted and that no hard clip cuts it.
44
+ - **Browser geometry suite.**
45
+ - `tests/browser/layout-reliability.spec.mjs` covers 11 adversarial fixtures at 320, 375, 768 and 1280px and at 200% text. It also runs dialog, dropdown, focus, media, theme, skin and axe checks. On 0.12.0 it failed 48 of 78 checks across Chromium, Firefox and WebKit.
46
+ - `tests/browser/word-integrity.spec.mjs` checks that no canonical pattern or fixture splits a word that fits its box.
47
+ - `tests/browser/causa-representative.spec.mjs` covers Causa-shaped regions at phone, iPad portrait, iPad landscape and desktop sizes.
48
+
49
+ ### Known limitations
50
+ - `.ef-dashboard-grid`, `.ef-split-pane` and `.ef-operation-status` reflow only at a viewport media query. Under page-level 200% text at iPad portrait width they stay wider than their region. This is the same class as the alert's viewport-based stacking. The Causa spec asserts these still fail, so a fix is noticed.
51
+ - A narrowed `<svg>` with `width`/`height` attributes keeps its attribute height, so its box is taller than the drawing; the drawing itself is letterboxed, not distorted.
52
+ - No single declaration fixes this without a regression in some engine. `block-size: auto` makes icons stretch in flex rows in every engine. The intrinsic keywords avoid that stretch, but WebKit does not resize an SVG with them.
53
+ - Workaround: give such charts `block-size: auto` yourself, or omit the `height` attribute.
54
+
55
+ ## @echelon-foundry/forma-workflow 1.2.0
56
+
57
+ Minor release of the workflow package (and `EchelonFoundry.Forma.Workflow` 1.2.0): it ships the Forma 0.12.0 engine. **Behaviour change: validation is stricter.** Documents that validated under 1.1.0 can now fail with `A11Y-NONTEXT-CONTRAST` (WCAG 1.4.11, 3:1), and `render` refuses documents that fail validation. Added: `token:status-warning|danger|success|info` diagram colours, `ValidationOptions.Theme` and `--theme` on `validate`/`render`. No element, class, attribute or token was removed or renamed (QD-004), so this is a minor release, as Forma 0.12.0 was. See 0.12.0 below for the migration.
58
+
10
59
  ## 0.12.0
11
60
 
12
61
  Minor release: diagram non-text contrast and status tokens.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "format": "forma.color-contract/1.0.0",
3
- "formaVersion": "0.12.0",
3
+ "formaVersion": "0.13.0",
4
4
  "validationScope": "CSS var() references to --ef-color-* plus reserved internal color-related prefixes; other Forma APIs are governed by their own contracts.",
5
5
  "compatibility": {
6
6
  "policy": "Validate with the contract shipped by the pinned Forma package version.",
package/dist/all.css CHANGED
@@ -794,6 +794,20 @@
794
794
  outline: 3px solid var(--ef-color-focus-gap, #f2efe7);
795
795
  outline-offset: 2px;
796
796
  box-shadow: 0 0 0 5px var(--ef-color-focus-ring, #171a18);
797
+ /* Focus scrolling brings the whole indicator into view, not only the
798
+ element's border box (LRP-001). Changes scroll position only. */
799
+ scroll-margin: 0.5rem;
800
+ }
801
+
802
+ /* Keyboard focus on a date or time control's own picker button leaves the
803
+ host input :focus-within but neither :focus nor :focus-visible
804
+ (Chromium), which painted no indicator at all (LRP-001). :not(:focus)
805
+ limits this to that case: pointer focus on the input itself (WebKit)
806
+ keeps the native behaviour. */
807
+ :where(input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"], input[type="week"]):focus-within:not(:focus) {
808
+ outline: 3px solid var(--ef-color-focus-gap, #f2efe7);
809
+ outline-offset: 2px;
810
+ box-shadow: 0 0 0 5px var(--ef-color-focus-ring, #171a18);
797
811
  }
798
812
 
799
813
  .ef-stack { display: flex; flex-direction: column; gap: var(--ef-stack-gap, var(--ef-primitive-spacing-4, 1rem)); }
@@ -811,6 +825,39 @@
811
825
  overflow-wrap: anywhere;
812
826
  }
813
827
 
828
+ /*
829
+ * Layout reliability (LRP-001, docs/reliability/layout-baseline.md).
830
+ * Forma containers that hold arbitrary application content pass
831
+ * overflow-wrap: break-word to that content by inheritance, so a long token
832
+ * in a span, strong, em, div or li breaks instead of escaping the card,
833
+ * dialog or field. break-word, unlike anywhere, leaves min-content sizes
834
+ * alone: a word that fits is never split, and auto-sized flex and grid
835
+ * items keep whole words. Inheritance never overrides a declared value:
836
+ * text elements above keep anywhere, character-grid runs keep normal, and
837
+ * white-space: pre or nowrap content (code blocks, truncating selects,
838
+ * scrollers) does not wrap.
839
+ */
840
+ :where(
841
+ .ef-surface, .ef-stack, .ef-cluster, .ef-actions, .ef-grid, .ef-responsive-grid,
842
+ .ef-rail, .ef-mosaic, .ef-priority-stack, .ef-focus-stage, .ef-sidebar, .ef-switcher, .ef-split-pane,
843
+ .ef-dialog, .ef-popover, .ef-menu, .ef-form, .ef-field, .ef-validation-summary, .ef-record-header
844
+ ) {
845
+ overflow-wrap: break-word;
846
+ }
847
+
848
+ /*
849
+ * A wrapping row's item can be no wider than the row (LRP-001), which also
850
+ * caps its automatic flex minimum, so a long label or an inline-block item
851
+ * cannot push the row past its container; the item's text then breaks
852
+ * inside it. Items keep their content size whenever it fits: nothing here
853
+ * shrinks a control below its label. Top-layer children (a dialog or
854
+ * popover authored inside the row) keep their own sizing. A full-bleed
855
+ * child with negative margins is capped at the row's width.
856
+ */
857
+ :where(.ef-cluster, .ef-actions) > :not(dialog, [popover]) {
858
+ max-inline-size: 100%;
859
+ }
860
+
814
861
  /* Action labels never split mid-word because their container allows it
815
862
  (a link button inherits the rule above; a button inherits it from a
816
863
  table cell). A single word wider than its container may still break
@@ -1075,6 +1122,12 @@
1075
1122
  .ef-form-row > .ef-field > .ef-field__description { grid-row: 2; margin-block-start: var(--ef-primitive-spacing-2, 0.5rem); }
1076
1123
  .ef-form-row > .ef-field > :not(.ef-field__label, .ef-field__description, .ef-validation-message) { grid-row: 3; margin-block-start: var(--ef-primitive-spacing-2, 0.5rem); }
1077
1124
  .ef-form-row > .ef-field > .ef-validation-message { grid-row: 4; margin-block-start: var(--ef-primitive-spacing-2, 0.5rem); }
1125
+ /* WebKit lets a select's longest option add scrollable overflow through
1126
+ the subgrid, so a 320px form scrolled sideways by the option's width
1127
+ (LRP-001; Safari is the primary iPad target). Layout containment keeps
1128
+ it inside the select. Rows align at start, so the lost baseline is
1129
+ unused here. */
1130
+ .ef-form-row > .ef-field > select { contain: layout; }
1078
1131
 
1079
1132
  /* Form contract ------------------------------------------------------- */
1080
1133
  .ef-form {
@@ -3044,6 +3097,9 @@
3044
3097
  border-inline: 0;
3045
3098
  border-block-end: 0;
3046
3099
  }
3100
+ /* A command's label is no wider than its button, so a long label breaks
3101
+ inside the button instead of overflowing the palette (LRP-001). */
3102
+ .ef-command-palette__group button > * { max-inline-size: 100%; }
3047
3103
  .ef-command-palette__group button:last-child { border-block-end: 1px solid var(--ef-color-border-functional, #3a403c); }
3048
3104
  .ef-command-palette kbd {
3049
3105
  padding: 0.1rem 0.35rem;
@@ -3178,7 +3234,9 @@
3178
3234
  row-gap: 0;
3179
3235
  min-inline-size: 0;
3180
3236
  }
3181
- .ef-record-header__title > :first-child > .ef-record-header__type { grid-row: 1; align-self: end; }
3237
+ /* A type label wider than a narrow header (320px at 200% text) breaks
3238
+ rather than setting the column's minimum and overflowing (LRP-001). */
3239
+ .ef-record-header__title > :first-child > .ef-record-header__type { grid-row: 1; align-self: end; overflow-wrap: anywhere; }
3182
3240
  .ef-record-header__title > :first-child > :is(h1, h2, h3, h4) { grid-row: 2; }
3183
3241
  .ef-record-header__title > :not(:first-child) { grid-row: 2; }
3184
3242
  @container (max-width: 24em) {
@@ -342,13 +342,13 @@
342
342
  "tokens.css"
343
343
  ],
344
344
  "bytes": {
345
- "all.css": 341701,
345
+ "all.css": 344943,
346
346
  "assessment.css": 36343,
347
347
  "brands/echelon.css": 4213,
348
348
  "brands/example-harbor.css": 4229,
349
349
  "brands/index.json": 335,
350
- "components.css": 213356,
351
- "foundations.css": 20395,
350
+ "components.css": 214119,
351
+ "foundations.css": 22874,
352
352
  "icons/add.svg": 216,
353
353
  "icons/agent.svg": 354,
354
354
  "icons/archive.svg": 248,
@@ -63,6 +63,12 @@
63
63
  .ef-form-row > .ef-field > .ef-field__description { grid-row: 2; margin-block-start: var(--ef-primitive-spacing-2, 0.5rem); }
64
64
  .ef-form-row > .ef-field > :not(.ef-field__label, .ef-field__description, .ef-validation-message) { grid-row: 3; margin-block-start: var(--ef-primitive-spacing-2, 0.5rem); }
65
65
  .ef-form-row > .ef-field > .ef-validation-message { grid-row: 4; margin-block-start: var(--ef-primitive-spacing-2, 0.5rem); }
66
+ /* WebKit lets a select's longest option add scrollable overflow through
67
+ the subgrid, so a 320px form scrolled sideways by the option's width
68
+ (LRP-001; Safari is the primary iPad target). Layout containment keeps
69
+ it inside the select. Rows align at start, so the lost baseline is
70
+ unused here. */
71
+ .ef-form-row > .ef-field > select { contain: layout; }
66
72
 
67
73
  /* Form contract ------------------------------------------------------- */
68
74
  .ef-form {
@@ -2032,6 +2038,9 @@
2032
2038
  border-inline: 0;
2033
2039
  border-block-end: 0;
2034
2040
  }
2041
+ /* A command's label is no wider than its button, so a long label breaks
2042
+ inside the button instead of overflowing the palette (LRP-001). */
2043
+ .ef-command-palette__group button > * { max-inline-size: 100%; }
2035
2044
  .ef-command-palette__group button:last-child { border-block-end: 1px solid var(--ef-color-border-functional, #3a403c); }
2036
2045
  .ef-command-palette kbd {
2037
2046
  padding: 0.1rem 0.35rem;
@@ -2166,7 +2175,9 @@
2166
2175
  row-gap: 0;
2167
2176
  min-inline-size: 0;
2168
2177
  }
2169
- .ef-record-header__title > :first-child > .ef-record-header__type { grid-row: 1; align-self: end; }
2178
+ /* A type label wider than a narrow header (320px at 200% text) breaks
2179
+ rather than setting the column's minimum and overflowing (LRP-001). */
2180
+ .ef-record-header__title > :first-child > .ef-record-header__type { grid-row: 1; align-self: end; overflow-wrap: anywhere; }
2170
2181
  .ef-record-header__title > :first-child > :is(h1, h2, h3, h4) { grid-row: 2; }
2171
2182
  .ef-record-header__title > :not(:first-child) { grid-row: 2; }
2172
2183
  @container (max-width: 24em) {
@@ -183,6 +183,20 @@
183
183
  outline: 3px solid var(--ef-color-focus-gap, #f2efe7);
184
184
  outline-offset: 2px;
185
185
  box-shadow: 0 0 0 5px var(--ef-color-focus-ring, #171a18);
186
+ /* Focus scrolling brings the whole indicator into view, not only the
187
+ element's border box (LRP-001). Changes scroll position only. */
188
+ scroll-margin: 0.5rem;
189
+ }
190
+
191
+ /* Keyboard focus on a date or time control's own picker button leaves the
192
+ host input :focus-within but neither :focus nor :focus-visible
193
+ (Chromium), which painted no indicator at all (LRP-001). :not(:focus)
194
+ limits this to that case: pointer focus on the input itself (WebKit)
195
+ keeps the native behaviour. */
196
+ :where(input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"], input[type="week"]):focus-within:not(:focus) {
197
+ outline: 3px solid var(--ef-color-focus-gap, #f2efe7);
198
+ outline-offset: 2px;
199
+ box-shadow: 0 0 0 5px var(--ef-color-focus-ring, #171a18);
186
200
  }
187
201
 
188
202
  .ef-stack { display: flex; flex-direction: column; gap: var(--ef-stack-gap, var(--ef-primitive-spacing-4, 1rem)); }
@@ -200,6 +214,39 @@
200
214
  overflow-wrap: anywhere;
201
215
  }
202
216
 
217
+ /*
218
+ * Layout reliability (LRP-001, docs/reliability/layout-baseline.md).
219
+ * Forma containers that hold arbitrary application content pass
220
+ * overflow-wrap: break-word to that content by inheritance, so a long token
221
+ * in a span, strong, em, div or li breaks instead of escaping the card,
222
+ * dialog or field. break-word, unlike anywhere, leaves min-content sizes
223
+ * alone: a word that fits is never split, and auto-sized flex and grid
224
+ * items keep whole words. Inheritance never overrides a declared value:
225
+ * text elements above keep anywhere, character-grid runs keep normal, and
226
+ * white-space: pre or nowrap content (code blocks, truncating selects,
227
+ * scrollers) does not wrap.
228
+ */
229
+ :where(
230
+ .ef-surface, .ef-stack, .ef-cluster, .ef-actions, .ef-grid, .ef-responsive-grid,
231
+ .ef-rail, .ef-mosaic, .ef-priority-stack, .ef-focus-stage, .ef-sidebar, .ef-switcher, .ef-split-pane,
232
+ .ef-dialog, .ef-popover, .ef-menu, .ef-form, .ef-field, .ef-validation-summary, .ef-record-header
233
+ ) {
234
+ overflow-wrap: break-word;
235
+ }
236
+
237
+ /*
238
+ * A wrapping row's item can be no wider than the row (LRP-001), which also
239
+ * caps its automatic flex minimum, so a long label or an inline-block item
240
+ * cannot push the row past its container; the item's text then breaks
241
+ * inside it. Items keep their content size whenever it fits: nothing here
242
+ * shrinks a control below its label. Top-layer children (a dialog or
243
+ * popover authored inside the row) keep their own sizing. A full-bleed
244
+ * child with negative margins is capped at the row's width.
245
+ */
246
+ :where(.ef-cluster, .ef-actions) > :not(dialog, [popover]) {
247
+ max-inline-size: 100%;
248
+ }
249
+
203
250
  /* Action labels never split mid-word because their container allows it
204
251
  (a link button inherits the rule above; a button inherits it from a
205
252
  table cell). A single word wider than its container may still break
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "formaVersion": "0.12.0",
3
+ "formaVersion": "0.13.0",
4
4
  "grid": 24,
5
5
  "icons": [
6
6
  {
@@ -1,4 +1,4 @@
1
- /*! Forma 0.12.0 | forma-all.css | bundle echelon-marketing
1
+ /*! Forma 0.13.0 | forma-all.css | bundle echelon-marketing
2
2
  Complete Forma surface (application components, assessment, and marketing layers) for sites that also present Forma application patterns such as data grids or status lozenges.
3
3
  Generated by tools/PresentationBundler. Do not edit; pin by version and sha256. */
4
4
 
@@ -799,6 +799,20 @@
799
799
  outline: 3px solid var(--ef-color-focus-gap, #f2efe7);
800
800
  outline-offset: 2px;
801
801
  box-shadow: 0 0 0 5px var(--ef-color-focus-ring, #171a18);
802
+ /* Focus scrolling brings the whole indicator into view, not only the
803
+ element's border box (LRP-001). Changes scroll position only. */
804
+ scroll-margin: 0.5rem;
805
+ }
806
+
807
+ /* Keyboard focus on a date or time control's own picker button leaves the
808
+ host input :focus-within but neither :focus nor :focus-visible
809
+ (Chromium), which painted no indicator at all (LRP-001). :not(:focus)
810
+ limits this to that case: pointer focus on the input itself (WebKit)
811
+ keeps the native behaviour. */
812
+ :where(input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"], input[type="week"]):focus-within:not(:focus) {
813
+ outline: 3px solid var(--ef-color-focus-gap, #f2efe7);
814
+ outline-offset: 2px;
815
+ box-shadow: 0 0 0 5px var(--ef-color-focus-ring, #171a18);
802
816
  }
803
817
 
804
818
  .ef-stack { display: flex; flex-direction: column; gap: var(--ef-stack-gap, var(--ef-primitive-spacing-4, 1rem)); }
@@ -816,6 +830,39 @@
816
830
  overflow-wrap: anywhere;
817
831
  }
818
832
 
833
+ /*
834
+ * Layout reliability (LRP-001, docs/reliability/layout-baseline.md).
835
+ * Forma containers that hold arbitrary application content pass
836
+ * overflow-wrap: break-word to that content by inheritance, so a long token
837
+ * in a span, strong, em, div or li breaks instead of escaping the card,
838
+ * dialog or field. break-word, unlike anywhere, leaves min-content sizes
839
+ * alone: a word that fits is never split, and auto-sized flex and grid
840
+ * items keep whole words. Inheritance never overrides a declared value:
841
+ * text elements above keep anywhere, character-grid runs keep normal, and
842
+ * white-space: pre or nowrap content (code blocks, truncating selects,
843
+ * scrollers) does not wrap.
844
+ */
845
+ :where(
846
+ .ef-surface, .ef-stack, .ef-cluster, .ef-actions, .ef-grid, .ef-responsive-grid,
847
+ .ef-rail, .ef-mosaic, .ef-priority-stack, .ef-focus-stage, .ef-sidebar, .ef-switcher, .ef-split-pane,
848
+ .ef-dialog, .ef-popover, .ef-menu, .ef-form, .ef-field, .ef-validation-summary, .ef-record-header
849
+ ) {
850
+ overflow-wrap: break-word;
851
+ }
852
+
853
+ /*
854
+ * A wrapping row's item can be no wider than the row (LRP-001), which also
855
+ * caps its automatic flex minimum, so a long label or an inline-block item
856
+ * cannot push the row past its container; the item's text then breaks
857
+ * inside it. Items keep their content size whenever it fits: nothing here
858
+ * shrinks a control below its label. Top-layer children (a dialog or
859
+ * popover authored inside the row) keep their own sizing. A full-bleed
860
+ * child with negative margins is capped at the row's width.
861
+ */
862
+ :where(.ef-cluster, .ef-actions) > :not(dialog, [popover]) {
863
+ max-inline-size: 100%;
864
+ }
865
+
819
866
  /* Action labels never split mid-word because their container allows it
820
867
  (a link button inherits the rule above; a button inherits it from a
821
868
  table cell). A single word wider than its container may still break
@@ -1080,6 +1127,12 @@
1080
1127
  .ef-form-row > .ef-field > .ef-field__description { grid-row: 2; margin-block-start: var(--ef-primitive-spacing-2, 0.5rem); }
1081
1128
  .ef-form-row > .ef-field > :not(.ef-field__label, .ef-field__description, .ef-validation-message) { grid-row: 3; margin-block-start: var(--ef-primitive-spacing-2, 0.5rem); }
1082
1129
  .ef-form-row > .ef-field > .ef-validation-message { grid-row: 4; margin-block-start: var(--ef-primitive-spacing-2, 0.5rem); }
1130
+ /* WebKit lets a select's longest option add scrollable overflow through
1131
+ the subgrid, so a 320px form scrolled sideways by the option's width
1132
+ (LRP-001; Safari is the primary iPad target). Layout containment keeps
1133
+ it inside the select. Rows align at start, so the lost baseline is
1134
+ unused here. */
1135
+ .ef-form-row > .ef-field > select { contain: layout; }
1083
1136
 
1084
1137
  /* Form contract ------------------------------------------------------- */
1085
1138
  .ef-form {
@@ -3049,6 +3102,9 @@
3049
3102
  border-inline: 0;
3050
3103
  border-block-end: 0;
3051
3104
  }
3105
+ /* A command's label is no wider than its button, so a long label breaks
3106
+ inside the button instead of overflowing the palette (LRP-001). */
3107
+ .ef-command-palette__group button > * { max-inline-size: 100%; }
3052
3108
  .ef-command-palette__group button:last-child { border-block-end: 1px solid var(--ef-color-border-functional, #3a403c); }
3053
3109
  .ef-command-palette kbd {
3054
3110
  padding: 0.1rem 0.35rem;
@@ -3183,7 +3239,9 @@
3183
3239
  row-gap: 0;
3184
3240
  min-inline-size: 0;
3185
3241
  }
3186
- .ef-record-header__title > :first-child > .ef-record-header__type { grid-row: 1; align-self: end; }
3242
+ /* A type label wider than a narrow header (320px at 200% text) breaks
3243
+ rather than setting the column's minimum and overflowing (LRP-001). */
3244
+ .ef-record-header__title > :first-child > .ef-record-header__type { grid-row: 1; align-self: end; overflow-wrap: anywhere; }
3187
3245
  .ef-record-header__title > :first-child > :is(h1, h2, h3, h4) { grid-row: 2; }
3188
3246
  .ef-record-header__title > :not(:first-child) { grid-row: 2; }
3189
3247
  @container (max-width: 24em) {
@@ -1,4 +1,4 @@
1
- /*! Forma 0.12.0 | forma-echelon-marketing.css | bundle echelon-marketing
1
+ /*! Forma 0.13.0 | forma-echelon-marketing.css | bundle echelon-marketing
2
2
  Single-file Echelon marketing presentation: forma-marketing.css followed by the Echelon theme. Most sites load only this file.
3
3
  Generated by tools/PresentationBundler. Do not edit; pin by version and sha256. */
4
4
 
@@ -903,6 +903,20 @@
903
903
  outline: 3px solid var(--ef-color-focus-gap, #f2efe7);
904
904
  outline-offset: 2px;
905
905
  box-shadow: 0 0 0 5px var(--ef-color-focus-ring, #171a18);
906
+ /* Focus scrolling brings the whole indicator into view, not only the
907
+ element's border box (LRP-001). Changes scroll position only. */
908
+ scroll-margin: 0.5rem;
909
+ }
910
+
911
+ /* Keyboard focus on a date or time control's own picker button leaves the
912
+ host input :focus-within but neither :focus nor :focus-visible
913
+ (Chromium), which painted no indicator at all (LRP-001). :not(:focus)
914
+ limits this to that case: pointer focus on the input itself (WebKit)
915
+ keeps the native behaviour. */
916
+ :where(input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"], input[type="week"]):focus-within:not(:focus) {
917
+ outline: 3px solid var(--ef-color-focus-gap, #f2efe7);
918
+ outline-offset: 2px;
919
+ box-shadow: 0 0 0 5px var(--ef-color-focus-ring, #171a18);
906
920
  }
907
921
 
908
922
  .ef-stack { display: flex; flex-direction: column; gap: var(--ef-stack-gap, var(--ef-primitive-spacing-4, 1rem)); }
@@ -920,6 +934,39 @@
920
934
  overflow-wrap: anywhere;
921
935
  }
922
936
 
937
+ /*
938
+ * Layout reliability (LRP-001, docs/reliability/layout-baseline.md).
939
+ * Forma containers that hold arbitrary application content pass
940
+ * overflow-wrap: break-word to that content by inheritance, so a long token
941
+ * in a span, strong, em, div or li breaks instead of escaping the card,
942
+ * dialog or field. break-word, unlike anywhere, leaves min-content sizes
943
+ * alone: a word that fits is never split, and auto-sized flex and grid
944
+ * items keep whole words. Inheritance never overrides a declared value:
945
+ * text elements above keep anywhere, character-grid runs keep normal, and
946
+ * white-space: pre or nowrap content (code blocks, truncating selects,
947
+ * scrollers) does not wrap.
948
+ */
949
+ :where(
950
+ .ef-surface, .ef-stack, .ef-cluster, .ef-actions, .ef-grid, .ef-responsive-grid,
951
+ .ef-rail, .ef-mosaic, .ef-priority-stack, .ef-focus-stage, .ef-sidebar, .ef-switcher, .ef-split-pane,
952
+ .ef-dialog, .ef-popover, .ef-menu, .ef-form, .ef-field, .ef-validation-summary, .ef-record-header
953
+ ) {
954
+ overflow-wrap: break-word;
955
+ }
956
+
957
+ /*
958
+ * A wrapping row's item can be no wider than the row (LRP-001), which also
959
+ * caps its automatic flex minimum, so a long label or an inline-block item
960
+ * cannot push the row past its container; the item's text then breaks
961
+ * inside it. Items keep their content size whenever it fits: nothing here
962
+ * shrinks a control below its label. Top-layer children (a dialog or
963
+ * popover authored inside the row) keep their own sizing. A full-bleed
964
+ * child with negative margins is capped at the row's width.
965
+ */
966
+ :where(.ef-cluster, .ef-actions) > :not(dialog, [popover]) {
967
+ max-inline-size: 100%;
968
+ }
969
+
923
970
  /* Action labels never split mid-word because their container allows it
924
971
  (a link button inherits the rule above; a button inherits it from a
925
972
  table cell). A single word wider than its container may still break
@@ -1,4 +1,4 @@
1
- /*! Forma 0.12.0 | forma-marketing-theme-echelon.css | bundle echelon-marketing
1
+ /*! Forma 0.13.0 | forma-marketing-theme-echelon.css | bundle echelon-marketing
2
2
  Echelon Marketing Theme: compiled theme tokens and theme hooks. Load after forma-marketing.css.
3
3
  Generated by tools/PresentationBundler. Do not edit; pin by version and sha256. */
4
4
 
@@ -1,4 +1,4 @@
1
- /*! Forma 0.12.0 | forma-marketing.css | bundle echelon-marketing
1
+ /*! Forma 0.13.0 | forma-marketing.css | bundle echelon-marketing
2
2
  Brand-neutral marketing layer: core tokens, role defaults, Forma foundations, marketing foundations, components, and layouts.
3
3
  Generated by tools/PresentationBundler. Do not edit; pin by version and sha256. */
4
4
 
@@ -902,6 +902,20 @@
902
902
  outline: 3px solid var(--ef-color-focus-gap, #f2efe7);
903
903
  outline-offset: 2px;
904
904
  box-shadow: 0 0 0 5px var(--ef-color-focus-ring, #171a18);
905
+ /* Focus scrolling brings the whole indicator into view, not only the
906
+ element's border box (LRP-001). Changes scroll position only. */
907
+ scroll-margin: 0.5rem;
908
+ }
909
+
910
+ /* Keyboard focus on a date or time control's own picker button leaves the
911
+ host input :focus-within but neither :focus nor :focus-visible
912
+ (Chromium), which painted no indicator at all (LRP-001). :not(:focus)
913
+ limits this to that case: pointer focus on the input itself (WebKit)
914
+ keeps the native behaviour. */
915
+ :where(input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"], input[type="week"]):focus-within:not(:focus) {
916
+ outline: 3px solid var(--ef-color-focus-gap, #f2efe7);
917
+ outline-offset: 2px;
918
+ box-shadow: 0 0 0 5px var(--ef-color-focus-ring, #171a18);
905
919
  }
906
920
 
907
921
  .ef-stack { display: flex; flex-direction: column; gap: var(--ef-stack-gap, var(--ef-primitive-spacing-4, 1rem)); }
@@ -919,6 +933,39 @@
919
933
  overflow-wrap: anywhere;
920
934
  }
921
935
 
936
+ /*
937
+ * Layout reliability (LRP-001, docs/reliability/layout-baseline.md).
938
+ * Forma containers that hold arbitrary application content pass
939
+ * overflow-wrap: break-word to that content by inheritance, so a long token
940
+ * in a span, strong, em, div or li breaks instead of escaping the card,
941
+ * dialog or field. break-word, unlike anywhere, leaves min-content sizes
942
+ * alone: a word that fits is never split, and auto-sized flex and grid
943
+ * items keep whole words. Inheritance never overrides a declared value:
944
+ * text elements above keep anywhere, character-grid runs keep normal, and
945
+ * white-space: pre or nowrap content (code blocks, truncating selects,
946
+ * scrollers) does not wrap.
947
+ */
948
+ :where(
949
+ .ef-surface, .ef-stack, .ef-cluster, .ef-actions, .ef-grid, .ef-responsive-grid,
950
+ .ef-rail, .ef-mosaic, .ef-priority-stack, .ef-focus-stage, .ef-sidebar, .ef-switcher, .ef-split-pane,
951
+ .ef-dialog, .ef-popover, .ef-menu, .ef-form, .ef-field, .ef-validation-summary, .ef-record-header
952
+ ) {
953
+ overflow-wrap: break-word;
954
+ }
955
+
956
+ /*
957
+ * A wrapping row's item can be no wider than the row (LRP-001), which also
958
+ * caps its automatic flex minimum, so a long label or an inline-block item
959
+ * cannot push the row past its container; the item's text then breaks
960
+ * inside it. Items keep their content size whenever it fits: nothing here
961
+ * shrinks a control below its label. Top-layer children (a dialog or
962
+ * popover authored inside the row) keep their own sizing. A full-bleed
963
+ * child with negative margins is capped at the row's width.
964
+ */
965
+ :where(.ef-cluster, .ef-actions) > :not(dialog, [popover]) {
966
+ max-inline-size: 100%;
967
+ }
968
+
922
969
  /* Action labels never split mid-word because their container allows it
923
970
  (a link button inherits the rule above; a button inherits it from a
924
971
  table cell). A single word wider than its container may still break
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "forma": "0.12.0",
3
+ "forma": "0.13.0",
4
4
  "bundle": "echelon-marketing",
5
5
  "description": "Forma presentation artifacts for Echelon marketing sites. Every artifact is a single self-contained CSS file with no imports or relative URLs.",
6
- "releaseTag": "v0.12.0",
6
+ "releaseTag": "v0.13.0",
7
7
  "artifacts": [
8
8
  {
9
9
  "file": "forma-marketing.css",
10
10
  "description": "Brand-neutral marketing layer: core tokens, role defaults, Forma foundations, marketing foundations, components, and layouts.",
11
- "bytes": 92562,
12
- "sha256": "e945b2e9f5396108446be83c8b0af50b64487b5749fc35a05e3b8582090dbd5f",
11
+ "bytes": 95041,
12
+ "sha256": "a2248c09b9c7731178f3046a245541fb240715c12b21638300d56a4fdd61819d",
13
13
  "sources": [
14
14
  "dist/tokens.css",
15
15
  "src/marketing/roles.css",
@@ -23,7 +23,7 @@
23
23
  "file": "forma-marketing-theme-echelon.css",
24
24
  "description": "Echelon Marketing Theme: compiled theme tokens and theme hooks. Load after forma-marketing.css.",
25
25
  "bytes": 6296,
26
- "sha256": "495b81a7dae75204ac9fcd2531754a2e0fa6e912009430d78d97d87807104402",
26
+ "sha256": "0b803cade4de294a2e15ac8a8482cb2448617a2558d70fc6542a4b3b9a357bf2",
27
27
  "sources": [
28
28
  "build/marketing/echelon-marketing.tokens.css",
29
29
  "themes/echelon/marketing.css"
@@ -32,8 +32,8 @@
32
32
  {
33
33
  "file": "forma-echelon-marketing.css",
34
34
  "description": "Single-file Echelon marketing presentation: forma-marketing.css followed by the Echelon theme. Most sites load only this file.",
35
- "bytes": 98702,
36
- "sha256": "f93946ab2401d3a65f10ce96bbea8c830ad1c239bcc412c8f7906dfba8f6616c",
35
+ "bytes": 101181,
36
+ "sha256": "cdf0ebfa0b60219e72d1c32dc0ac068d73255654748b55283bbb1c7a9db78a7f",
37
37
  "sources": [
38
38
  "artifact:forma-marketing.css",
39
39
  "artifact:forma-marketing-theme-echelon.css"
@@ -42,8 +42,8 @@
42
42
  {
43
43
  "file": "forma-all.css",
44
44
  "description": "Complete Forma surface (application components, assessment, and marketing layers) for sites that also present Forma application patterns such as data grids or status lozenges.",
45
- "bytes": 342064,
46
- "sha256": "8126a388b0563f6d7f40a84058035bfd6cd62e966c181ff3c055f530bc2cc32c",
45
+ "bytes": 345306,
46
+ "sha256": "403d708505fc3fe2ac7bd2c78220e98ad9236a5c82f76d1b88d7a9c4b01ee213",
47
47
  "sources": [
48
48
  "dist/all.css"
49
49
  ]
@@ -1,4 +1,4 @@
1
- e945b2e9f5396108446be83c8b0af50b64487b5749fc35a05e3b8582090dbd5f forma-marketing.css
2
- 495b81a7dae75204ac9fcd2531754a2e0fa6e912009430d78d97d87807104402 forma-marketing-theme-echelon.css
3
- f93946ab2401d3a65f10ce96bbea8c830ad1c239bcc412c8f7906dfba8f6616c forma-echelon-marketing.css
4
- 8126a388b0563f6d7f40a84058035bfd6cd62e966c181ff3c055f530bc2cc32c forma-all.css
1
+ a2248c09b9c7731178f3046a245541fb240715c12b21638300d56a4fdd61819d forma-marketing.css
2
+ 0b803cade4de294a2e15ac8a8482cb2448617a2558d70fc6542a4b3b9a357bf2 forma-marketing-theme-echelon.css
3
+ cdf0ebfa0b60219e72d1c32dc0ac068d73255654748b55283bbb1c7a9db78a7f forma-echelon-marketing.css
4
+ 403d708505fc3fe2ac7bd2c78220e98ad9236a5c82f76d1b88d7a9c4b01ee213 forma-all.css
package/dist/runtime.js CHANGED
@@ -52,6 +52,6 @@ export function registerEchelonRuntime(name, metadata, target = globalThis) {
52
52
  return installEchelonRuntimeRegistrar(target)?.(name, metadata);
53
53
  }
54
54
 
55
- export const FORMA_VERSION = "0.12.0";
55
+ export const FORMA_VERSION = "0.13.0";
56
56
 
57
57
  if (typeof window !== "undefined") registerEchelonRuntime("forma", { version: FORMA_VERSION }, window);