@skra/ui 0.5.0 → 0.6.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.
@@ -1,85 +1,88 @@
1
- # Consuming Skra — Agent Guide (Core)
2
-
3
- > **DRAFT (2026-06-25).** All 7 sections drafted. Snippets are grounded in source (verified
4
- > against `libs/ui/*`) + the `apps/dogfood` friction journal (`F-n`) + the Iron Boundaries.
5
- > Still draft: not yet referenced from a real consumer repo, not lint-gated against ADR drift,
6
- > and cross-engine/Safari caveats inherit Skra's own open item O-9.
7
-
8
- This guide is for an **agent working inside a repository that consumes Skra** (imports
9
- `@skra/*`), not for authoring Skra itself. It is written to be **copied into / referenced
10
- from a consumer repo**.
11
-
12
- ---
13
-
14
- ## How to use this guide (read this first — it is the point)
15
-
16
- This file is **tiered on purpose** (same model as `FORGE_ADR_SUMMARY.md`). To keep token
17
- cost low:
18
-
19
- 1. **Always load this core** (it is small) — the Iron Rules + the Section Router below.
20
- 2. Then load **at most the ONE section** that matches your current task (the Router maps
21
- task → file). Do **not** read every section "to be safe" — that defeats the design.
22
- 3. Open the full Skra ADR / spec only when a section explicitly points you there.
23
-
24
- ---
25
-
26
- ## Iron Rules of Consumption (the 6 a consumer breaks on day one)
27
-
28
- Violating these is a review blocker in a consumer repo just as inside Skra.
29
-
30
- 1. **Import only from public `@skra/ui/*` entry points** — never a deep relative `libs/...`
31
- path. Skra ships as one package, `@skra/ui`, with a secondary entry point per component
32
- (e.g. `@skra/ui/button`, `@skra/ui/typography`, `@skra/ui/select`); the primary
33
- `@skra/ui` exports nothing. (Full entry-point list → `setup-and-gotchas.md`.)
34
- 2. **You own the value.** Skra form atoms do **not** implement `ControlValueAccessor` /
35
- `FormValueControl` (ADR 037). Native-host atoms take `[formField]`/`[formControl]`/
36
- `[(ngModel)]`; value-owned overlays need an explicit `[value]`/`(valueChange)` bridge.
37
- (How → `forms-and-binding.md`.)
38
- 3. **Accessible name has exactly ONE path** (ADR 033). No Skra atom exposes an `ariaLabel`
39
- input; never put `aria-label` next to visible text on the same host. (Paths →
40
- `accessible-names.md`.)
41
- 4. **Inside `<skra-field>`, do not pass `id` / `for` / `aria-describedby` by hand** — the
42
- DI context wires label↔control↔hint↔error automatically (ADR 013).
43
- 5. **Under Signal Forms, native constraint attributes go in the schema, not the template.**
44
- `name` on grouped radios and `maxlength` / `min` / `pattern` on a `[formField]` host raise
45
- `NG8022` — Signal Forms owns them (F-1/F-2). (Details → `forms-and-binding.md`.)
46
- 6. **Never theme with inline styles.** No `[style.--skra-*]` bindings or `style="…"` (breaks
47
- CSP, ADR 021). Override tokens via a CSS class / `data-skra-*` scope; components are
48
- `margin: 0` (ADR 022) — spacing comes from layout atoms, never a margin on the component.
49
-
50
- ---
51
-
52
- ## Section Router — load the ONE that matches your task
53
-
54
- | Section | File | Load when | Status |
55
- | --------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------- | -------- |
56
- | **Setup & gotchas** | `setup-and-gotchas.md` | First integration; import fails; which `@skra/*` package exports what; boolean-attribute & theming pitfalls | ✅ draft |
57
- | **Forms & value binding** | `forms-and-binding.md` | Wiring an input / select / checkbox / rating to Signal Forms, Reactive Forms, or `ngModel` | ✅ draft |
58
- | **Accessible names** | `accessible-names.md` | Naming buttons, icon-only controls, overlays; using `skra-sr-only` | ✅ draft |
59
- | **Theming & encapsulation** | `theming-and-encapsulation.md` | Skins, dark mode, density; mounting under Shadow DOM / a microfrontend / an iframe | ✅ draft |
60
- | **Overlays** | `overlays.md` | Dialog / tooltip / popover / menu / select in the top layer; shadow & iframe limits | ✅ draft |
61
- | **Layout** | `layout.md` | Spacing & composition with Stack / Box / Surface / Grid; building a card; the gap scale | ✅ draft |
62
- | **Wrapping Skra** | `wrapping-patterns.md` | Building your own higher-level components over Skra atoms (simplified API) | ✅ draft |
63
-
64
- ---
65
-
66
- ## Quick reference — task → section
67
-
68
- | I want to… | Go to |
69
- | -------------------------------------------------------------------------- | ----------------------------------------- |
70
- | Add `@skra/ui/*` to a project and import an atom | `setup-and-gotchas.md` |
71
- | Bind a text input / textarea to a form | `forms-and-binding.md` |
72
- | Bind a `skra-select` / `skra-datepicker` / `skra-star-rating` to a form | `forms-and-binding.md` (the value bridge) |
73
- | Name an icon-only button | `accessible-names.md` |
74
- | Switch skin / dark mode at runtime | `theming-and-encapsulation.md` |
75
- | Mount Skra inside a Web Component / microfrontend | `theming-and-encapsulation.md` |
76
- | Open a dialog or tooltip | `overlays.md` |
77
- | Space components apart | `layout.md` |
78
- | Build a card / panel / tile (there is no `skra-card`) | `layout.md` (Surface) |
79
- | Wrap a Skra atom in a project-specific component | `wrapping-patterns.md` |
80
-
81
- ---
82
-
83
- _Source of truth for the rules: Skra `FORGE_ADR_SUMMARY.md` + `adr/decisions/`. Real
84
- consumption evidence: `apps/dogfood/FRICTION_JOURNAL.md`. This guide restates the consumer
85
- slice of those — on divergence, the ADRs win._
1
+ # Consuming Skra — Agent Guide (Core)
2
+
3
+ > **DRAFT (2026-06-25; 8th section added 2026-08-30).** All 8 sections drafted. Snippets are grounded in source (verified
4
+ > against `libs/ui/*`) + the `apps/dogfood` friction journal (`F-n`) + the Iron Boundaries.
5
+ > Still draft: not yet referenced from a real consumer repo, not lint-gated against ADR drift,
6
+ > and cross-engine/Safari caveats inherit Skra's own open item O-9.
7
+
8
+ This guide is for an **agent working inside a repository that consumes Skra** (imports
9
+ `@skra/*`), not for authoring Skra itself. It is written to be **copied into / referenced
10
+ from a consumer repo**.
11
+
12
+ ---
13
+
14
+ ## How to use this guide (read this first — it is the point)
15
+
16
+ This file is **tiered on purpose** (same model as `FORGE_ADR_SUMMARY.md`). To keep token
17
+ cost low:
18
+
19
+ 1. **Always load this core** (it is small) — the Iron Rules + the Section Router below.
20
+ 2. Then load **at most the ONE section** that matches your current task (the Router maps
21
+ task → file). Do **not** read every section "to be safe" — that defeats the design.
22
+ 3. Open the full Skra ADR / spec only when a section explicitly points you there.
23
+
24
+ ---
25
+
26
+ ## Iron Rules of Consumption (the 6 a consumer breaks on day one)
27
+
28
+ Violating these is a review blocker in a consumer repo just as inside Skra.
29
+
30
+ 1. **Import only from public `@skra/ui/*` entry points** — never a deep relative `libs/...`
31
+ path. Skra ships as one package, `@skra/ui`, with a secondary entry point per component
32
+ (e.g. `@skra/ui/button`, `@skra/ui/typography`, `@skra/ui/select`); the primary
33
+ `@skra/ui` exports nothing. (Full entry-point list → `setup-and-gotchas.md`.)
34
+ 2. **You own the value.** Skra form atoms do **not** implement `ControlValueAccessor` /
35
+ `FormValueControl` (ADR 037). Native-host atoms take `[formField]`/`[formControl]`/
36
+ `[(ngModel)]`; value-owned overlays need an explicit `[value]`/`(valueChange)` bridge.
37
+ (How → `forms-and-binding.md`.)
38
+ 3. **Accessible name has exactly ONE path** (ADR 033). No Skra atom exposes an `ariaLabel`
39
+ input; never put `aria-label` next to visible text on the same host. (Paths →
40
+ `accessible-names.md`.)
41
+ 4. **Inside `<skra-field>`, do not pass `id` / `for` / `aria-describedby` by hand** — the
42
+ DI context wires label↔control↔hint↔error automatically (ADR 013).
43
+ 5. **Under Signal Forms, native constraint attributes go in the schema, not the template.**
44
+ `name` on grouped radios and `maxlength` / `min` / `pattern` on a `[formField]` host raise
45
+ `NG8022` — Signal Forms owns them (F-1/F-2). (Details → `forms-and-binding.md`.)
46
+ 6. **Never theme with inline styles.** No `[style.--skra-*]` bindings or `style="…"` (breaks
47
+ CSP, ADR 021). Override tokens via a CSS class / `data-skra-*` scope; components are
48
+ `margin: 0` (ADR 022) — spacing comes from layout atoms, never a margin on the component.
49
+
50
+ ---
51
+
52
+ ## Section Router — load the ONE that matches your task
53
+
54
+ | Section | File | Load when | Status |
55
+ | --------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------- |
56
+ | **Setup & gotchas** | `setup-and-gotchas.md` | First integration; import fails; which `@skra/*` package exports what; boolean-attribute & theming pitfalls | ✅ draft |
57
+ | **Forms & value binding** | `forms-and-binding.md` | Wiring an input / select / checkbox / rating to Signal Forms, Reactive Forms, or `ngModel` | ✅ draft |
58
+ | **Accessible names** | `accessible-names.md` | Naming buttons, icon-only controls, overlays; using `skra-sr-only` | ✅ draft |
59
+ | **Theming & encapsulation** | `theming-and-encapsulation.md` | Skins, dark mode, density; mounting under Shadow DOM / a microfrontend / an iframe | ✅ draft |
60
+ | **Overlays** | `overlays.md` | Dialog / tooltip / popover / menu / select in the top layer; shadow & iframe limits | ✅ draft |
61
+ | **Layout** | `layout.md` | Spacing & composition with Stack / Box / Surface / Grid; building a card; the gap scale | ✅ draft |
62
+ | **Wrapping Skra** | `wrapping-patterns.md` | Building your own higher-level components over Skra atoms (simplified API) | ✅ draft |
63
+ | **Custom components** | `custom-components.md` | Authoring a net-new widget Skra does not ship (nothing to wrap) on Skra tokens — own token tier, state, promotion path | ✅ draft |
64
+
65
+ ---
66
+
67
+ ## Quick reference — task → section
68
+
69
+ | I want to… | Go to |
70
+ | ----------------------------------------------------------------------- | ----------------------------------------- |
71
+ | Add `@skra/ui/*` to a project and import an atom | `setup-and-gotchas.md` |
72
+ | Bind a text input / textarea to a form | `forms-and-binding.md` |
73
+ | Bind a `skra-select` / `skra-datepicker` / `skra-star-rating` to a form | `forms-and-binding.md` (the value bridge) |
74
+ | Name an icon-only button | `accessible-names.md` |
75
+ | Switch skin / dark mode at runtime | `theming-and-encapsulation.md` |
76
+ | Mount Skra inside a Web Component / microfrontend | `theming-and-encapsulation.md` |
77
+ | Open a dialog or tooltip | `overlays.md` |
78
+ | Space components apart | `layout.md` |
79
+ | Build a card / panel / tile (there is no `skra-card`) | `layout.md` (Surface) |
80
+ | Wrap a Skra atom in a project-specific component | `wrapping-patterns.md` |
81
+ | Build a widget Skra does not ship (scrubber, timeline, domain visual) | `custom-components.md` |
82
+ | Keep an app-local component on-skin, on-density, dark-mode-correct | `custom-components.md` |
83
+
84
+ ---
85
+
86
+ _Source of truth for the rules: Skra `FORGE_ADR_SUMMARY.md` + `adr/decisions/`. Real
87
+ consumption evidence: `apps/dogfood/FRICTION_JOURNAL.md`. This guide restates the consumer
88
+ slice of those — on divergence, the ADRs win._
@@ -0,0 +1,154 @@
1
+ # Building Your Own Component on Skra Tokens
2
+
3
+ > Load this for: authoring a **net-new widget that Skra does not ship and that has no Skra atom
4
+ > to wrap** — a scrubber, a timeline, a domain visual — while keeping it inside the design
5
+ > system's guarantees (skin, dark scheme, density, contrast, CSP).
6
+ >
7
+ > If you are simplifying the API of an atom Skra _does_ ship, you want `wrapping-patterns.md`
8
+ > instead. This page is for the case where there is nothing to wrap.
9
+
10
+ ## Step 0 — decompose before you build
11
+
12
+ Most "I need a component Skra doesn't have" cases are not one component. Split the widget into
13
+ three buckets first, because two of them are already solved:
14
+
15
+ | Bucket | Where it belongs |
16
+ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------- |
17
+ | Parts that ARE Skra atoms — buttons, icons, text, a painted container | `@skra/ui` (`skraButton`, `skra-icon`, `skraTypography`, `skraSurface`) |
18
+ | Domain logic — media elements, state machines, data fetching, formatting | your app's services/stores. Not a design-system concern at any level |
19
+ | The genuinely novel visual — the part with no atom and no analogue | this page |
20
+
21
+ The third bucket is usually much smaller than it first looks. Build only that.
22
+
23
+ ## The rule — own your tier 3, resolve it to tier 2
24
+
25
+ Skra's token engine has four layers: **1 `ref`** (raw values) → **2 `sys`** (semantic) →
26
+ **3 `comp`** (component-scoped) → **4 API** (per-instance overrides).
27
+
28
+ Layer 3 can only be _minted_ inside the Skra repo, because a `--skra-comp-*` token needs a value
29
+ in every skin. That does **not** mean your component drops to layer 2. It means your component
30
+ **owns its own layer 3**, in its own namespace, with defaults that resolve to layer 2:
31
+
32
+ ```scss
33
+ /* app-scrubber.component.scss */
34
+ :host {
35
+ /* Your tier 3 — one block, the only place a value is decided. */
36
+ --app-scrubber-track-background: var(--skra-sys-color-surface-muted);
37
+ --app-scrubber-track-radius: var(--skra-sys-radius-full);
38
+ --app-scrubber-track-size: var(--skra-sys-spacing-2);
39
+ --app-scrubber-thumb-background: var(--skra-sys-color-primary-base);
40
+ --app-scrubber-thumb-size: var(--skra-sys-size-interactive-sm);
41
+ --app-scrubber-padding-inline: var(--skra-sys-spacing-3);
42
+ --app-scrubber-focus-ring-color: var(--skra-sys-color-focus-ring);
43
+ --app-scrubber-focus-ring-width: 0.125rem; /* no sys token for this — core keeps it literal too */
44
+
45
+ display: block;
46
+ margin: 0; /* ADR 022 — outer spacing is the container's job, never the component's */
47
+ padding-inline: var(--app-scrubber-padding-inline);
48
+ }
49
+
50
+ /* State comes from an attribute, never from a bound style (ADR 021 — CSP-safe). */
51
+ :host(:hover) {
52
+ --app-scrubber-thumb-background: var(--skra-sys-color-primary-base-hover);
53
+ }
54
+ :host([data-app-dragging]) {
55
+ --app-scrubber-thumb-background: var(--skra-sys-color-primary-base-active);
56
+ }
57
+ :host([data-app-disabled]) {
58
+ --app-scrubber-track-background: var(--skra-sys-color-neutral-soft);
59
+ --app-scrubber-thumb-background: var(--skra-sys-color-neutral-base);
60
+ }
61
+
62
+ .app-scrubber-track {
63
+ background: var(--app-scrubber-track-background);
64
+ border-radius: var(--app-scrubber-track-radius);
65
+ block-size: var(--app-scrubber-track-size);
66
+ }
67
+ .app-scrubber-thumb {
68
+ background: var(--app-scrubber-thumb-background);
69
+ inline-size: var(--app-scrubber-thumb-size);
70
+ block-size: var(--app-scrubber-thumb-size);
71
+ }
72
+ .app-scrubber-thumb:focus-visible {
73
+ outline: var(--app-scrubber-focus-ring-width) solid var(--app-scrubber-focus-ring-color);
74
+ }
75
+ ```
76
+
77
+ ```ts
78
+ // app-scrubber.component.ts — state reaches CSS as attributes, not as style bindings.
79
+ @Component({
80
+ selector: 'app-scrubber',
81
+ host: {
82
+ '[attr.data-app-dragging]': 'dragging() ? "" : null',
83
+ '[attr.data-app-disabled]': 'disabled() ? "" : null',
84
+ },
85
+ // …
86
+ })
87
+ ```
88
+
89
+ Why not read `sys` directly at each property? Because then the value is scattered across the
90
+ file, per-instance overrides have nothing to target, and promoting the widget into Skra later
91
+ becomes a rewrite instead of a rename.
92
+
93
+ **Namespace:** use **your own prefix** (`--app-*`, `--acme-*`). Do **not** declare
94
+ `--skra-comp-*` or `--skra-sys-*` values of your own — that namespace belongs to the token
95
+ engine, a skin may overwrite it at any time, and a future Skra component of the same name
96
+ would collide with you.
97
+
98
+ ## What you get for free
99
+
100
+ Everything below follows from resolving your defaults to `sys` — no work on your side:
101
+
102
+ | Capability | Why it works |
103
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104
+ | **Every skin** | `sys` is redefined per skin; your widget re-paints when the skin changes |
105
+ | **Dark scheme** | the dark layer redefines the same `sys` names |
106
+ | **Density** | `[data-skra-density]` remaps `--skra-sys-spacing-*` and `--skra-sys-size-interactive-*` at the **sys** layer — so a widget sized from those follows density automatically. Hardcoding `padding: 0.5rem` opts you out |
107
+ | **Interaction states** | the build synthesises `-hover` / `-active` for `sys` colours too (`--skra-sys-color-primary-base-hover`), scaled by each skin's interaction profile |
108
+ | **Shadow DOM / CSP** | custom properties inherit across shadow boundaries; attribute-driven state needs no inline styles |
109
+
110
+ ## What you do NOT get — plan around it
111
+
112
+ 1. **No build-time contrast gate on your pairs.** Skra fails its own build on a bad
113
+ foreground/background pair; your widget's pairs are not measured. Prefer pairs Skra already
114
+ gates (`*-strong` / `*-soft`, `on-*` foregrounds) over improvised combinations, and check
115
+ any novel pair manually.
116
+ 2. **No state synthesis from a colour of your own.** You cannot generate a hover ramp from a
117
+ bespoke base — pick a `sys` colour that already ships `-hover` / `-active`. In practice this
118
+ is a healthy constraint: it keeps you inside the skin's palette.
119
+ 3. **No skin-surface manifest.** Your tokens are invisible to skin/preset tooling.
120
+ 4. **No per-skin bespoke treatment.** Your widget inherits what flows through `sys` (each
121
+ skin's own radius scale, palette and interaction strength) — not a hand-tuned variant.
122
+
123
+ ## The floor — non-negotiable, whatever the widget
124
+
125
+ These are the rules that make a component _reusable_ rather than _copy-pasteable_. A hardcoded
126
+ `#FFD700` cannot be re-skinned by your next app; that is the whole difference.
127
+
128
+ - **Every value maps to a token.** No colour literals, no magic pixel values where a
129
+ `--skra-sys-*` exists. Structural constants (a `1.4s` pulse, a `50%` split) are fine.
130
+ - **CSP-safe state.** State reflects as an attribute on the host and is styled by an attribute
131
+ selector. Never `[style.--var]="signal()"`, never runtime style mutation.
132
+ - **Zero outer margin.** `margin: 0` on the host; spacing between components belongs to the
133
+ layout atoms (`skraStack` / `skraBox`).
134
+ - **Accessible name and keyboard behaviour before paint.** Follow the WAI-ARIA APG pattern the
135
+ widget actually is; see `accessible-names.md`.
136
+ - **No `!important`, no `::ng-deep`, no skin names in selectors.**
137
+
138
+ ## Promotion path — when the widget outgrows the app
139
+
140
+ Location is deliberately reversible: a widget whose defaults resolve to `sys` moves without a
141
+ single token change.
142
+
143
+ 1. **App-local** — the default, and where it stays while exactly one app uses it.
144
+ 2. **Your own shared library** (`@yourorg/ui`, built ON Skra) — when a **second** app genuinely
145
+ needs it. Do not build the shared package for a widget that exists in one app; that is
146
+ speculative generality.
147
+ 3. **Skra core** — only if it stops being domain-specific: no domain vocabulary, no third-party
148
+ runtime dependency, and a consumer in a _different_ domain would use it as-is. That is a
149
+ feature request against Skra, not something you can do from outside.
150
+
151
+ ---
152
+
153
+ _See also: `wrapping-patterns.md` (simplifying a Skra atom's API), `theming-and-encapsulation.md`
154
+ (loading token layers, skin/scheme/density switching), `accessible-names.md`, `layout.md`._
@@ -1,90 +1,98 @@
1
- # Wrapping Skra in Your Own Components
2
-
3
- > Load this for: building project-specific components on top of Skra atoms to simplify the
4
- > API down to the inputs your app actually needs. Governed by the *atoms-agnostic* doctrine +
5
- > ADR 037 (consumer owns value/ergonomics).
6
-
7
- ## Why wrap, and where the wrapper belongs
8
-
9
- Skra atoms are deliberately low-level and semantic-only — they ship a11y + tokens + binding
10
- seams, not app ergonomics. The intended consumption shape is: **your app wraps Skra atoms in
11
- higher-level components** that expose just the inputs your domain needs.
12
-
13
- The hard rule: **the wrapper lives in YOUR app (or a consumer library), never back inside
14
- `@skra/*`.** Pulling app ergonomics into the atom breaks the agnostic contract (ADR 037). If a
15
- wrapper becomes reusable across several of your apps, promote it to **your own library built ON
16
- Skra** (`@yourorg/ui`), not into Skra core.
17
-
18
- ## Pattern 1 — a field wrapper (collapse the field boilerplate)
19
-
20
- The `<skra-field>` + label + control + hint + error + binding cluster is verbose to repeat.
21
- Wrap it once:
22
-
23
- ```ts
24
- // app-text-field.component.ts (YOUR code)
25
- @Component({
26
- selector: 'app-text-field',
27
- standalone: true,
28
- imports: [SkraField, SkraInput, SkraLabelFor, SkraHint, SkraError],
29
- template: `
30
- <skra-field>
31
- <label skraLabelFor>{{ label() }}</label>
32
- <input skraInput [formField]="field()" />
33
- @if (hint()) { <skra-hint>{{ hint() }}</skra-hint> }
34
- @if (errorText()) { <skra-error>{{ errorText() }}</skra-error> }
35
- </skra-field>
36
- `,
37
- })
38
- export class AppTextField {
39
- readonly label = input.required<string>();
40
- readonly field = input.required<Field<string>>(); // your Signal Forms node type
41
- readonly hint = input<string>();
42
- readonly errorText = input<string>();
43
- }
44
- ```
45
-
46
- ```html
47
- <!-- call site: domain-minimal API -->
48
- <app-text-field label="Email" [field]="form().email" hint="We never share it." />
49
- ```
50
-
51
- **What NOT to break when wrapping:**
52
-
53
- - Don't re-implement a11y — keep the control inside `<skra-field>` so the DI context still
54
- wires `id`/`for`/`aria-describedby` (ADR 013). Don't add `aria-label` (ADR 033).
55
- - Don't introduce inline styles to theme — scope tokens with a class (ADR 021).
56
- - Keep the accessible-name path intact (the projected `<label skraLabelFor>` IS the name).
57
-
58
- ## Pattern 2 — a value-bridge adapter for class-B overlays
59
-
60
- The class-B overlays (`skra-select`, `skra-datepicker`, `skra-star-rating`, …) need the
61
- two-binding `[value]`/`(valueChange)` bridge (see `forms-and-binding.md`). If that recurs, the
62
- sanctioned shape is a **consumer-side adapter directive** that maps a form node ↔
63
- value/valueChange, so the call site goes back to one binding:
64
-
65
- ```ts
66
- // app-control.directive.ts (YOUR interop layer — a sketch)
67
- @Directive({ selector: '[appControl]', standalone: true })
68
- export class AppControl<T> {
69
- readonly appControl = input.required<Field<T>>();
70
- // read [value] from the host element + write back on (valueChange),
71
- // syncing this.appControl().value() ↔ the field node
72
- }
73
- ```
74
-
75
- ```html
76
- <skra-select [appControl]="form().country">…</skra-select>
77
- ```
78
-
79
- This adapter is **yours**, in your interop layer — it must not live in the atom (ADR 037). The
80
- friction journal calls this candidate `[skraControl]` (F-3).
81
-
82
- ## The open question — `@skra/angular-interop`? (resolved: not now)
83
-
84
- If *every* Angular consumer needs the same value-bridge adapter, there is an argument that
85
- Skra could ship an **optional, opt-in** `@skra/angular-interop` package — separate from the
86
- agnostic atoms (atoms stay pure; interop is a voluntary layer on top). **Resolved 2026-06-25
87
- (ADR 037 §7): DECLINED for now** — with one consumer and a still-iterating Signal Forms API,
88
- the bridge stays consumer-owned (Pattern 2). The trigger to revisit, then via its own ADR:
89
- several real consumers feeling the friction AND a stabilised Angular forms API (that ADR must
90
- first resolve strategy-agnosticism — Signal Forms AND Reactive via a seam, never bless one).
1
+ # Wrapping Skra in Your Own Components
2
+
3
+ > Load this for: building project-specific components on top of Skra atoms to simplify the
4
+ > API down to the inputs your app actually needs. Governed by the _atoms-agnostic_ doctrine +
5
+ > ADR 037 (consumer owns value/ergonomics).
6
+
7
+ ## Why wrap, and where the wrapper belongs
8
+
9
+ Skra atoms are deliberately low-level and semantic-only — they ship a11y + tokens + binding
10
+ seams, not app ergonomics. The intended consumption shape is: **your app wraps Skra atoms in
11
+ higher-level components** that expose just the inputs your domain needs.
12
+
13
+ The hard rule: **the wrapper lives in YOUR app (or a consumer library), never back inside
14
+ `@skra/*`.** Pulling app ergonomics into the atom breaks the agnostic contract (ADR 037). If a
15
+ wrapper becomes reusable across several of your apps, promote it to **your own library built ON
16
+ Skra** (`@yourorg/ui`), not into Skra core.
17
+
18
+ **If there is no atom to wrap** — a net-new widget Skra does not ship at all (a scrubber, a
19
+ timeline, a domain visual) — this page does not cover it. Go to `custom-components.md`, which
20
+ gives the token contract that keeps such a component on-skin, on-density and dark-mode-correct.
21
+
22
+ ## Pattern 1 — a field wrapper (collapse the field boilerplate)
23
+
24
+ The `<skra-field>` + label + control + hint + error + binding cluster is verbose to repeat.
25
+ Wrap it once:
26
+
27
+ ```ts
28
+ // app-text-field.component.ts (YOUR code)
29
+ @Component({
30
+ selector: 'app-text-field',
31
+ standalone: true,
32
+ imports: [SkraField, SkraInput, SkraLabelFor, SkraHint, SkraError],
33
+ template: `
34
+ <skra-field>
35
+ <label skraLabelFor>{{ label() }}</label>
36
+ <input skraInput [formField]="field()" />
37
+ @if (hint()) {
38
+ <skra-hint>{{ hint() }}</skra-hint>
39
+ }
40
+ @if (errorText()) {
41
+ <skra-error>{{ errorText() }}</skra-error>
42
+ }
43
+ </skra-field>
44
+ `,
45
+ })
46
+ export class AppTextField {
47
+ readonly label = input.required<string>();
48
+ readonly field = input.required<Field<string>>(); // your Signal Forms node type
49
+ readonly hint = input<string>();
50
+ readonly errorText = input<string>();
51
+ }
52
+ ```
53
+
54
+ ```html
55
+ <!-- call site: domain-minimal API -->
56
+ <app-text-field label="Email" [field]="form().email" hint="We never share it." />
57
+ ```
58
+
59
+ **What NOT to break when wrapping:**
60
+
61
+ - Don't re-implement a11y — keep the control inside `<skra-field>` so the DI context still
62
+ wires `id`/`for`/`aria-describedby` (ADR 013). Don't add `aria-label` (ADR 033).
63
+ - Don't introduce inline styles to theme — scope tokens with a class (ADR 021).
64
+ - Keep the accessible-name path intact (the projected `<label skraLabelFor>` IS the name).
65
+
66
+ ## Pattern 2 — a value-bridge adapter for class-B overlays
67
+
68
+ The class-B overlays (`skra-select`, `skra-datepicker`, `skra-star-rating`, …) need the
69
+ two-binding `[value]`/`(valueChange)` bridge (see `forms-and-binding.md`). If that recurs, the
70
+ sanctioned shape is a **consumer-side adapter directive** that maps a form node ↔
71
+ value/valueChange, so the call site goes back to one binding:
72
+
73
+ ```ts
74
+ // app-control.directive.ts (YOUR interop layer — a sketch)
75
+ @Directive({ selector: '[appControl]', standalone: true })
76
+ export class AppControl<T> {
77
+ readonly appControl = input.required<Field<T>>();
78
+ // read [value] from the host element + write back on (valueChange),
79
+ // syncing this.appControl().value() ↔ the field node
80
+ }
81
+ ```
82
+
83
+ ```html
84
+ <skra-select [appControl]="form().country">…</skra-select>
85
+ ```
86
+
87
+ This adapter is **yours**, in your interop layer — it must not live in the atom (ADR 037). The
88
+ friction journal calls this candidate `[skraControl]` (F-3).
89
+
90
+ ## The open question — `@skra/angular-interop`? (resolved: not now)
91
+
92
+ If _every_ Angular consumer needs the same value-bridge adapter, there is an argument that
93
+ Skra could ship an **optional, opt-in** `@skra/angular-interop` package — separate from the
94
+ agnostic atoms (atoms stay pure; interop is a voluntary layer on top). **Resolved 2026-06-25
95
+ (ADR 037 §7): DECLINED for now** — with one consumer and a still-iterating Signal Forms API,
96
+ the bridge stays consumer-owned (Pattern 2). The trigger to revisit, then via its own ADR:
97
+ several real consumers feeling the friction AND a stabilised Angular forms API (that ADR must
98
+ first resolve strategy-agnosticism — Signal Forms AND Reactive via a seam, never bless one).