@skra/ui 0.6.0 → 0.8.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,88 +1,92 @@
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._
1
+ # Consuming Skra — Agent Guide (Core)
2
+
3
+ > **DRAFT (2026-06-25; 8th section added 2026-08-30; 9th — Feedback — 2026-09-27).** All 9 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
+ | **File upload** | `file-upload.md` | Choosing files (picker / drop zone), a file list with progress, the queue model, a tus upload; `accept` and language limits | ✅ draft |
61
+ | **Feedback** | `feedback.md` | Alert vs toast vs badge vs field error; `live` announcing; dismissal; an alert with a next-step action | ✅ draft |
62
+ | **Overlays** | `overlays.md` | Dialog / tooltip / popover / menu / select in the top layer; shadow & iframe limits | ✅ draft |
63
+ | **Layout** | `layout.md` | Spacing & composition with Stack / Box / Surface / Grid; building a card; the gap scale | ✅ draft |
64
+ | **Wrapping Skra** | `wrapping-patterns.md` | Building your own higher-level components over Skra atoms (simplified API) | ✅ draft |
65
+ | **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 |
66
+
67
+ ---
68
+
69
+ ## Quick reference — task → section
70
+
71
+ | I want to… | Go to |
72
+ | ----------------------------------------------------------------------- | ----------------------------------------- |
73
+ | Add `@skra/ui/*` to a project and import an atom | `setup-and-gotchas.md` |
74
+ | Bind a text input / textarea to a form | `forms-and-binding.md` |
75
+ | Bind a `skra-select` / `skra-datepicker` / `skra-star-rating` to a form | `forms-and-binding.md` (the value bridge) |
76
+ | Name an icon-only button | `accessible-names.md` |
77
+ | Switch skin / dark mode at runtime | `theming-and-encapsulation.md` |
78
+ | Mount Skra inside a Web Component / microfrontend | `theming-and-encapsulation.md` |
79
+ | Let the user pick or drop files, show them with progress, upload them | `file-upload.md` |
80
+ | Show an alert / toast / badge, or add a "Try again" button to an alert | `feedback.md` |
81
+ | Open a dialog or tooltip | `overlays.md` |
82
+ | Space components apart | `layout.md` |
83
+ | Build a card / panel / tile (there is no `skra-card`) | `layout.md` (Surface) |
84
+ | Wrap a Skra atom in a project-specific component | `wrapping-patterns.md` |
85
+ | Build a widget Skra does not ship (scrubber, timeline, domain visual) | `custom-components.md` |
86
+ | Keep an app-local component on-skin, on-density, dark-mode-correct | `custom-components.md` |
87
+
88
+ ---
89
+
90
+ _Source of truth for the rules: Skra `FORGE_ADR_SUMMARY.md` + `adr/decisions/`. Real
91
+ consumption evidence: `apps/dogfood/FRICTION_JOURNAL.md`. This guide restates the consumer
92
+ slice of those — on divergence, the ADRs win._
@@ -42,7 +42,8 @@ must be loaded (and bridged into any shadow root — see `theming-and-encapsulat
42
42
  or the text renders visibly.
43
43
 
44
44
  > The same pattern names other built-in sr-only slots, e.g. `[skra-alert-close-label]` on
45
- > `skra-alert`'s dismiss button.
45
+ > `skra-alert`'s dismiss button. An alert **action** (`[skra-alert-actions]`) names itself by its
46
+ > visible text (Path 1); in a live alert that label is announced with the message (`feedback.md`).
46
47
 
47
48
  ## Path 3 — `aria-label` on an icon-only host
48
49
 
@@ -0,0 +1,100 @@
1
+ # Feedback — Alerts, Toasts, Badges
2
+
3
+ > Load this for: telling the user what happened — an in-page alert, a transient toast, a status
4
+ > badge — and putting a next step ("Try again") inside an alert.
5
+ > Governed by ADR 0052 (live-region semantics), ADR 035 (intent / emphasis) and ADR 0059
6
+ > (actions on an intent-tinted ground).
7
+
8
+ ## Which one?
9
+
10
+ | You need to… | Use |
11
+ | ---------------------------------------------------------- | ---------------------------------------------------------------------- |
12
+ | Say something about a page or a section, and keep it there | `<skra-alert>` (`@skra/ui/alert`) |
13
+ | Confirm an action briefly without moving focus ("Saved") | `<skra-toast>` inside one `<skra-toast-region>` (`@skra/ui/toast`) |
14
+ | Label a thing with a short status ("Draft", "3 new") | `<skra-badge>` (`@skra/ui/badge`) |
15
+ | Say what is wrong with ONE form value | `<skra-error>` inside `<skra-field>` (`@skra/ui/field`) — not an alert |
16
+
17
+ The alert, the toast and the badge take the same two look-free axes (`<skra-error>` is always an error): `intent` (`neutral` · `info` · `success` · `warning` ·
18
+ `danger`) and `emphasis` (`high` = filled, `medium` = soft; the default). Put the severity in the
19
+ **text** too — colour and icon alone do not carry meaning (WCAG 1.4.1).
20
+
21
+ ## Announcing (`live`)
22
+
23
+ `live` decides whether a screen reader announces the content when it appears; it maps to the
24
+ implicit `role` (`polite` → `status`, `assertive` → `alert`). It is about **how the content
25
+ arrives**, not how severe it is:
26
+
27
+ - present when the page loads → leave `live="off"` (alert default);
28
+ - inserted after the user did something → `live="polite"`, or `"assertive"` for a failure that
29
+ blocks them.
30
+
31
+ Skra sets the role only — never add `aria-live` / `aria-atomic` yourself. A live region announces
32
+ its whole text, **including the labels of the controls inside it** (the dismiss label, an action).
33
+
34
+ ## Dismissal is yours
35
+
36
+ `dismissible` renders a close control and emits `(dismissed)`. The alert does **not** remove
37
+ itself — you do, with `@if`, the same way you showed it. Name the control with a projected
38
+ `[skra-alert-close-label]` (visually hidden text; `accessible-names.md`). If the close control had
39
+ focus, move focus somewhere sensible before you remove the alert.
40
+
41
+ ```html
42
+ @if (saveFailed()) {
43
+ <skra-alert intent="danger" live="assertive" dismissible (dismissed)="saveFailed.set(false)">
44
+ Could not save.
45
+ <span skra-alert-close-label class="skra-sr-only">Dismiss</span>
46
+ </skra-alert>
47
+ }
48
+ ```
49
+
50
+ ## An alert with an action
51
+
52
+ Mark each next-step button `[skra-alert-actions]`. The alert places the actions **below the
53
+ message**, as a row that wraps on a narrow container, and never lets them collide with the close
54
+ control. Several actions are fine; they keep your order.
55
+
56
+ ```html
57
+ <skra-alert intent="danger" live="assertive">
58
+ <span skra-alert-title>Could not save</span>
59
+ Your changes are kept on this device. Check your connection and try again.
60
+ <button skraButton skra-alert-actions emphasis="medium" type="button" (click)="retry()">Try again</button>
61
+ </skra-alert>
62
+ ```
63
+
64
+ **The action keeps its own meaning.** The alert does not pass its `intent` to the button. A retry
65
+ inside a `danger` alert is not a destructive action, so it must not be a `danger` button — that
66
+ colour means "this deletes something". Choose the button's `intent` for what the button **does**,
67
+ exactly as anywhere else.
68
+
69
+ **What is guaranteed legible on the alert's colour.** Skra measures these combinations in every
70
+ state (rest, hover, pressed, focus), both colour schemes and every shipped skin:
71
+
72
+ | Alert | Guaranteed actions |
73
+ | ----------------------------- | ------------------------------------------------------------------------------- |
74
+ | `emphasis="medium"` (default) | a `neutral` button at any emphasis; a button of any intent at `emphasis="high"` |
75
+ | `emphasis="high"` (filled) | a button of any intent at `emphasis="high"` **other than the alert's own** |
76
+
77
+ - The zero-config `<button skraButton skra-alert-actions>` (`neutral`, `high`) is guaranteed on
78
+ every alert except a `neutral` + `high` one.
79
+ - **Not supported:** a coloured `medium`/`low` button; a `medium`/`low` button on a filled alert
80
+ (it measured as low as 1.2:1); a `high` button in the alert's own intent (it has the alert's
81
+ exact colour — no visible button). Nothing stops you writing them; nothing guarantees them either.
82
+ - Only `button[skraButton]` is measured in the actions row. A link or a badge there is on you.
83
+ - The focus ring of the actions follows the alert (on a filled alert it switches to the alert's
84
+ text colour, because the page ring disappears on the fill). To re-colour it, target the alert's
85
+ per-variant hook `--skra-comp-alert-focus-ring-color-<intent>-<emphasis>` in a skin overlay preset
86
+ (the only tier its manifest allows) — never the button's ring.
87
+ - An action that **navigates** is still a `<button>` that routes — Skra has no link styled as a
88
+ button (ADR 032).
89
+
90
+ ## Toasts
91
+
92
+ One `<skra-toast-region>` near the app root announces every toast from persistent live regions;
93
+ each `<skra-toast>` sets `intent`, `emphasis`, `live` (default `polite`), `duration` (ms; pauses on
94
+ hover / focus) and `dismissible` → `(dismissed)`. Removal is yours, as for the alert. Toasts do
95
+ not take actions yet.
96
+
97
+ ## Badges
98
+
99
+ `<skra-badge>` is a small, inline, non-interactive status label (`intent`, `emphasis`, optional
100
+ `[skra-badge-icon]`). Leave `live="off"` unless the badge changes while the user watches it.
@@ -0,0 +1,210 @@
1
+ # File upload — choosing files, showing them, uploading them yourself
2
+
3
+ > Section of the Skra consumer guide (added 2026-11-01, O-80). Load it when you need a file
4
+ > picker, a drop zone, or a list of files with progress. Source of truth:
5
+ > `adr/specs/component-file-upload-spec.md` + ADR 0056 Groups B and D.
6
+
7
+ Entry point: **`@skra/ui/file-upload`** — three atoms and two helpers:
8
+
9
+ | Export | What it is |
10
+ | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
11
+ | `SkraFileInput` — `input[type="file"][skraFileInput]` | The native control, skinned. Field wiring, drop normalisation, `filesChange` / `filesRejected`, `clear()`. |
12
+ | `SkraFileUpload` — `<skra-file-upload>` | Optional face over the input: your own action text, a drop zone, a live status. No inputs, no outputs. |
13
+ | `SkraFileItem` — `li[skraFileItem]` | One row of YOUR file list: middle-truncated name, formatted size, slots for preview / progress / error / actions. |
14
+ | `SkraFileUploadAction` / `…Instruction` / `…Status` / `…Start`, `SkraFileItemStart` / `…Detail` / `…Error` / `…End` | Slot markers — import the ones you use (projection works without them; slot DETECTION needs them). |
15
+ | `skraFormatFileSize(bytes, locale?)`, `skraFileMatchesAccept(file, accept)` | Pure helpers: SI sizes via `Intl` (`2,3 GB`), the HTML `accept` grammar. |
16
+
17
+ **Skra never uploads.** Transport (tus, multipart `fetch`, S3 presigned `PUT`), retry,
18
+ pause / resume and queue policy are yours. Skra gives you the control, the drop surface,
19
+ the rows and the two helpers.
20
+
21
+ ---
22
+
23
+ ## 1. Native atom or composite? — decide first
24
+
25
+ | You need… | Use |
26
+ | ----------------------------------------------------------- | -------------------------------------------------------------- |
27
+ | A plain file field in a form, browser wording is acceptable | `input[skraFileInput]` alone, inside `<skra-field>` |
28
+ | The button / status text **in your page's language** | `<skra-file-upload>` around the input |
29
+ | A drop zone ("or drag it here") | `<skra-file-upload>` **with** `[skra-file-upload-instruction]` |
30
+ | A compact, field-height row with your own wording | `<skra-file-upload>` **without** an instruction |
31
+ | A list of chosen / uploading files | a **sibling** `ul[skraStack]` of `li[skraFileItem]` |
32
+
33
+ **Why the composite exists.** The native control's two strings — "Choose File" and
34
+ "No file chosen" — come from the **browser's UI language, not your page's `lang`**, and no CSS
35
+ or attribute can replace them (measured in Chromium, ADR 0056 §1.6). A Polish app in an
36
+ English browser shows an English button. `<skra-file-upload>` paints its own face with your
37
+ text while the native input stays the real control — transparent, on top, covering the whole
38
+ face — so focus, keyboard, the OS picker (incl. the iOS camera / library sheet), `required`,
39
+ form submission and drops all keep working.
40
+
41
+ Even with the composite, the control's **accessibility value** stays the browser's string
42
+ ("No file chosen"); that is why your status text is linked as a description and announced.
43
+
44
+ ---
45
+
46
+ ## 2. The patterns
47
+
48
+ ### Single file in a form (`station`'s case)
49
+
50
+ ```html
51
+ <skra-field [hasError]="!!error()">
52
+ <label skraLabelFor>Session recording</label>
53
+ <skra-file-upload>
54
+ <skra-icon skra-start aria-hidden="true">…</skra-icon>
55
+ <span skra-file-upload-action>Choose a video</span>
56
+ <span skra-file-upload-status>{{ summary() }}</span>
57
+ <input skraFileInput type="file" accept="video/*" (filesChange)="onFiles($event)" (filesRejected)="onRejected($event)" />
58
+ </skra-file-upload>
59
+ <skra-hint>MP4 or MOV, up to 20 GB. Required.</skra-hint>
60
+ <skra-error>{{ error() }}</skra-error>
61
+ </skra-field>
62
+ ```
63
+
64
+ ```ts
65
+ readonly file = signal<File | null>(null);
66
+ readonly error = signal('');
67
+ readonly summary = computed(() => {
68
+ const f = this.file();
69
+ return f ? `${f.name}, ${skraFormatFileSize(f.size, 'pl-PL')}` : 'Nie wybrano nagrania';
70
+ });
71
+
72
+ onFiles(files: readonly File[]) {
73
+ const f = files[0] ?? null;
74
+ // Product policy is YOURS — Skra enforces only what the platform's picker enforces.
75
+ if (f && f.size > 20e9) { this.error.set('Plik jest większy niż 20 GB.'); return; }
76
+ this.error.set('');
77
+ this.file.set(f);
78
+ }
79
+
80
+ onRejected(rejected: readonly SkraFileRejection[]) {
81
+ const r = rejected[0];
82
+ this.error.set(
83
+ r.reason === 'type' ? `${r.file.name} nie jest plikiem wideo.`
84
+ : r.reason === 'count' ? 'Upuść jedno nagranie naraz.'
85
+ : `${r.file.name} to folder — wybierz plik.`,
86
+ );
87
+ }
88
+ ```
89
+
90
+ - The name is **"field label + your action text"** automatically (`aria-labelledby`; WCAG 2.5.3) —
91
+ **inside `<skra-field>` with `label[skraLabelFor]`**. Outside a field the action text alone names
92
+ the control, and that reference overrides a plain `<label for>` (dev mode warns).
93
+ Never put `aria-label` on the input.
94
+ - **Never project anything interactive into `<skra-file-upload>`** — the transparent input
95
+ covers the whole face and swallows the click (dev mode reports it). Buttons go in the list.
96
+
97
+ ### Many files + a list
98
+
99
+ ```html
100
+ <skra-file-upload>
101
+ <span skra-file-upload-action>Choose videos</span>
102
+ <span skra-file-upload-instruction>or drag them here</span>
103
+ <span skra-file-upload-status>{{ summary() }}</span>
104
+ <input skraFileInput type="file" accept="video/*" multiple (filesChange)="add($event)" />
105
+ </skra-file-upload>
106
+
107
+ <ul skraStack gap="sm" aria-label="Uploads">
108
+ @for (u of uploads(); track u.id) {
109
+ <li skraFileItem [name]="u.file.name" [size]="u.file.size" locale="pl-PL">
110
+ <progress skraProgress skra-file-item-detail [value]="u.sent" [max]="u.file.size" [attr.aria-label]="'Wysyłanie ' + u.file.name"></progress>
111
+ @if (u.error) { <span skra-file-item-error>{{ u.error }}</span> }
112
+ <button skraButton skra-end emphasis="low" (click)="cancel(u)">Anuluj<span skra-sr-only> {{ u.file.name }}</span></button>
113
+ </li>
114
+ }
115
+ </ul>
116
+ ```
117
+
118
+ - A row turns **invalid only while `[skra-file-item-error]` text is projected** — no `hasError`
119
+ input exists, so the colour can never appear without the words.
120
+ - Row actions: visible verb + `[skra-sr-only]` file name (ADR 033 Path 2). The progress bar has no
121
+ text, so it takes `aria-label` (Path 3).
122
+ - Previews are your content: put an `<img [src]="objectUrl">` in `[skra-start]`
123
+ (`URL.createObjectURL`, and `revokeObjectURL` when the row goes).
124
+
125
+ ### The queue model — `clear()` so the same file can be picked again
126
+
127
+ A file input fires no `change` when the user picks the file that is already selected. In a
128
+ queue (pick → enqueue → pick again) clear the native selection after taking the files:
129
+
130
+ ```html
131
+ <input skraFileInput type="file" multiple #picker (filesChange)="enqueue($event); picker.clear()" />
132
+ ```
133
+
134
+ `clear()` emits nothing. `#picker` resolves to the `SkraFileInput` component.
135
+
136
+ ### A resumable upload with tus (multi-GB, `station`)
137
+
138
+ Skra hands you `File`s; the transport is yours. With `tus-js-client` (your dependency):
139
+
140
+ ```ts
141
+ import { Upload } from 'tus-js-client';
142
+
143
+ start(file: File) {
144
+ const row = { id: crypto.randomUUID(), file, sent: 0, error: '' };
145
+ this.uploads.update((all) => [...all, row]);
146
+ const upload = new Upload(file, {
147
+ endpoint: '/api/uploads',
148
+ chunkSize: 50 * 1024 * 1024,
149
+ retryDelays: [0, 3000, 10000, 30000],
150
+ metadata: { filename: file.name, filetype: file.type },
151
+ onProgress: (sent) => this.patch(row.id, { sent }),
152
+ onError: () => this.patch(row.id, { error: 'Połączenie przerwane. Spróbuj ponownie.' }),
153
+ onSuccess: () => this.patch(row.id, { sent: file.size }),
154
+ });
155
+ upload.findPreviousUploads().then((prev) => {
156
+ if (prev.length) upload.resumeFromPreviousUpload(prev[0]);
157
+ upload.start();
158
+ });
159
+ }
160
+ ```
161
+
162
+ While any `<skra-file-upload>` is mounted, a file dropped a few pixels **outside** the zone is
163
+ cancelled instead of making the browser navigate to it (which would abort the upload). Text and
164
+ link drags, any native `<input type="file">` and editable content are left alone.
165
+
166
+ ---
167
+
168
+ ## 3. Rules and limits you must know
169
+
170
+ 1. **`accept` is never a guarantee.** Every OS lets the user bypass it ("All files", Android
171
+ pickers). Skra re-checks picks AND drops against it (`filesRejected` reason `type`), but the
172
+ **server must validate** type and size.
173
+ 2. **Skra enforces only what the platform's picker enforces**: `type` (accept), `count` (several
174
+ files on a non-`multiple` input — ALL are rejected, never one of N), `directory` (a dropped
175
+ folder). Size limits, a maximum count, duplicates — yours, in `filesChange`.
176
+ 3. **Typeless files** (`File.type === ''`, common for `.mkv` / `.heic`): matched by extension
177
+ tokens only, and **accepted** when `accept` lists only MIME types. Add `.mkv` etc. to `accept`
178
+ if you want a strict list.
179
+ 4. **Order: `filesChange` first, `filesRejected` last** — on both paths. Clear your error in
180
+ `onFiles` and set it in `onRejected`, and a mixed selection ends on the message. A selection where
181
+ every file was rejected emits only `filesRejected`.
182
+ 5. **`webkitdirectory`** hands over every file of the folder and counts as multiple — no `count`
183
+ rejection, even without `multiple`.
184
+ 6. **Native `(change)` vs `filesChange`.** `(change)` still fires and reports the RAW selection;
185
+ `filesChange` is the normalised one. A drop that is fully rejected changes nothing (no
186
+ `change`, only `filesRejected`).
187
+ 7. **"Required" does not reach screen readers on a file input in Chromium.** The control is
188
+ exposed as a `button`, which carries neither `aria-required` nor native `required` (measured).
189
+ Skra still sets `aria-required` from `<skra-field required>`; **also say "required" in visible
190
+ text** — the label or the hint.
191
+ 8. **Readonly** (`<skra-field readonly>`): native `readonly` does not apply to file inputs, and
192
+ `aria-readonly` is silent for a `button`, so the control is `aria-disabled` (still focusable,
193
+ announced "unavailable"); the picker and drops are blocked.
194
+ 9. **The native atom alone shows the browser's words** — fine for internal tools; use the
195
+ composite when the page language matters. In a narrow container the browser also CLIPS its
196
+ own status text ("No file ch…") — Skra cannot wrap a user-agent internal. The composite
197
+ wraps instead: its status moves below the action, and a file row's actions move below the
198
+ name, whenever they cannot share a line with at least half of it.
199
+ 10. **Inputs you will NOT find**: `accept`, `multiple`, `capture`, `required`, `disabled` are the
200
+ native attributes on your `<input>` — Skra reads them, never mirrors them. No `ariaLabel`, no
201
+ `maxSize`, no upload URL.
202
+
203
+ ---
204
+
205
+ ## 4. Verification status
206
+
207
+ Built and verified on Chromium, Firefox and desktop WebKit by automated tests (the picker opens
208
+ from every point of the face, drop normalisation, drag-over, height parity at every density,
209
+ the accessibility tree in Chromium, forced colours in Chromium). **Real-device iOS Safari,
210
+ Android and screen-reader passes are still open (O-81)** — see the gate table in the spec.
@@ -10,43 +10,44 @@ will break on any internal refactor). Skra ships as **one package, `@skra/ui`**,
10
10
  secondary entry point per component (ADR 0054 / O-31 consolidation). The primary `@skra/ui`
11
11
  entry exports nothing — always import from a `@skra/ui/<entry>` subpath.
12
12
 
13
- | Entry point | Exports (representative) |
14
- | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
15
- | `@skra/ui/button` | `button[skraButton]` |
16
- | `@skra/ui/link` | `a[skraLink]` |
17
- | `@skra/ui/icon` | icon container atom |
18
- | `@skra/ui/typography` | `[skraTypography]` |
19
- | `@skra/ui/layout` | `[skraStack]`, `[skraBox]`, `[skraGrid]`, `[skraSurface]` |
20
- | `@skra/ui/kbd` | `kbd[skraKbd]` |
13
+ | Entry point | Exports (representative) |
14
+ | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
15
+ | `@skra/ui/button` | `button[skraButton]` |
16
+ | `@skra/ui/link` | `a[skraLink]` |
17
+ | `@skra/ui/icon` | icon container atom |
18
+ | `@skra/ui/typography` | `[skraTypography]` |
19
+ | `@skra/ui/layout` | `[skraStack]`, `[skraBox]`, `[skraGrid]`, `[skraSurface]` |
20
+ | `@skra/ui/kbd` | `kbd[skraKbd]` |
21
21
  | `@skra/ui/field` | `<skra-field>`, `[skraLabel]`, `[skraHint]`, `[skraError]`, `fieldset[skraFieldset]`, `legend[skraLegend]`, `SkraFieldContext` |
22
- | `@skra/ui/input` | `input[skraInput]` |
23
- | `@skra/ui/textarea` | `textarea[skraTextarea]`, auto-resize |
24
- | `@skra/ui/checkbox` | `input[skraCheckbox]` |
25
- | `@skra/ui/radio` | `input[skraRadio]` |
26
- | `@skra/ui/switch` | `input[skraSwitch]` |
27
- | `@skra/ui/counter` | `skra-counter` |
28
- | `@skra/ui/star-rating` | `skra-star-rating` |
29
- | `@skra/ui/native-select` | `select[skraSelect]` (`SkraNativeSelect`) |
30
- | `@skra/ui/dialog` | `dialog[skraDialog]` |
31
- | `@skra/ui/tooltip` | `[skraTooltip]` |
32
- | `@skra/ui/popover` | `skra-popover` |
33
- | `@skra/ui/menu` | `skra-menu`, `skra-menu-item` |
34
- | `@skra/ui/select` | `skra-select` (+ `SkraOption` & option directives, re-exported) |
35
- | `@skra/ui/multi-select` | `skra-multi-select` (+ `SkraOption` & option directives, re-exported) |
36
- | `@skra/ui/combobox` | `skra-combobox` (+ `SkraOption` & option directives, re-exported) |
37
- | `@skra/ui/datepicker` | `skra-datepicker` |
38
- | `@skra/ui/alert` | `skra-alert` |
39
- | `@skra/ui/badge` | `skra-badge` |
40
- | `@skra/ui/chip` | `skra-chip` |
41
- | `@skra/ui/toast` | `skra-toast`, `skra-toast-region` |
42
- | `@skra/ui/tabs` | `[skraTabs]`, `[skraTablist]`, `button[skraTab]`, `[skraTabPanel]` |
43
- | `@skra/ui/accordion` | `[skraAccordion]` family |
44
- | `@skra/ui/progress` | `progress[skraProgress]` |
45
- | `@skra/ui/skeleton` | `skra-skeleton` |
46
- | `@skra/ui/loading-indicator` | `[skraLoadingIndicator]` |
47
- | `@skra/ui/avatar` | `skra-avatar` |
48
- | `@skra/ui/status-dot` | `skra-status-dot` |
49
- | `@skra/ui/rating-display` | `skra-rating-display` |
22
+ | `@skra/ui/input` | `input[skraInput]` |
23
+ | `@skra/ui/textarea` | `textarea[skraTextarea]`, auto-resize |
24
+ | `@skra/ui/checkbox` | `input[skraCheckbox]` |
25
+ | `@skra/ui/radio` | `input[skraRadio]` |
26
+ | `@skra/ui/switch` | `input[skraSwitch]` |
27
+ | `@skra/ui/counter` | `skra-counter` |
28
+ | `@skra/ui/star-rating` | `skra-star-rating` |
29
+ | `@skra/ui/file-upload` | `input[skraFileInput]`, `skra-file-upload`, `li[skraFileItem]`, `skraFormatFileSize`, `skraFileMatchesAccept` |
30
+ | `@skra/ui/native-select` | `select[skraSelect]` (`SkraNativeSelect`) |
31
+ | `@skra/ui/dialog` | `dialog[skraDialog]` |
32
+ | `@skra/ui/tooltip` | `[skraTooltip]` |
33
+ | `@skra/ui/popover` | `skra-popover` |
34
+ | `@skra/ui/menu` | `skra-menu`, `skra-menu-item` |
35
+ | `@skra/ui/select` | `skra-select` (+ `SkraOption` & option directives, re-exported) |
36
+ | `@skra/ui/multi-select` | `skra-multi-select` (+ `SkraOption` & option directives, re-exported) |
37
+ | `@skra/ui/combobox` | `skra-combobox` (+ `SkraOption` & option directives, re-exported) |
38
+ | `@skra/ui/datepicker` | `skra-datepicker` |
39
+ | `@skra/ui/alert` | `skra-alert` |
40
+ | `@skra/ui/badge` | `skra-badge` |
41
+ | `@skra/ui/chip` | `skra-chip` |
42
+ | `@skra/ui/toast` | `skra-toast`, `skra-toast-region` |
43
+ | `@skra/ui/tabs` | `[skraTabs]`, `[skraTablist]`, `button[skraTab]`, `[skraTabPanel]` |
44
+ | `@skra/ui/accordion` | `[skraAccordion]` family |
45
+ | `@skra/ui/progress` | `progress[skraProgress]` |
46
+ | `@skra/ui/skeleton` | `skra-skeleton` |
47
+ | `@skra/ui/loading-indicator` | `[skraLoadingIndicator]` |
48
+ | `@skra/ui/avatar` | `skra-avatar` |
49
+ | `@skra/ui/status-dot` | `skra-status-dot` |
50
+ | `@skra/ui/rating-display` | `skra-rating-display` |
50
51
 
51
52
  > `@skra/ui/core` exists for cross-component shared satellites (`SkraOption` & option
52
53
  > directives, `SkraSelectTrigger`, `SKRA_LISTBOX_PANEL`). Prefer the re-exports from