@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.
- package/consumer-guide/README.md +88 -85
- package/consumer-guide/custom-components.md +154 -0
- package/consumer-guide/wrapping-patterns.md +98 -90
- package/fesm2022/skra-ui-button.mjs +2 -2
- package/fesm2022/skra-ui-button.mjs.map +1 -1
- package/fesm2022/skra-ui-checkbox.mjs +2 -2
- package/fesm2022/skra-ui-checkbox.mjs.map +1 -1
- package/fesm2022/skra-ui-core.mjs +28 -3
- package/fesm2022/skra-ui-core.mjs.map +1 -1
- package/fesm2022/skra-ui-input.mjs +2 -2
- package/fesm2022/skra-ui-input.mjs.map +1 -1
- package/fesm2022/skra-ui-menu.mjs +38 -14
- package/fesm2022/skra-ui-menu.mjs.map +1 -1
- package/fesm2022/skra-ui-popover.mjs +25 -9
- package/fesm2022/skra-ui-popover.mjs.map +1 -1
- package/fesm2022/skra-ui-radio.mjs +2 -2
- package/fesm2022/skra-ui-radio.mjs.map +1 -1
- package/fesm2022/skra-ui-rating-display.mjs +2 -2
- package/fesm2022/skra-ui-rating-display.mjs.map +1 -1
- package/fesm2022/skra-ui-star-rating.mjs +2 -2
- package/fesm2022/skra-ui-star-rating.mjs.map +1 -1
- package/fesm2022/skra-ui-switch.mjs +2 -2
- package/fesm2022/skra-ui-switch.mjs.map +1 -1
- package/fesm2022/skra-ui-tooltip.mjs +7 -2
- package/fesm2022/skra-ui-tooltip.mjs.map +1 -1
- package/package.json +2 -2
- package/types/skra-ui-core.d.ts.map +1 -1
- package/types/skra-ui-menu.d.ts +18 -6
- package/types/skra-ui-menu.d.ts.map +1 -1
- package/types/skra-ui-popover.d.ts +13 -4
- package/types/skra-ui-popover.d.ts.map +1 -1
- package/types/skra-ui-tooltip.d.ts.map +1 -1
package/consumer-guide/README.md
CHANGED
|
@@ -1,85 +1,88 @@
|
|
|
1
|
-
# Consuming Skra — Agent Guide (Core)
|
|
2
|
-
|
|
3
|
-
> **DRAFT (2026-06-25).** All
|
|
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
|
|
55
|
-
| --------------------------- | ------------------------------ |
|
|
56
|
-
| **Setup & gotchas** | `setup-and-gotchas.md` | First integration; import fails; which `@skra/*` package exports what; boolean-attribute & theming pitfalls
|
|
57
|
-
| **Forms & value binding** | `forms-and-binding.md` | Wiring an input / select / checkbox / rating to Signal Forms, Reactive Forms, or `ngModel`
|
|
58
|
-
| **Accessible names** | `accessible-names.md` | Naming buttons, icon-only controls, overlays; using `skra-sr-only`
|
|
59
|
-
| **Theming & encapsulation** | `theming-and-encapsulation.md` | Skins, dark mode, density; mounting under Shadow DOM / a microfrontend / an iframe
|
|
60
|
-
| **Overlays** | `overlays.md` | Dialog / tooltip / popover / menu / select in the top layer; shadow & iframe limits
|
|
61
|
-
| **Layout** | `layout.md` | Spacing & composition with Stack / Box / Surface / Grid; building a card; the gap scale
|
|
62
|
-
| **Wrapping Skra**
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
|
70
|
-
|
|
|
71
|
-
|
|
|
72
|
-
| Bind a
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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).
|