@neuravision/construct 2.3.1 → 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
@@ -5,6 +5,75 @@ All notable changes to `@neuravision/construct` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Unreleased]
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
+
55
+ ## [2.3.2] - 2026-10-12
56
+
57
+ ### Fixed
58
+
59
+ - `ct-tabs` clamps its content track like every other grid content slot since 2.3.1. Its `auto`
60
+ column grew to the widest panel's max-content size, so a table or other wide content in a tab
61
+ panel stretched the whole tab set — list and panel — past the width it was given; on a phone the
62
+ surrounding card clipped it. Content now wraps or scrolls inside the panel. `ct-tabs--vertical`
63
+ clamps its panel column the same way. The Content Slot Containment story covers tabs: on the
64
+ previous CSS the panel track measured 426.4px inside a 320px slot.
65
+ - Table cell modifiers align their column again. `.ct-table th, .ct-table td` set
66
+ `text-align: start` with a higher specificity than `ct-table__cell--numeric`, `--actions` and
67
+ `--checkbox`, so numbers and row actions stayed left-aligned and checkboxes uncentred. The
68
+ modifiers are now scoped to `.ct-table`; the new Card & Table "Cell types" story checks the
69
+ computed alignment.
70
+ - `publish.sh` asks for a one-time password whenever npm demands one. It assumed that an
71
+ `NPM_TOKEN` in the environment made 2FA unnecessary, which only holds for automation tokens —
72
+ with any other credential the publish stopped at `EOTP` and the script had no way to recover,
73
+ because its OTP prompt sat in the branch without a token. The token now only authenticates,
74
+ the prompt is shared by both paths, and a failure that is not `EOTP` aborts instead of asking
75
+ for a code.
76
+
8
77
  ## [2.3.1] - 2026-09-17
9
78
 
10
79
  ### Fixed
@@ -215,7 +215,7 @@ Content card with header, body, and footer:
215
215
  ```
216
216
 
217
217
  `ct-card__body` clamps its content track, as every grid content slot does (`ct-modal__body`,
218
- `ct-drawer__body`, `ct-field`, `ct-alert`, `ct-confirmation`). Content wider than the card wraps,
218
+ `ct-drawer__body`, `ct-field`, `ct-alert`, `ct-confirmation`, `ct-tabs`). Content wider than the card wraps,
219
219
  truncates, or scrolls inside its own scroll container — `ct-table-wrap` for tables — instead of
220
220
  stretching the card past the width it was given.
221
221
 
@@ -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);
@@ -115,17 +118,19 @@ th[aria-sort='descending'] .ct-table__sort-indicator::before {
115
118
  justify-content: flex-end;
116
119
  }
117
120
 
118
- .ct-table__cell--checkbox {
121
+ /* Cell modifiers are scoped to the table so they outrank the base
122
+ `.ct-table th/td` alignment; unscoped, `text-align: start` always won. */
123
+ .ct-table .ct-table__cell--checkbox {
119
124
  width: var(--space-8);
120
125
  text-align: center;
121
126
  }
122
127
 
123
- .ct-table__cell--numeric {
128
+ .ct-table .ct-table__cell--numeric {
124
129
  text-align: end;
125
130
  font-variant-numeric: tabular-nums;
126
131
  }
127
132
 
128
- .ct-table__cell--actions {
133
+ .ct-table .ct-table__cell--actions {
129
134
  text-align: end;
130
135
  white-space: nowrap;
131
136
  }
@@ -10,6 +10,9 @@
10
10
  --ct-tabs-indicator-color: var(--color-brand-accent);
11
11
 
12
12
  display: grid;
13
+ /* Clamped content track: a panel's projected content wraps or scrolls inside
14
+ the tabs instead of widening them. See `.ct-card__body`. */
15
+ grid-template-columns: minmax(0, 1fr);
13
16
  gap: var(--ct-tabs-gap);
14
17
  }
15
18
 
@@ -119,7 +122,8 @@ a.ct-tabs__trigger:active {
119
122
  /* Vertical orientation */
120
123
 
121
124
  .ct-tabs--vertical {
122
- grid-template-columns: auto 1fr;
125
+ /* The panel column is clamped so wide content stays inside the tabs. */
126
+ grid-template-columns: auto minmax(0, 1fr);
123
127
  }
124
128
 
125
129
  .ct-tabs--vertical .ct-tabs__list {
@@ -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).