@neuravision/construct 2.3.2 → 2.4.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/CHANGELOG.md CHANGED
@@ -7,6 +7,51 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.4.0] - 2026-11-05
11
+
12
+ ### Added
13
+
14
+ - **Aperture app grammar:** `data-grammar="aperture"` re-maps the semantic tokens for whole
15
+ applications (paper and ink, cobalt as the only accent, 4px radii, grotesk). The source is
16
+ `tokens/grammar.aperture.json`; `build-tokens.mjs` emits one reference-only block after all theme
17
+ and system blocks and validates the default contrast pairs for every theme with the grammar
18
+ applied. New `schemas/grammar.schema.json`, signature layer `components/aperture-app.css` (seam
19
+ mark instead of the Datum edge, Datum marks hidden), story **Patterns → Aperture App** and
20
+ `docs/aperture-app.md`.
21
+ - New Aperture roles `elevated`, `accentHover`, `accentActive`, `borderSubtle` in every theme and
22
+ primitives cobalt 100/300/600/800/900 and graphite 200/300/700/900.
23
+ - **Stepper (`ct-stepper`):** progress indicator for multi-step flows. Ordered list in a labelled
24
+ `<nav>`, state from `aria-current="step"` and `data-state="completed" | "error"`, native buttons
25
+ (disabled = not reachable), `--vertical` modifier, 44px targets, labels collapse to the current step
26
+ below 600px, forced-colors support. In Aperture the finished path reads as a cobalt seam. Story
27
+ **Components → Navigation → Stepper**.
28
+ - **Bottom navigation (`ct-bottom-nav`):** primary destinations on small screens, icon + label,
29
+ current destination via `aria-current="page"` (Datum leading edge on top, Aperture split seam),
30
+ `--fixed` modifier with safe-area inset, 44px targets. Story
31
+ **Components → Navigation → Bottom Navigation**.
32
+
33
+ ### Fixed
34
+
35
+ - Control aliases (`--ct-control-*`) now re-resolve inside nested `[data-theme]` and `[data-grammar]`
36
+ scopes. Before, they were substituted once at `:root`, so inputs inside a dark section of a light
37
+ page kept the light background.
38
+ - `.ct-table-wrap` is now a containing block (`position: relative`). Absolutely positioned content in
39
+ a wide table (e.g. visually hidden header text) was not clipped by the scroll container and made
40
+ the whole page scroll sideways on phones.
41
+
42
+ ### Security
43
+
44
+ - Development toolchain updated for new advisories that failed the release audit gate: Storybook
45
+ 10.6.6 (preview origin validation and dev-server code injection, GHSA-f2ph-cmg5-wxc8,
46
+ GHSA-px85-f4q5-jh77) and Vitest 4.1.12 (`@vitest/mocker` manual-mock injection,
47
+ GHSA-qxg6-89mg-rqv8, GHSA-xp26-3454-mjgp). Development dependencies only; the published CSS,
48
+ tokens and behaviors are unchanged.
49
+
50
+ - Development toolchain updated for two new advisories that failed the CI audit gate: Vitest
51
+ 4.1.11 (`@vitest/mocker` path traversal, GHSA-82fw-gwwq-j7x9) and `source-map-js` 1.2.2
52
+ (event-loop denial of service, GHSA-68fv-2mgg-jv7q). Development dependencies only; the
53
+ published CSS, tokens and behaviors are unchanged.
54
+
10
55
  ## [2.3.2] - 2026-10-12
11
56
 
12
57
  ### Fixed
@@ -816,6 +816,44 @@ Use `ct-navbar--compact` for dense product headers. Mark expendable actions with
816
816
 
817
817
  The CSS controls presentation only. Application code must keep `aria-expanded`, `data-state`, focus movement, Escape handling, and focus return in sync.
818
818
 
819
+ ### Stepper
820
+
821
+ ```html
822
+ <nav class="ct-stepper" aria-label="Einrichtung">
823
+ <ol class="ct-stepper__list">
824
+ <li class="ct-stepper__item" data-state="completed">
825
+ <button class="ct-stepper__button" type="button">
826
+ <span class="ct-stepper__marker" aria-hidden="true">✓</span>
827
+ <span class="ct-stepper__text"><span class="ct-stepper__label">Unternehmen</span></span>
828
+ <span class="ct-stepper__sr">Schritt 1 von 3, erledigt</span>
829
+ </button>
830
+ </li>
831
+ <li class="ct-stepper__item" aria-current="step">…</li>
832
+ <li class="ct-stepper__item"><button class="ct-stepper__button" type="button" disabled>…</button></li>
833
+ </ol>
834
+ </nav>
835
+ ```
836
+
837
+ - State: `aria-current="step"` (current), `data-state="completed" | "error"`; unreachable steps are `disabled`.
838
+ - `.ct-stepper--vertical` stacks the steps; below 600px only the current step keeps its label.
839
+ - In `data-grammar="aperture"` the finished path is drawn as a seam.
840
+
841
+ ### Bottom Navigation
842
+
843
+ ```html
844
+ <nav class="ct-bottom-nav ct-bottom-nav--fixed" aria-label="Hauptnavigation">
845
+ <ul class="ct-bottom-nav__list">
846
+ <li><a class="ct-bottom-nav__link" href="/rechnungen" aria-current="page">
847
+ <svg class="ct-bottom-nav__icon" aria-hidden="true">…</svg><span class="ct-bottom-nav__label">Rechnungen</span>
848
+ </a></li>
849
+ </ul>
850
+ </nav>
851
+ ```
852
+
853
+ - 3–5 destinations; the current one has `aria-current="page"`.
854
+ - `--fixed` pins the bar to the bottom (safe-area aware). The product hides it above its breakpoint and
855
+ reserves space at the end of the content (`padding-block-end`).
856
+
819
857
  ### App Shell
820
858
 
821
859
  App Shell V2 is the recommended layout for new applications. The original `.ct-app-shell` remains a supported compatibility layout and is not being rewritten, so existing DOM and selectors keep working. Both variants now share these public override hooks:
@@ -1,4 +1,8 @@
1
- :root {
1
+ /* Re-resolve the aliases at every theme and grammar scope: custom properties are substituted where
2
+ they are declared, so a root-only declaration would freeze light values inside nested scopes. */
3
+ :root,
4
+ [data-theme],
5
+ [data-grammar] {
2
6
  --ct-control-bg: var(--component-control-background);
3
7
  --ct-control-bg-hover: var(--component-control-background-hover);
4
8
  --ct-control-bg-disabled: var(--component-control-background-disabled);
@@ -0,0 +1,120 @@
1
+ /* Aperture as app grammar — signature layer.
2
+ *
3
+ * Activated by data-grammar="aperture" (on <html> or any subtree). The token re-map itself is
4
+ * generated from tokens/grammar.aperture.json into tokens.css; this file only swaps the Datum
5
+ * signature for Aperture motifs inside that scope:
6
+ * - the orange leading edge becomes a split cobalt seam mark (46 / 8 / 46 rhythm),
7
+ * - Datum registration marks and eyebrow ticks are hidden, eyebrows read like aperture labels,
8
+ * - text and brand typography switch to the grotesk stack.
9
+ * Without the attribute nothing in this file matches, so Datum surfaces are unchanged. */
10
+
11
+ [data-grammar='aperture'] {
12
+ --font-family-text: var(--font-family-aperture);
13
+ --font-family-brand: var(--font-family-aperture);
14
+ }
15
+
16
+ /* Seam mark: current navigation item, active list item. */
17
+ [data-grammar='aperture']
18
+ :is(
19
+ .ct-nav-item--active,
20
+ .ct-nav-item[aria-current='page'],
21
+ .ct-list__item--active,
22
+ .ct-list__item[data-state='active'],
23
+ .ct-list__item[aria-selected='true']
24
+ ),
25
+ [data-grammar='aperture']
26
+ .ct-table
27
+ tbody
28
+ tr:is([aria-selected='true'], [aria-current='true'], [data-state='selected'])
29
+ > :first-child {
30
+ box-shadow: none;
31
+ background-image: linear-gradient(
32
+ to bottom,
33
+ var(--color-brand-accent) 0 46%,
34
+ transparent 46% 54%,
35
+ var(--color-brand-accent) 54% 100%
36
+ );
37
+ background-repeat: no-repeat;
38
+ background-size: var(--component-datum-edge-width) calc(100% - var(--space-2));
39
+ background-position: left 0 top var(--space-2);
40
+ }
41
+
42
+ [data-grammar='aperture']
43
+ :is(
44
+ .ct-nav-item--active,
45
+ .ct-nav-item[aria-current='page'],
46
+ .ct-list__item--active,
47
+ .ct-list__item[data-state='active'],
48
+ .ct-list__item[aria-selected='true']
49
+ ):dir(rtl) {
50
+ background-position: right 0 top var(--space-2);
51
+ }
52
+
53
+ /* Sorted table column: horizontal seam instead of a continuous underline. */
54
+ [data-grammar='aperture'] .ct-table thead th:is([aria-sort='ascending'], [aria-sort='descending']) {
55
+ box-shadow: none;
56
+ background-image: linear-gradient(
57
+ to right,
58
+ var(--color-brand-accent) 0 46%,
59
+ transparent 46% 54%,
60
+ var(--color-brand-accent) 54% 100%
61
+ );
62
+ background-repeat: no-repeat;
63
+ background-size: 100% var(--component-datum-edge-width);
64
+ background-position: left bottom;
65
+ }
66
+
67
+ /* Datum cards and frames become quiet paper cards with a hairline. */
68
+ [data-grammar='aperture'] :is(.ct-datum-frame, .ct-card--datum, .ct-frame--datum)::before {
69
+ display: none;
70
+ }
71
+
72
+ [data-grammar='aperture'] .ct-card[data-state='active'] {
73
+ box-shadow: var(--component-card-shadow);
74
+ border-color: var(--color-brand-accent);
75
+ }
76
+
77
+ /* Eyebrows read as aperture labels; the orange tick disappears. */
78
+ [data-grammar='aperture'] .ct-eyebrow::before,
79
+ [data-grammar='aperture'] .ct-eyebrow--numbered::after {
80
+ display: none;
81
+ }
82
+
83
+ [data-grammar='aperture'] .ct-eyebrow {
84
+ color: var(--color-text-secondary);
85
+ padding-inline-start: 0;
86
+ }
87
+
88
+ /* Stepper: the finished part of the path reads as a seam (46 / 8 / 46), not a solid line. */
89
+ [data-grammar='aperture'] .ct-stepper__item[data-state='completed']::after {
90
+ background: linear-gradient(
91
+ to right,
92
+ var(--color-brand-accent) 0 46%,
93
+ transparent 46% 54%,
94
+ var(--color-brand-accent) 54% 100%
95
+ );
96
+ }
97
+
98
+ /* Bottom navigation: split seam instead of the Datum leading edge. */
99
+ [data-grammar='aperture'] .ct-bottom-nav__link[aria-current='page']::before {
100
+ background: linear-gradient(
101
+ to right,
102
+ var(--color-brand-accent) 0 46%,
103
+ transparent 46% 54%,
104
+ var(--color-brand-accent) 54% 100%
105
+ );
106
+ }
107
+
108
+ @media (forced-colors: active) {
109
+ [data-grammar='aperture']
110
+ :is(
111
+ .ct-nav-item--active,
112
+ .ct-nav-item[aria-current='page'],
113
+ .ct-list__item--active,
114
+ .ct-list__item[data-state='active'],
115
+ .ct-list__item[aria-selected='true']
116
+ ),
117
+ [data-grammar='aperture'] .ct-table thead th:is([aria-sort='ascending'], [aria-sort='descending']) {
118
+ background-image: none;
119
+ }
120
+ }
@@ -0,0 +1,112 @@
1
+ /* Bottom navigation — primary destinations on small screens.
2
+ *
3
+ * Markup: <nav class="ct-bottom-nav" aria-label> > <ul class="ct-bottom-nav__list"> > <li> >
4
+ * <a class="ct-bottom-nav__link" aria-current="page"?> <svg class="ct-bottom-nav__icon" aria-hidden> <span class="ct-bottom-nav__label">
5
+ * The current destination is marked with aria-current="page" (no modifier class).
6
+ * .ct-bottom-nav--fixed pins the bar to the viewport bottom and respects the safe-area inset;
7
+ * the product decides at which breakpoint it replaces the sidebar. 3–5 destinations work best. */
8
+
9
+ .ct-bottom-nav {
10
+ display: block;
11
+ border-block-start: var(--border-thin) solid var(--color-border-subtle);
12
+ background: var(--color-bg-surface);
13
+ color: var(--color-text-secondary);
14
+ }
15
+
16
+ .ct-bottom-nav--fixed {
17
+ position: fixed;
18
+ inset-inline: 0;
19
+ inset-block-end: 0;
20
+ z-index: var(--z-sticky);
21
+ padding-block-end: env(safe-area-inset-bottom, 0px);
22
+ }
23
+
24
+ .ct-bottom-nav__list {
25
+ display: flex;
26
+ margin: 0;
27
+ padding: 0;
28
+ list-style: none;
29
+ }
30
+
31
+ .ct-bottom-nav__list > li {
32
+ flex: 1 1 0;
33
+ min-width: 0;
34
+ }
35
+
36
+ .ct-bottom-nav__link {
37
+ position: relative;
38
+ display: flex;
39
+ flex-direction: column;
40
+ align-items: center;
41
+ justify-content: center;
42
+ gap: var(--space-1);
43
+ min-height: 56px;
44
+ min-width: 44px;
45
+ padding: var(--space-2) var(--space-1);
46
+ border: 0;
47
+ background: none;
48
+ color: inherit;
49
+ font: inherit;
50
+ font-size: var(--font-size-xs);
51
+ line-height: 1.2;
52
+ text-align: center;
53
+ text-decoration: none;
54
+ cursor: pointer;
55
+ }
56
+
57
+ .ct-bottom-nav__link:hover {
58
+ color: var(--color-text-primary);
59
+ }
60
+
61
+ .ct-bottom-nav__link:focus-visible {
62
+ outline: 2px solid var(--color-focus-ring);
63
+ outline-offset: -4px;
64
+ border-radius: var(--radius-sm);
65
+ }
66
+
67
+ /* Datum: the current destination carries the leading edge on top. */
68
+ .ct-bottom-nav__link[aria-current='page'] {
69
+ color: var(--color-text-primary);
70
+ font-weight: var(--font-weight-semibold);
71
+ }
72
+
73
+ .ct-bottom-nav__link[aria-current='page']::before {
74
+ content: '';
75
+ position: absolute;
76
+ inset-block-start: 0;
77
+ inset-inline: 25%;
78
+ height: var(--component-datum-edge-width);
79
+ background: var(--color-brand-accent);
80
+ }
81
+
82
+ .ct-bottom-nav__icon {
83
+ flex: none;
84
+ display: inline-grid;
85
+ place-items: center;
86
+ width: 1.5rem;
87
+ height: 1.5rem;
88
+ }
89
+
90
+ /* The icon class may sit on the <svg> itself or on a wrapper around it. */
91
+ .ct-bottom-nav__icon > svg {
92
+ width: 100%;
93
+ height: 100%;
94
+ }
95
+
96
+ .ct-bottom-nav__label {
97
+ max-width: 100%;
98
+ overflow: hidden;
99
+ text-overflow: ellipsis;
100
+ white-space: nowrap;
101
+ }
102
+
103
+ @media (forced-colors: active) {
104
+ .ct-bottom-nav {
105
+ border-block-start-color: CanvasText;
106
+ }
107
+
108
+ .ct-bottom-nav__link[aria-current='page']::before {
109
+ background: Highlight;
110
+ forced-color-adjust: none;
111
+ }
112
+ }
@@ -62,3 +62,8 @@
62
62
  @import './tree.css';
63
63
  @import './chart.css';
64
64
  @import './status.css';
65
+ @import './stepper.css';
66
+ @import './bottom-nav.css';
67
+
68
+ /* Aperture as app grammar — signature layer for data-grammar="aperture" (token re-map lives in tokens.css) */
69
+ @import './aperture-app.css';
@@ -0,0 +1,162 @@
1
+ /* Stepper — progress indicator for multi-step flows.
2
+ *
3
+ * Markup: <nav class="ct-stepper" aria-label> > <ol class="ct-stepper__list"> > <li class="ct-stepper__item">
4
+ * State is driven by attributes, not modifier classes:
5
+ * - aria-current="step" on the current item,
6
+ * - data-state="completed" | "error" on finished or failing items.
7
+ * Each item holds a native <button class="ct-stepper__button"> (disabled = not reachable yet).
8
+ * Position/state texts for screen readers go into .ct-stepper__sr. */
9
+
10
+ .ct-stepper {
11
+ --_marker-size: var(--space-8);
12
+ --_connector: var(--color-border-default);
13
+ --_connector-done: var(--color-brand-primary);
14
+ display: block;
15
+ }
16
+
17
+ .ct-stepper__list {
18
+ display: flex;
19
+ gap: var(--space-2);
20
+ margin: 0;
21
+ padding: 0;
22
+ list-style: none;
23
+ }
24
+
25
+ .ct-stepper--vertical .ct-stepper__list {
26
+ flex-direction: column;
27
+ }
28
+
29
+ .ct-stepper__item {
30
+ position: relative;
31
+ flex: 1 1 0;
32
+ min-width: 0;
33
+ }
34
+
35
+ /* Connector to the next step. */
36
+ .ct-stepper__item::after {
37
+ content: '';
38
+ position: absolute;
39
+ inset-block-start: calc(var(--_marker-size) / 2 - 1px);
40
+ inset-inline: calc(var(--_marker-size) + var(--space-2)) var(--space-2);
41
+ height: 2px;
42
+ background: var(--_connector);
43
+ }
44
+
45
+ .ct-stepper__item:last-child::after,
46
+ .ct-stepper--vertical .ct-stepper__item::after {
47
+ display: none;
48
+ }
49
+
50
+ .ct-stepper__item[data-state='completed']::after {
51
+ background: var(--_connector-done);
52
+ }
53
+
54
+ .ct-stepper__button {
55
+ display: flex;
56
+ align-items: flex-start;
57
+ gap: var(--space-2);
58
+ width: 100%;
59
+ min-height: 44px;
60
+ padding: 0;
61
+ border: 0;
62
+ background: none;
63
+ color: var(--color-text-secondary);
64
+ font: inherit;
65
+ text-align: start;
66
+ cursor: pointer;
67
+ }
68
+
69
+ .ct-stepper__button:disabled {
70
+ color: var(--color-text-muted);
71
+ cursor: default;
72
+ }
73
+
74
+ .ct-stepper__button:focus-visible {
75
+ outline: 2px solid var(--color-focus-ring);
76
+ outline-offset: 2px;
77
+ border-radius: var(--radius-sm);
78
+ }
79
+
80
+ .ct-stepper__marker {
81
+ position: relative;
82
+ z-index: 1;
83
+ flex: none;
84
+ display: inline-grid;
85
+ place-items: center;
86
+ width: var(--_marker-size);
87
+ height: var(--_marker-size);
88
+ border: 2px solid var(--color-border-strong);
89
+ border-radius: var(--radius-round);
90
+ background: var(--color-bg-surface);
91
+ font-variant-numeric: tabular-nums;
92
+ font-weight: var(--font-weight-semibold);
93
+ }
94
+
95
+ .ct-stepper__item[data-state='completed'] .ct-stepper__marker {
96
+ border-color: var(--color-brand-primary);
97
+ color: var(--color-brand-primary);
98
+ }
99
+
100
+ .ct-stepper__item[aria-current='step'] .ct-stepper__marker {
101
+ border-color: var(--color-brand-primary);
102
+ background: var(--color-brand-primary);
103
+ color: var(--color-text-on-primary);
104
+ }
105
+
106
+ .ct-stepper__item[data-state='error'] .ct-stepper__marker {
107
+ border-color: var(--color-state-danger);
108
+ color: var(--color-state-danger-text);
109
+ }
110
+
111
+ .ct-stepper__text {
112
+ display: flex;
113
+ flex-direction: column;
114
+ min-width: 0;
115
+ padding-block-start: var(--space-1);
116
+ }
117
+
118
+ .ct-stepper__label {
119
+ overflow-wrap: anywhere;
120
+ }
121
+
122
+ .ct-stepper__item[aria-current='step'] .ct-stepper__label {
123
+ color: var(--color-text-primary);
124
+ font-weight: var(--font-weight-semibold);
125
+ }
126
+
127
+ .ct-stepper__meta {
128
+ color: var(--color-text-muted);
129
+ font-size: var(--font-size-xs);
130
+ }
131
+
132
+ .ct-stepper__sr {
133
+ position: absolute;
134
+ width: 1px;
135
+ height: 1px;
136
+ overflow: hidden;
137
+ clip-path: inset(50%);
138
+ white-space: nowrap;
139
+ }
140
+
141
+ /* Small screens: only the current step keeps its label. */
142
+ @media (max-width: 599px) {
143
+ .ct-stepper:not(.ct-stepper--vertical) .ct-stepper__item:not([aria-current='step']) .ct-stepper__text {
144
+ display: none;
145
+ }
146
+ }
147
+
148
+ @media (forced-colors: active) {
149
+ .ct-stepper__marker {
150
+ border-color: ButtonText;
151
+ }
152
+
153
+ .ct-stepper__item[aria-current='step'] .ct-stepper__marker {
154
+ background: Highlight;
155
+ color: HighlightText;
156
+ forced-color-adjust: none;
157
+ }
158
+
159
+ .ct-stepper__item::after {
160
+ background: ButtonText;
161
+ }
162
+ }
@@ -1,4 +1,7 @@
1
1
  .ct-table-wrap {
2
+ /* Containing block for absolutely positioned content (e.g. visually hidden header text),
3
+ so the scroll container clips it instead of widening the page. */
4
+ position: relative;
2
5
  border: var(--border-thin) solid var(--color-border-subtle);
3
6
  border-radius: var(--radius-md);
4
7
  background: var(--color-bg-elevated);
@@ -0,0 +1,55 @@
1
+ # Aperture as app grammar
2
+
3
+ `data-grammar="aperture"` turns Aperture from an editorial vocabulary into the look of a whole
4
+ application: paper and ink, cobalt as the only accent, 4px radii and the grotesk stack. The orange
5
+ Datum edge is replaced by a split cobalt seam mark.
6
+
7
+ ```html
8
+ <html lang="de" data-grammar="aperture"> <!-- whole app -->
9
+ <section data-grammar="aperture" data-theme="dark"> <!-- or a subtree -->
10
+ ```
11
+
12
+ - Works with all four themes and with the system preference.
13
+ - Nested `[data-theme]` scopes keep the grammar (the generated block re-applies the re-map).
14
+ - Without the attribute nothing changes; Datum stays the default grammar.
15
+ - Do not mix both signatures on one surface: inside an Aperture scope Datum marks are hidden.
16
+
17
+ ## How it is built
18
+
19
+ The source is `tokens/grammar.aperture.json` (schema `schemas/grammar.schema.json`). Every value
20
+ references an existing token, so `scripts/build-tokens.mjs` emits **one** block at the end of
21
+ `tokens.css` that follows every theme. The pipeline resolves the grammar against each built-in theme
22
+ and runs the default contrast pairs (text on surfaces ≥ 4.5:1, focus and control boundaries ≥ 3:1,
23
+ text on the accent ≥ 4.5:1); a failing pair fails the build. Never edit the generated block.
24
+
25
+ `components/aperture-app.css` is the signature layer: seam marks, hidden Datum marks and the font
26
+ switch for `--font-family-text`/`--font-family-brand`.
27
+
28
+ ## Token re-map
29
+
30
+ | Semantic token | Datum | Aperture app |
31
+ |---|---|---|
32
+ | `--color-bg-canvas`, `--color-bg-surface` | stone scale | `--color-aperture-paper` |
33
+ | `--color-bg-elevated` | stone 0 / 700 | `--color-aperture-elevated` (new role) |
34
+ | `--color-bg-muted`, `--color-bg-subtle` | stone scale | `--color-aperture-surface` |
35
+ | `--color-text-primary` | stone ink | `--color-aperture-ink` |
36
+ | `--color-text-secondary`, `--color-text-muted` | stone | `--color-aperture-muted` |
37
+ | `--color-text-on-primary`, `--color-brand-on-accent` | stone | `--color-aperture-on-accent` |
38
+ | `--color-brand-primary`, `--color-brand-accent` | ink / orange | `--color-aperture-accent` (cobalt) |
39
+ | `…-hover` / `…-active`, `--color-brand-accent-strong` | orange scale | `--color-aperture-accent-hover` / `-active` (new roles) |
40
+ | `--color-border-subtle`, `--color-border-muted` | stone | `--color-aperture-border-subtle` (new role) |
41
+ | `--color-border-default` | stone 500 | `--color-aperture-muted` (≥ 3:1 for controls) |
42
+ | `--color-border-strong` | stone 600 | `--color-aperture-ink` |
43
+ | `--color-focus-ring` | orange | `--color-aperture-accent` |
44
+ | `--font-family-heading`, `--font-family-body` (+ text/brand in the signature layer) | Lato | `--font-family-aperture` |
45
+ | `--radius-control`, `--radius-card`, `--radius-modal` | md / lg / xl | `--radius-sm` |
46
+ | `--color-state-*` | unchanged | unchanged |
47
+
48
+ New primitives: cobalt 100/300/600/800/900 and graphite 200/300/700/900.
49
+
50
+ ## Verification
51
+
52
+ - `npm run check:tokens` – contrast for every theme with the grammar applied.
53
+ - `scripts/test-token-pipeline.mjs` – one reference-only block after all theme and system blocks.
54
+ - Story **Patterns → Aperture App** – all four themes, a nested dark scope, and the regression gate
55
+ (a button outside the grammar keeps the Datum colour); runs with the a11y addon in error mode.
package/docs/aperture.md CHANGED
@@ -112,3 +112,11 @@ an underlined selected radio label.
112
112
  Storybook includes interaction and automated accessibility checks. Touch and
113
113
  forced-colors suites cover the new controls. These are bounded automated checks,
114
114
  not a claim that any consuming page is completely WCAG conformant.
115
+
116
+ ## App grammar
117
+
118
+ Aperture can also style complete applications: set `data-grammar="aperture"` on `<html>` (or a
119
+ subtree). The editorial primitives above stay available; app components switch to paper, ink and
120
+ cobalt. Rules: one dominant mark per composition, cobalt only for the main focus (one primary action
121
+ per view), and never Datum and Aperture signatures on the same surface. Details, token table and
122
+ verification: [aperture-app.md](aperture-app.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuravision/construct",
3
- "version": "2.3.2",
3
+ "version": "2.4.0",
4
4
  "description": "Construct Design System - Accessible, token-based design system for modern web applications",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -111,15 +111,15 @@
111
111
  "README.md"
112
112
  ],
113
113
  "devDependencies": {
114
- "@storybook/addon-a11y": "^10.5.0",
115
- "@storybook/addon-docs": "^10.5.0",
116
- "@storybook/addon-themes": "^10.5.0",
117
- "@storybook/addon-vitest": "^10.5.0",
118
- "@storybook/html-vite": "^10.5.0",
119
- "@vitest/browser-playwright": "^4.1.11",
114
+ "@storybook/addon-a11y": "^10.6.6",
115
+ "@storybook/addon-docs": "^10.6.6",
116
+ "@storybook/addon-themes": "^10.6.6",
117
+ "@storybook/addon-vitest": "^10.6.6",
118
+ "@storybook/html-vite": "^10.6.6",
119
+ "@vitest/browser-playwright": "^4.1.12",
120
120
  "playwright": "^1.61.1",
121
121
  "remark-gfm": "^4.0.1",
122
- "storybook": "^10.5.0",
123
- "vitest": "^4.1.11"
122
+ "storybook": "^10.6.6",
123
+ "vitest": "^4.1.12"
124
124
  }
125
125
  }
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://construct.neuravision.dev/schemas/grammar.schema.json",
4
+ "title": "Construct grammar",
5
+ "description": "A grammar re-maps semantic tokens inside a scope (data-grammar attribute). Values must reference existing tokens so that one generated block follows every theme.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["name", "selector", "semantic"],
9
+ "properties": {
10
+ "$schema": { "type": "string" },
11
+ "name": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$" },
12
+ "description": { "type": "string" },
13
+ "selector": { "type": "string", "pattern": "^\\[data-grammar=\"[a-z][a-z0-9-]*\"\\]$" },
14
+ "semantic": { "type": "object" }
15
+ }
16
+ }
@@ -28,7 +28,10 @@
28
28
  "aperture": {
29
29
  "type": "object",
30
30
  "additionalProperties": false,
31
- "required": ["paper", "surface", "ink", "muted", "accent", "onAccent", "border"],
31
+ "required": [
32
+ "paper", "surface", "ink", "muted", "accent", "onAccent", "border",
33
+ "elevated", "accentHover", "accentActive", "borderSubtle"
34
+ ],
32
35
  "properties": {
33
36
  "paper": { "$ref": "#/$defs/token" },
34
37
  "surface": { "$ref": "#/$defs/token" },
@@ -36,7 +39,11 @@
36
39
  "muted": { "$ref": "#/$defs/token" },
37
40
  "accent": { "$ref": "#/$defs/token" },
38
41
  "onAccent": { "$ref": "#/$defs/token" },
39
- "border": { "$ref": "#/$defs/token" }
42
+ "border": { "$ref": "#/$defs/token" },
43
+ "elevated": { "$ref": "#/$defs/token" },
44
+ "accentHover": { "$ref": "#/$defs/token" },
45
+ "accentActive": { "$ref": "#/$defs/token" },
46
+ "borderSubtle": { "$ref": "#/$defs/token" }
40
47
  }
41
48
  },
42
49
  "brand": {