@skra/ui 0.7.0 → 0.9.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 -90
- package/consumer-guide/file-upload.md +210 -0
- package/consumer-guide/layout.md +116 -38
- package/consumer-guide/setup-and-gotchas.md +37 -36
- package/fesm2022/skra-ui-file-upload.mjs +894 -0
- package/fesm2022/skra-ui-file-upload.mjs.map +1 -0
- package/fesm2022/skra-ui-layout.mjs +74 -8
- package/fesm2022/skra-ui-layout.mjs.map +1 -1
- package/fesm2022/skra-ui-link.mjs +62 -2
- package/fesm2022/skra-ui-link.mjs.map +1 -1
- package/package.json +6 -2
- package/types/skra-ui-file-upload.d.ts +360 -0
- package/types/skra-ui-file-upload.d.ts.map +1 -0
- package/types/skra-ui-layout.d.ts +31 -2
- package/types/skra-ui-layout.d.ts.map +1 -1
- package/types/skra-ui-link.d.ts +28 -1
- package/types/skra-ui-link.d.ts.map +1 -1
package/consumer-guide/README.md
CHANGED
|
@@ -1,90 +1,92 @@
|
|
|
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
|
|
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
|
-
| Bind a
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
| Build a
|
|
84
|
-
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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; cards, nav links, 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._
|
|
@@ -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.
|
package/consumer-guide/layout.md
CHANGED
|
@@ -11,8 +11,12 @@ a native host** (a bounded allowlist of `div`/`section`/`nav`/`ul`/…), **never
|
|
|
11
11
|
`<skra-stack>` custom element. Put them on the semantic element you'd use anyway:
|
|
12
12
|
|
|
13
13
|
```html
|
|
14
|
-
<section skraStack gap="md">…</section>
|
|
15
|
-
|
|
14
|
+
<section skraStack gap="md">…</section>
|
|
15
|
+
<!-- not <div><skra-stack> -->
|
|
16
|
+
<ul skraStack gap="sm">
|
|
17
|
+
…
|
|
18
|
+
</ul>
|
|
19
|
+
<!-- list markers normalized, restorable -->
|
|
16
20
|
```
|
|
17
21
|
|
|
18
22
|
## Sizing container-filling components (e.g. `skra-select`)
|
|
@@ -38,16 +42,16 @@ width.
|
|
|
38
42
|
|
|
39
43
|
Arranges children in a row or column.
|
|
40
44
|
|
|
41
|
-
| Input
|
|
42
|
-
|
|
|
43
|
-
| `direction` | `row` \| `column`
|
|
44
|
-
| `gap`
|
|
45
|
-
| `align`
|
|
46
|
-
| `justify`
|
|
47
|
-
| `wrap`
|
|
45
|
+
| Input | Values | Default |
|
|
46
|
+
| ----------- | ------------------------------------------------------- | --------- |
|
|
47
|
+
| `direction` | `row` \| `column` | `column` |
|
|
48
|
+
| `gap` | `none` \| `sm` \| `md` \| `lg` | `md` |
|
|
49
|
+
| `align` | `start` \| `center` \| `end` \| `stretch` \| `baseline` | `stretch` |
|
|
50
|
+
| `justify` | `start` \| `center` \| `end` \| `between` \| `around` | `start` |
|
|
51
|
+
| `wrap` | boolean | `false` |
|
|
48
52
|
|
|
49
53
|
```html
|
|
50
|
-
<div skraStack direction="row" gap="sm" wrap align="center">
|
|
54
|
+
<div skraStack direction="row" gap="sm" wrap [align]="'center'">
|
|
51
55
|
<skra-chip>One</skra-chip>
|
|
52
56
|
<skra-chip>Two</skra-chip>
|
|
53
57
|
</div>
|
|
@@ -56,13 +60,24 @@ Arranges children in a row or column.
|
|
|
56
60
|
(`wrap` is a bare boolean attribute — every Skra boolean input carries `booleanAttribute`, so
|
|
57
61
|
the bare form works; see `setup-and-gotchas.md`.)
|
|
58
62
|
|
|
63
|
+
**Gotcha — bind `align`, don't write it as a static attribute.** `align` is also a legacy HTML
|
|
64
|
+
attribute. Written statically (`<div skraStack align="center">`), Angular sets the input but the
|
|
65
|
+
attribute stays in the DOM, and the browser turns it into `text-align` on the host — Chromium
|
|
66
|
+
does so on every host Stack accepts (`div`, `section`, `header`, `ul`, …) and for both `center`
|
|
67
|
+
and `end`. Any text inside the stack is then centred or end-aligned. The bound form
|
|
68
|
+
`[align]="'center'"` sets the input without writing the attribute. The same applies to
|
|
69
|
+
`skraGrid`'s `align`. Since O-89 the atoms neutralise the hint themselves (`text-align: inherit`
|
|
70
|
+
while the attribute is present), so the static form no longer misaligns text — the bound form
|
|
71
|
+
stays the recommended one. If you want aligned text inside a stack, set `text-align` on a child
|
|
72
|
+
(or on the host with a selector more specific than a single class).
|
|
73
|
+
|
|
59
74
|
## Box — padding / spacing (`[skraBox]`)
|
|
60
75
|
|
|
61
76
|
A semantically-neutral padding primitive. Does **not** arrange children (no list hosts).
|
|
62
77
|
|
|
63
|
-
| Input
|
|
64
|
-
|
|
|
65
|
-
| `padding` | `none` \| `sm` \| `md` \| `lg`
|
|
78
|
+
| Input | Values | Default |
|
|
79
|
+
| --------- | ----------------------------------- | ------- |
|
|
80
|
+
| `padding` | `none` \| `sm` \| `md` \| `lg` | `none` |
|
|
66
81
|
| `display` | `block` \| `inline-block` \| `flex` | `block` |
|
|
67
82
|
|
|
68
83
|
```html
|
|
@@ -74,8 +89,8 @@ A semantically-neutral padding primitive. Does **not** arrange children (no list
|
|
|
74
89
|
Box's **painting** sibling: background, border, radius, shadow — plus its own padding. Same
|
|
75
90
|
host allowlist as Box (`div`/`section`/`article`/`main`/`nav`/`aside`/`header`/`footer`/`figure`).
|
|
76
91
|
|
|
77
|
-
| Input
|
|
78
|
-
|
|
|
92
|
+
| Input | Values | Default |
|
|
93
|
+
| --------- | ------------------------------ | -------- |
|
|
79
94
|
| `padding` | `none` \| `sm` \| `md` \| `lg` | **`md`** |
|
|
80
95
|
|
|
81
96
|
That is the whole API. Note the default differs from Box's `none` — Box is spacing-neutral by
|
|
@@ -109,16 +124,16 @@ There is no `selected` input, and there never will be one — `aria-selected` is
|
|
|
109
124
|
producing invalid ARIA. Instead, Surface paints a selected state when **your own markup** says
|
|
110
125
|
so, from exactly two signals:
|
|
111
126
|
|
|
112
|
-
| Your markup
|
|
113
|
-
|
|
|
114
|
-
| `aria-current="true"` (or `page` / `step` / `date`) on the Surface host
|
|
115
|
-
| `aria-selected="true"` on a host that **already** carries `option`/`row`/`gridcell`/`tab` | only inside a listbox / grid / tablist you actually built
|
|
127
|
+
| Your markup | When it is the right one |
|
|
128
|
+
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
129
|
+
| `aria-current="true"` (or `page` / `step` / `date`) on the Surface host | the current item of a set — a global ARIA state, so it is valid on a role-less `<article>` |
|
|
130
|
+
| `aria-selected="true"` on a host that **already** carries `option`/`row`/`gridcell`/`tab` | only inside a listbox / grid / tablist you actually built |
|
|
116
131
|
|
|
117
132
|
```html
|
|
118
133
|
<article skraSurface padding="md" aria-current="true" aria-labelledby="t">…</article>
|
|
119
134
|
```
|
|
120
135
|
|
|
121
|
-
`aria-current="false"` and `aria-selected="false"` are explicit
|
|
136
|
+
`aria-current="false"` and `aria-selected="false"` are explicit _not_-current and paint as
|
|
122
137
|
resting.
|
|
123
138
|
|
|
124
139
|
**Gotcha — a checked control inside the card is NOT a signal.** A card wrapping a checked
|
|
@@ -133,34 +148,97 @@ there: the radio already announces "selected", and adding `aria-current` makes a
|
|
|
133
148
|
technology announce one fact twice under two names. A dedicated marker for that shape is a known
|
|
134
149
|
gap (Skra-side `[skraSurfaceSelect]`), not something to emulate with a class.
|
|
135
150
|
|
|
136
|
-
### Interactive (clickable) cards
|
|
151
|
+
### Interactive (clickable) cards — `[skraSurfaceLink]`
|
|
137
152
|
|
|
138
153
|
Surface is a container, not a widget, and the host allowlist deliberately excludes `a` and
|
|
139
|
-
`button`: `<button>` cannot legally contain flow content
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
`button`: `<button>` cannot legally contain flow content, and an `<a>` wrapping the whole card
|
|
155
|
+
takes the card's **entire** text as its accessible name — which `aria-label` may not fix
|
|
156
|
+
(Iron Rule 3). Instead, put `skraSurfaceLink` on **your own** primary link (or `<button>`, when
|
|
157
|
+
the card performs an action) inside the Surface:
|
|
158
|
+
|
|
159
|
+
```html
|
|
160
|
+
<article skraSurface padding="lg">
|
|
161
|
+
<h3 skraTypography="title-small">
|
|
162
|
+
<a skraSurfaceLink routerLink="/runs/2026-08-24">Morning run</a>
|
|
163
|
+
</h3>
|
|
164
|
+
<p skraTypography="body-medium">5.2 km · 24:31</p>
|
|
165
|
+
</article>
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
What you get, with no inputs:
|
|
169
|
+
|
|
170
|
+
- The link's hit area covers the whole card; one tab stop; the accessible name is the link
|
|
171
|
+
text ("Morning run"), not the whole card body.
|
|
172
|
+
- Hover fills the card (pointer devices only — it does not latch after a tap), and the focus
|
|
173
|
+
ring is drawn **around the card**, the real target.
|
|
174
|
+
- The link text takes the card's text colour, without an underline (a skin may change that).
|
|
142
175
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
176
|
+
Rules:
|
|
177
|
+
|
|
178
|
+
- **One `skraSurfaceLink` per Surface.** A second one, or one with no Surface ancestor, is a
|
|
179
|
+
dev-mode console error. A list of targets is a list of Surfaces — `<li><article skraSurface>…` — and
|
|
180
|
+
a Surface nested in another paints only itself; the outer panel does not light up.
|
|
181
|
+
- **`skraLink` + `skraSurfaceLink` on one `<a>` is not possible** (both are components). The
|
|
182
|
+
marker already carries the text look.
|
|
183
|
+
- **A second control inside a linked card** (a "Share" button) sits under the overlay unless
|
|
184
|
+
you raise it: give it `position: relative` and keep it after the link in the DOM. Keep it at
|
|
185
|
+
least 24×24 CSS px (WCAG 2.5.8).
|
|
186
|
+
- Text inside a linked card cannot be selected with the mouse — a known, accepted cost of the
|
|
187
|
+
pattern.
|
|
188
|
+
- Do not make a card clickable with `(click)` on a `<div>` — that fails keyboard access
|
|
189
|
+
(WCAG 2.1.1) and has no role.
|
|
190
|
+
|
|
191
|
+
**Hover without a link:** an informational card has no hover state, and there is no
|
|
192
|
+
`hoverable` input. If you add your own hover paint in app CSS, gate it behind
|
|
193
|
+
`@media (hover: hover) and (pointer: fine)`, or it will latch after every tap on touch.
|
|
194
|
+
|
|
195
|
+
## Navigation — `a[skraNavLink]` (ADR 0060)
|
|
196
|
+
|
|
197
|
+
A primary or section navigation whose **current** destination is visible, not only announced.
|
|
198
|
+
`skraNavLink` is a link — middle-click, "open in new tab", "copy link" all stay — with a
|
|
199
|
+
surface shape (pill or square, the skin decides) and a current state:
|
|
200
|
+
|
|
201
|
+
```html
|
|
202
|
+
<nav aria-label="Primary">
|
|
203
|
+
<ul skraStack direction="row" gap="sm">
|
|
204
|
+
<li>
|
|
205
|
+
<a skraNavLink routerLink="/groups" routerLinkActive ariaCurrentWhenActive="page">Groups</a>
|
|
206
|
+
</li>
|
|
207
|
+
<li>
|
|
208
|
+
<a skraNavLink routerLink="/runs" routerLinkActive ariaCurrentWhenActive="page">Runs</a>
|
|
209
|
+
</li>
|
|
210
|
+
</ul>
|
|
211
|
+
</nav>
|
|
212
|
+
```
|
|
146
213
|
|
|
147
|
-
**
|
|
148
|
-
|
|
149
|
-
|
|
214
|
+
- **The current state comes from `aria-current`, which you (or the router) write.**
|
|
215
|
+
`skraNavLink` has no `active` input — it reads the attribute, so what the eye sees and what a
|
|
216
|
+
screen reader announces cannot disagree. Without `routerLinkActive` (or your own
|
|
217
|
+
`[attr.aria-current]`), no item ever shows as current. `aria-current="false"` is not current.
|
|
218
|
+
- **It must sit inside `<nav>`** (or `role="navigation"`); outside one it is a dev-mode console
|
|
219
|
+
error. A pill-shaped link in a hero section is a marketing CTA, which Skra does not ship
|
|
220
|
+
(ADR 032) — use `skraButton` for an action, `skraLink` for a link in text.
|
|
221
|
+
- **No `intent`, `emphasis`, `size` or `disabled`.** A destination the user cannot visit is not a
|
|
222
|
+
link: leave it out of the nav, or render it as plain text.
|
|
223
|
+
- **Name = the visible text.** Do not add `aria-label`.
|
|
224
|
+
- Spacing between items is the list's `skraStack` gap; for a side nav use
|
|
225
|
+
`direction="column"`.
|
|
226
|
+
- `skraLink` vs `skraNavLink`: underlined text in a sentence → `skraLink`; a destination in a
|
|
227
|
+
navigation bar → `skraNavLink`. Do not put both on one `<a>` (both are components).
|
|
150
228
|
|
|
151
229
|
## Grid — 2D (`[skraGrid]`)
|
|
152
230
|
|
|
153
231
|
Bucket-only CSS Grid. The default `columns="auto-fit"` + `min` gives an intrinsically
|
|
154
232
|
responsive card grid (column count tracks width, **no media/container queries**):
|
|
155
233
|
|
|
156
|
-
| Input
|
|
157
|
-
|
|
|
158
|
-
| `columns`
|
|
159
|
-
| `min`
|
|
160
|
-
| `gap`
|
|
161
|
-
| `flow`
|
|
162
|
-
| `align`
|
|
163
|
-
| `justifyItems` | `start` \| `center` \| `end` \| `stretch`
|
|
234
|
+
| Input | Values | Default |
|
|
235
|
+
| -------------- | ------------------------------------------------------------------------------------------------ | ---------- |
|
|
236
|
+
| `columns` | `1`…`6` \| `auto-fit` \| `auto-fill` | `auto-fit` |
|
|
237
|
+
| `min` | `sm` \| `md` \| `lg` | — |
|
|
238
|
+
| `gap` | `none` \| `sm` \| `md` \| `lg` | — |
|
|
239
|
+
| `flow` | `row` \| `column` \| `dense` | — |
|
|
240
|
+
| `align` | `start` \| `center` \| `end` \| `stretch` — bind it: `[align]="'center'"` (see the Stack gotcha) | — |
|
|
241
|
+
| `justifyItems` | `start` \| `center` \| `end` \| `stretch` | — |
|
|
164
242
|
|
|
165
243
|
```html
|
|
166
244
|
<div skraGrid columns="auto-fit" min="md" gap="md">
|