@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 +69 -0
- package/components/README.md +39 -1
- package/components/_shared.css +5 -1
- package/components/aperture-app.css +120 -0
- package/components/bottom-nav.css +112 -0
- package/components/index.css +5 -0
- package/components/stepper.css +162 -0
- package/components/table.css +8 -3
- package/components/tabs.css +5 -1
- package/docs/aperture-app.md +55 -0
- package/docs/aperture.md +8 -0
- package/package.json +9 -9
- package/schemas/grammar.schema.json +16 -0
- package/schemas/semantic.schema.json +9 -2
- package/scripts/build-tokens.mjs +73 -0
- package/standalone/aperture-app.css +3 -0
- package/standalone/bottom-nav.css +3 -0
- package/standalone/stepper.css +3 -0
- package/tokens/grammar.aperture.json +53 -0
- package/tokens/primitives.json +10 -1
- package/tokens/semantic.dark.json +5 -1
- package/tokens/semantic.high-contrast-dark.json +5 -1
- package/tokens/semantic.high-contrast.json +5 -1
- package/tokens/semantic.light.json +5 -1
- package/tokens/tokens.css +75 -0
- package/tokens/tokens.js +56 -8
- package/tokens/tokens.json +56 -8
- package/tokens/tokens.ts +56 -8
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
|
package/components/README.md
CHANGED
|
@@ -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:
|
package/components/_shared.css
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
:
|
|
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
|
+
}
|
package/components/index.css
CHANGED
|
@@ -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
|
+
}
|
package/components/table.css
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/components/tabs.css
CHANGED
|
@@ -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
|
-
|
|
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).
|