@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.
- package/consumer-guide/README.md +92 -88
- package/consumer-guide/accessible-names.md +2 -1
- package/consumer-guide/feedback.md +100 -0
- package/consumer-guide/file-upload.md +210 -0
- package/consumer-guide/setup-and-gotchas.md +37 -36
- package/fesm2022/skra-ui-alert.mjs +55 -6
- package/fesm2022/skra-ui-alert.mjs.map +1 -1
- package/fesm2022/skra-ui-button.mjs +2 -2
- package/fesm2022/skra-ui-button.mjs.map +1 -1
- package/fesm2022/skra-ui-chip.mjs +2 -2
- package/fesm2022/skra-ui-chip.mjs.map +1 -1
- package/fesm2022/skra-ui-core.mjs +2 -2
- package/fesm2022/skra-ui-core.mjs.map +1 -1
- package/fesm2022/skra-ui-file-upload.mjs +894 -0
- package/fesm2022/skra-ui-file-upload.mjs.map +1 -0
- package/fesm2022/skra-ui-toast.mjs +2 -2
- package/fesm2022/skra-ui-toast.mjs.map +1 -1
- package/package.json +6 -2
- package/types/skra-ui-alert.d.ts +39 -2
- package/types/skra-ui-alert.d.ts.map +1 -1
- package/types/skra-ui-file-upload.d.ts +360 -0
- package/types/skra-ui-file-upload.d.ts.map +1 -0
package/consumer-guide/README.md
CHANGED
|
@@ -1,88 +1,92 @@
|
|
|
1
|
-
# Consuming Skra — Agent Guide (Core)
|
|
2
|
-
|
|
3
|
-
> **DRAFT (2026-06-25; 8th section added 2026-08-30).** 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
|
-
| **
|
|
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
|
-
|
|
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/
|
|
30
|
-
| `@skra/ui/
|
|
31
|
-
| `@skra/ui/
|
|
32
|
-
| `@skra/ui/
|
|
33
|
-
| `@skra/ui/
|
|
34
|
-
| `@skra/ui/
|
|
35
|
-
| `@skra/ui/
|
|
36
|
-
| `@skra/ui/
|
|
37
|
-
| `@skra/ui/
|
|
38
|
-
| `@skra/ui/
|
|
39
|
-
| `@skra/ui/
|
|
40
|
-
| `@skra/ui/
|
|
41
|
-
| `@skra/ui/
|
|
42
|
-
| `@skra/ui/
|
|
43
|
-
| `@skra/ui/
|
|
44
|
-
| `@skra/ui/
|
|
45
|
-
| `@skra/ui/
|
|
46
|
-
| `@skra/ui/
|
|
47
|
-
| `@skra/ui/
|
|
48
|
-
| `@skra/ui/
|
|
49
|
-
| `@skra/ui/
|
|
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
|