@pokela/components 2.13.0 → 2.15.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/Alert.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import type { ReactNode } from 'react';
2
+ /**
3
+ * Inline-ilmoitus: sävy, otsikko, teksti, toiminto ja sulkeminen.
4
+ *
5
+ * Sovelluskomponentti (päätökset 22 ja 29). Vasen viiva
6
+ * --hairline-strong, ei pintaa eikä kehystä: sama paksuus kuin kentän
7
+ * virheviivassa, ja kevein tapa erottaa ilmoitus sisällöstä.
8
+ *
9
+ * Sävyjä on kolme, ja vain virhe on värillinen (--danger). Info ja
10
+ * onnistui erottuvat ikonilla ja otsikolla. Varoitusta ei ole: ilman
11
+ * toista tilaväriä se erottuisi virheestä vain sävyllä.
12
+ *
13
+ * Ruudunlukija: virhe on role="alert", muut role="status". Rooli
14
+ * ilmoittaa sisällön, kun ilmoitus lisätään sivulle; jo latautuessa
15
+ * näkyvä ilmoitus luetaan kuten muukin teksti.
16
+ */
17
+ export type AlertTone = 'info' | 'success' | 'error';
18
+ export type AlertProps = {
19
+ tone?: AlertTone;
20
+ title: string;
21
+ /** Selittävä teksti otsikon alla. */
22
+ children?: ReactNode;
23
+ /** Toiminto tekstin alla, esim. `<a className="btn btn--text">`. */
24
+ action?: ReactNode;
25
+ /** Kun annettu, ilmoituksessa on sulkunappi. */
26
+ onDismiss?: () => void;
27
+ /** Sulkunapin nimi ruudunlukijalle. */
28
+ dismissLabel?: string;
29
+ className?: string;
30
+ };
31
+ export default function Alert({ tone, title, children, action, onDismiss, dismissLabel, className, }: AlertProps): import("react/jsx-runtime").JSX.Element;
package/Alert.js ADDED
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import Icon from './Icon';
3
+ const IKONI = { info: 'info', success: 'check', error: 'alert' };
4
+ export default function Alert({ tone = 'info', title, children, action, onDismiss, dismissLabel = 'Sulje', className, }) {
5
+ return (_jsxs("div", { className: ['alert', `alert--${tone}`, className].filter(Boolean).join(' '), role: tone === 'error' ? 'alert' : 'status', children: [_jsx(Icon, { name: IKONI[tone], className: "alert__icon" }), _jsxs("div", { className: "alert__content", children: [_jsx("p", { className: "alert__title", children: title }), children ? _jsx("div", { className: "alert__body", children: children }) : null, action ? _jsx("div", { className: "alert__actions", children: action }) : null] }), onDismiss ? (_jsx("button", { type: "button", className: "alert__close", "aria-label": dismissLabel, onClick: onDismiss, children: _jsx(Icon, { name: "close" }) })) : null] }));
6
+ }
package/Icon.d.ts CHANGED
@@ -18,14 +18,15 @@
18
18
  * Koot ovat tokeneita: --icon-s (12) tekstin seassa metan kanssa,
19
19
  * --icon-m (16) napin sisällä, --icon-l (24) omana painikkeenaan.
20
20
  *
21
- * `alert` kulkee aina virhetekstin rinnalla, ei yksin: merkitys on
22
- * tekstissä, ikoni ja --danger vain nopeuttavat sen löytämistä.
21
+ * `alert` ja `info` kulkevat aina tekstin rinnalla, eivät yksin:
22
+ * merkitys on tekstissä, ikoni (ja virheessä --danger) vain nopeuttaa
23
+ * sen löytämistä.
23
24
  *
24
25
  * Nuoli on kahta lajia eikä yhtä: `arrow-right` vie sivustolla eteen-
25
26
  * päin, `arrow-up-right` ulos. Ero on lukijalle sama kuin
26
27
  * target="_blank" ruudunlukijalle.
27
28
  */
28
- export type IconName = 'arrow-right' | 'arrow-up-right' | 'plus' | 'minus' | 'close' | 'menu' | 'alert' | 'chevron-down' | 'check';
29
+ export type IconName = 'arrow-right' | 'arrow-up-right' | 'plus' | 'minus' | 'close' | 'menu' | 'alert' | 'info' | 'chevron-down' | 'check';
29
30
  export type IconSize = 's' | 'm' | 'l';
30
31
  export type IconProps = {
31
32
  name: IconName;
package/Icon.js CHANGED
@@ -12,6 +12,9 @@ const PATHS = {
12
12
  vinot sivut eivät osu puolikkaan pikselin ruudukolle. Piste on
13
13
  lyhyt viiva, koska täyttöä ei ole. */
14
14
  alert: ['M2.5 2.5 H14.5 V14.5 H2.5 Z', 'M8.5 5 V10', 'M8.5 11.5 V12.5'],
15
+ /* Tieto. Alertin neliö ylösalaisin: piste ylhäällä, varsi alla.
16
+ Erottuu virheestä muodolla eikä vain värillä. */
17
+ info: ['M2.5 2.5 H14.5 V14.5 H2.5 Z', 'M8.5 4.5 V5.5', 'M8.5 7 V12'],
15
18
  /* Selectin nuoli ja valitun vaihtoehdon merkki. 45 asteen viivat
16
19
  puolikkaan pikselin ruudukolla, kuten nuolissa. */
17
20
  'chevron-down': ['M4.5 6.5 L8.5 10.5 L12.5 6.5'],
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @pokela/components
2
2
 
3
- React-komponentit: **Pokela Design System**, versio 2.13.0.
3
+ React-komponentit: **Pokela Design System**, versio 2.15.0.
4
4
 
5
5
  > Generoitu hakemistosta `components/` — samoista tiedostoista joita sivusto
6
6
  > ajaa. Älä muokkaa käsin: muutokset tehdään lähteeseen ja paketti
@@ -33,6 +33,8 @@ tyylit saa myös suoraan: `@pokela/components/styles.css`.
33
33
  - `Select`
34
34
  - `Tabs`
35
35
  - `Tab`
36
+ - `Alert`
37
+ - `Table`
36
38
  - `Grid`
37
39
  - `Reveal`
38
40
  - `ThemeScript`
@@ -72,6 +74,8 @@ import Link from 'next/link';
72
74
  - `styles/choice.css`
73
75
  - `styles/select.css`
74
76
  - `styles/tabs.css`
77
+ - `styles/alert.css`
78
+ - `styles/table.css`
75
79
 
76
80
  Luokat ovat globaaleja, eivät CSS-moduuleja. Se on tarkoituksellista: sama
77
81
  luokka on myös Figmassa komponentin nimenä, ja hajautettu nimi katkaisisi
package/Table.d.ts ADDED
@@ -0,0 +1,57 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * Taulukko: sarakkeet, rivit, lajittelu ja tyhjä tila.
4
+ *
5
+ * Sovelluskomponentti (päätökset 22 ja 30). Rivit erotetaan
6
+ * hiusviivalla, otsikkorivin alla on musteviiva. Ei pystyviivoja,
7
+ * raitoja eikä pintaa: sama kieli kuin ListRow'ssa. Otsikot ovat
8
+ * metatekstiä, jotta huomio menee dataan.
9
+ *
10
+ * Saavutettavuus:
11
+ * · oikea <table>, jolla on <caption> (oletuksena piilotettu) ja
12
+ * <th scope="col">
13
+ * · lajiteltava otsikko on <th>:n sisällä oleva nappi, ja <th>
14
+ * kertoo järjestyksen aria-sortilla (APG: Sortable Table)
15
+ * · kun taulukko ei mahdu, vierityslaatikosta tulee nimetty alue,
16
+ * johon pääsee sarkaimella ja jota voi vierittää nuolilla
17
+ *
18
+ * Lajittelu tehdään täällä: rivit annetaan sellaisinaan, ja taulukko
19
+ * järjestää ne valitun sarakkeen mukaan. Tila voi olla ohjattu (`sort`
20
+ * + `onSortChange`) tai oma (`defaultSort`).
21
+ */
22
+ export type TableSortDirection = 'ascending' | 'descending';
23
+ export type TableSort = {
24
+ key: string;
25
+ direction: TableSortDirection;
26
+ };
27
+ export type TableColumn<R> = {
28
+ /** Sarakkeen avain. Ilman `cell`iä solu näyttää `row[key]`. */
29
+ key: string;
30
+ header: string;
31
+ /** Luku: oikealle ja tasalevyisin numeroin. */
32
+ numeric?: boolean;
33
+ /** Toissijainen tieto --ink-mutedilla. */
34
+ muted?: boolean;
35
+ sortable?: boolean;
36
+ /** Otsikko vain ruudunlukijalle, esim. toimintosarake. */
37
+ hideHeader?: boolean;
38
+ cell?: (row: R) => ReactNode;
39
+ /** Arvo, jonka mukaan lajitellaan. Oletus `row[key]`. */
40
+ sortValue?: (row: R) => string | number;
41
+ };
42
+ export type TableProps<R> = {
43
+ /** Taulukon nimi. Ruudunlukija lukee sen aina. */
44
+ caption: string;
45
+ /** Näytä nimi myös näkyvästi taulukon yläpuolella. */
46
+ showCaption?: boolean;
47
+ columns: TableColumn<R>[];
48
+ rows: R[];
49
+ rowKey: (row: R) => string;
50
+ sort?: TableSort | null;
51
+ defaultSort?: TableSort | null;
52
+ onSortChange?: (sort: TableSort) => void;
53
+ /** Teksti, kun rivejä ei ole. */
54
+ empty?: ReactNode;
55
+ className?: string;
56
+ };
57
+ export default function Table<R>({ caption, showCaption, columns, rows, rowKey, sort, defaultSort, onSortChange, empty, className, }: TableProps<R>): import("react/jsx-runtime").JSX.Element;
package/Table.js ADDED
@@ -0,0 +1,62 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useMemo, useRef, useState } from 'react';
4
+ import Icon from './Icon';
5
+ const vertailija = new Intl.Collator('fi', { numeric: true, sensitivity: 'base' });
6
+ function arvo(rivi, sarake) {
7
+ if (sarake.sortValue)
8
+ return sarake.sortValue(rivi);
9
+ const v = rivi[sarake.key];
10
+ return typeof v === 'number' ? v : String(v ?? '');
11
+ }
12
+ export default function Table({ caption, showCaption = false, columns, rows, rowKey, sort, defaultSort = null, onSortChange, empty = 'Ei rivejä.', className, }) {
13
+ const [omaSort, setOmaSort] = useState(defaultSort);
14
+ const nykyinen = sort !== undefined ? sort : omaSort;
15
+ const jarjestetty = useMemo(() => {
16
+ const sarake = nykyinen && columns.find((c) => c.key === nykyinen.key);
17
+ if (!nykyinen || !sarake)
18
+ return rows;
19
+ const suunta = nykyinen.direction === 'ascending' ? 1 : -1;
20
+ return [...rows].sort((a, b) => {
21
+ const x = arvo(a, sarake);
22
+ const y = arvo(b, sarake);
23
+ const ero = typeof x === 'number' && typeof y === 'number' ? x - y : vertailija.compare(String(x), String(y));
24
+ return ero * suunta;
25
+ });
26
+ }, [rows, columns, nykyinen]);
27
+ const lajittele = (key) => {
28
+ const seuraava = {
29
+ key,
30
+ direction: nykyinen?.key === key && nykyinen.direction === 'ascending' ? 'descending' : 'ascending',
31
+ };
32
+ if (sort === undefined)
33
+ setOmaSort(seuraava);
34
+ onSortChange?.(seuraava);
35
+ };
36
+ /* Vierityslaatikko on alue ja sarkainpysäkki vain, kun se oikeasti
37
+ vierittyy: muuten se olisi turha pysäkki jokaisella näppäimistön
38
+ käyttäjällä. `data-scrolled` näyttää kiinnitetyn sarakkeen viivan
39
+ vasta, kun sen alle on vieritetty jotain. */
40
+ const laatikko = useRef(null);
41
+ const [vierittyy, setVierittyy] = useState(false);
42
+ useEffect(() => {
43
+ const el = laatikko.current;
44
+ if (!el)
45
+ return;
46
+ const mittaa = () => setVierittyy(el.scrollWidth > el.clientWidth + 1);
47
+ const vieritys = () => el.toggleAttribute('data-scrolled', el.scrollLeft > 0);
48
+ mittaa();
49
+ const ro = new ResizeObserver(mittaa);
50
+ ro.observe(el);
51
+ el.addEventListener('scroll', vieritys, { passive: true });
52
+ return () => {
53
+ ro.disconnect();
54
+ el.removeEventListener('scroll', vieritys);
55
+ };
56
+ }, []);
57
+ const captionId = useId();
58
+ return (_jsx("div", { ref: laatikko, className: ['table', className].filter(Boolean).join(' '), role: vierittyy ? 'region' : undefined, "aria-labelledby": vierittyy ? captionId : undefined, tabIndex: vierittyy ? 0 : undefined, children: _jsxs("table", { className: "table__table", children: [_jsx("caption", { id: captionId, className: showCaption ? 'table__caption' : 'visually-hidden', children: caption }), _jsx("thead", { children: _jsx("tr", { children: columns.map((c) => {
59
+ const suunta = nykyinen?.key === c.key ? nykyinen.direction : undefined;
60
+ return (_jsx("th", { scope: "col", className: c.numeric ? 'table__num' : undefined, "aria-sort": c.sortable ? (suunta ?? 'none') : undefined, children: c.hideHeader ? (_jsx("span", { className: "visually-hidden", children: c.header })) : c.sortable ? (_jsxs("button", { type: "button", className: "table__sort", "data-direction": suunta, onClick: () => lajittele(c.key), children: [c.header, _jsx(Icon, { name: "chevron-down", size: "s", className: "table__sort-icon" })] })) : (c.header) }, c.key));
61
+ }) }) }), _jsx("tbody", { children: jarjestetty.length === 0 ? (_jsx("tr", { className: "table__empty", children: _jsx("td", { colSpan: columns.length, children: empty }) })) : (jarjestetty.map((rivi) => (_jsx("tr", { children: columns.map((c) => (_jsx("td", { className: [c.numeric && 'table__num', c.muted && 'table__muted'].filter(Boolean).join(' ') || undefined, children: c.cell ? c.cell(rivi) : String(rivi[c.key] ?? '') }, c.key))) }, rowKey(rivi))))) })] }) }));
62
+ }
@@ -18,6 +18,8 @@ Use these instead of building an equivalent.
18
18
  - `Select`
19
19
  - `Tabs`
20
20
  - `Tab`
21
+ - `Alert`
22
+ - `Table`
21
23
  - `Grid`, `Col`
22
24
  - `Reveal`
23
25
  - `ThemeScript`, `THEME_STORAGE_KEY`
@@ -146,6 +148,42 @@ for a step-by-step flow.
146
148
  </Tabs>
147
149
  ```
148
150
 
151
+ `Alert` is an inline message inside the page, next to what it is about: a
152
+ form error summary, a saved confirmation, a billing problem. `tone` is `info`
153
+ (default), `success` or `error`; only `error` is coloured. Give a short
154
+ `title` and the explanation as children; pass `action` for one next step (a
155
+ `btn btn--text` link) and `onDismiss` to show a close button. Use `error` only
156
+ for something that went wrong, never for emphasis. There is no warning tone.
157
+
158
+ ```jsx
159
+ <Alert tone="error" title="Payment failed" action={<a className="btn btn--text" href="/billing">Update card</a>}>
160
+ The card was declined. Update it to keep the subscription.
161
+ </Alert>
162
+ ```
163
+
164
+ `Table` is for data people compare across rows: members, invoices, logs.
165
+ Never use it for layout or for a list of links (use `ListRow`). Pass
166
+ `columns` and `rows`; mark numbers `numeric` (right-aligned, tabular) and
167
+ secondary text `muted`. `sortable` columns sort when the header is clicked;
168
+ set `defaultSort` for the initial order. Always give a `caption`; it is read
169
+ by screen readers even when hidden. Status in a cell is an icon and text in
170
+ `table__status` (`table__status--error` for failures), never colour alone; a
171
+ row action is a `table__action` link whose name includes the row.
172
+
173
+ ```jsx
174
+ <Table
175
+ caption="Team members"
176
+ columns={[
177
+ { key: 'name', header: 'Name', sortable: true },
178
+ { key: 'changes', header: 'Changes', numeric: true, sortable: true },
179
+ ]}
180
+ rows={members}
181
+ rowKey={(m) => m.id}
182
+ defaultSort={{ key: 'name', direction: 'ascending' }}
183
+ empty="No members yet. Invite the first one."
184
+ />
185
+ ```
186
+
149
187
  ## Patterns
150
188
 
151
189
  `PageHeader` and `Section` are patterns: they add no look of their own, only
@@ -30,7 +30,7 @@ else. Never set body text in the display face.
30
30
  <!-- generated:make-sarakkeet -->`Grid` is 12 columns: 4 on mobile, 8 from 600px, 12 from 900px.<!-- /generated --> Place
31
31
  things in columns, never in pixels. `Col` takes the span per breakpoint.
32
32
 
33
- **No icon libraries and no emoji.** Use `Icon`. <!-- generated:make-ikonit -->It draws its own 9 marks<!-- /generated --> on
33
+ **No icon libraries and no emoji.** Use `Icon`. <!-- generated:make-ikonit -->It draws its own 10 marks<!-- /generated --> on
34
34
  a 16×16 grid with the same hairline as every border.
35
35
 
36
36
  ## Buttons
@@ -9,7 +9,7 @@ inversion — `--invert-surface` with `--invert-ink` — or the quiet
9
9
  coloured button. Colour on the page comes only from photographs.
10
10
 
11
11
  The one exception is `--danger`, a state colour for errors only. Do not use it
12
- for emphasis, buttons or decoration. `TextField`, `ChoiceGroup` and `Select` already apply it, always
12
+ for emphasis, buttons or decoration. `TextField`, `ChoiceGroup`, `Select`, `Alert` and `Table` already apply it, always
13
13
  with the `alert` icon and a message, so the meaning never depends on colour.
14
14
 
15
15
  **No shadows.** There are no shadow tokens. Separation is a 1px line
package/index.d.ts CHANGED
@@ -28,6 +28,10 @@ export { default as Tabs } from './Tabs.js';
28
28
  export type { TabsProps } from './Tabs.js';
29
29
  export { default as Tab } from './Tab.js';
30
30
  export type { TabProps } from './Tab.js';
31
+ export { default as Alert } from './Alert.js';
32
+ export type { AlertTone, AlertProps } from './Alert.js';
33
+ export { default as Table } from './Table.js';
34
+ export type { TableSortDirection, TableSort, TableColumn, TableProps } from './Table.js';
31
35
  export { Grid, Col } from './Grid.js';
32
36
  export { default as Reveal } from './Reveal.js';
33
37
  export { default as ThemeScript, THEME_STORAGE_KEY } from './ThemeScript.js';
package/index.js CHANGED
@@ -15,6 +15,8 @@ export { default as ChoiceGroup } from './ChoiceGroup.js';
15
15
  export { default as Select } from './Select.js';
16
16
  export { default as Tabs } from './Tabs.js';
17
17
  export { default as Tab } from './Tab.js';
18
+ export { default as Alert } from './Alert.js';
19
+ export { default as Table } from './Table.js';
18
20
  export { Grid, Col } from './Grid.js';
19
21
  export { default as Reveal } from './Reveal.js';
20
22
  export { default as ThemeScript, THEME_STORAGE_KEY } from './ThemeScript.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pokela/components",
3
- "version": "2.13.0",
3
+ "version": "2.15.0",
4
4
  "description": "React-komponentit: Pokela Design System. Generoitu tiedostoista components/, ei käsin ylläpidetty.",
5
5
  "license": "MIT",
6
6
  "author": "Veli-Matti Pokela",
@@ -27,7 +27,7 @@
27
27
  "LICENSE"
28
28
  ],
29
29
  "dependencies": {
30
- "@pokela/tokens": "^2.13.0"
30
+ "@pokela/tokens": "^2.15.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=18"
@@ -0,0 +1,74 @@
1
+ /* Inline-ilmoitus (Alert). Sovelluskomponentti, sivusto ei käytä tätä.
2
+ ==================================================================
3
+ Vasen viiva --hairline-strong, ei pintaa eikä kehystä (päätös 29).
4
+ Ikoni, otsikko (paino 500) ja teksti (--ink-muted) ovat samassa
5
+ koossa kuin kentän virheteksti. Virheessä viiva, ikoni ja otsikko
6
+ ovat --danger; teksti pysyy --ink-mutedina, jotta pitkäkin selitys
7
+ on luettava. */
8
+
9
+ .alert {
10
+ display: grid;
11
+ grid-template-columns: auto 1fr auto;
12
+ column-gap: var(--space-12);
13
+ align-items: start;
14
+ padding: var(--space-4) 0 var(--space-4) var(--space-16);
15
+ border-left: var(--hairline-strong) solid var(--ink);
16
+ color: var(--ink);
17
+ }
18
+
19
+ /* Ikoni ensimmäisen rivin keskelle, kuten .field__errorissa. */
20
+ .alert__icon { margin-top: calc((var(--lh-body-s) * var(--text-body-s) - var(--icon-m)) / 2); }
21
+
22
+ .alert__content { min-width: 0; }
23
+ .alert__title,
24
+ .alert__body {
25
+ margin: 0;
26
+ font-size: var(--text-body-s);
27
+ line-height: var(--lh-body-s);
28
+ }
29
+ .alert__title { font-weight: var(--weight-500); }
30
+ .alert__body { color: var(--ink-muted); }
31
+ .alert__body > * { margin: 0; }
32
+ .alert__actions {
33
+ display: flex;
34
+ flex-wrap: wrap;
35
+ gap: var(--space-12) var(--space-24);
36
+ margin-top: var(--space-12);
37
+ }
38
+
39
+ .alert--error { border-left-color: var(--danger); }
40
+ .alert--error .alert__icon,
41
+ .alert--error .alert__title { color: var(--danger); }
42
+
43
+ /* Sulkunappi: näkyvä 32 px, osuma-alue --tap-min kuten .btn--s:ssä
44
+ (päätös 26). Ensimmäisen rivin keskelle, ja oikea reuna vedetään
45
+ ikonin reunaan, jotta X on linjassa tekstipalstan oikean reunan
46
+ kanssa. */
47
+ .alert__close {
48
+ position: relative;
49
+ display: inline-flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ width: var(--space-32);
53
+ height: var(--space-32);
54
+ margin: calc((var(--lh-body-s) * var(--text-body-s) - var(--space-32)) / 2)
55
+ calc((var(--space-32) - var(--icon-m)) / -2) 0 0;
56
+ padding: 0;
57
+ background: none;
58
+ border: 0;
59
+ color: var(--ink-muted);
60
+ cursor: pointer;
61
+ }
62
+ .alert__close::after {
63
+ content: '';
64
+ position: absolute;
65
+ inset: calc((var(--space-32) - var(--tap-min)) / 2);
66
+ }
67
+ .alert__close:hover { color: var(--ink); }
68
+ .alert__close:focus-visible {
69
+ outline: var(--focus-width) solid var(--ink);
70
+ outline-offset: calc(var(--focus-width) * -1);
71
+ }
72
+ @media (prefers-reduced-motion: no-preference) {
73
+ .alert__close { transition: color var(--dur-fast) var(--ease-standard); }
74
+ }
@@ -0,0 +1,184 @@
1
+ /* Taulukko (Table). Sovelluskomponentti, sivusto ei käytä tätä.
2
+ ==================================================================
3
+ Rivit hiusviivalla, otsikkorivin alla musteviiva (päätös 30). Ei
4
+ pystyviivoja, raitoja eikä pintaa. Otsikot ovat metatekstiä, data
5
+ Body/S:ää. Rivi on vähintään --tap-min, jotta solun linkkiin ja
6
+ nappiin osuu sormella.
7
+
8
+ Laatikolla on 8 px sivusisennys samasta syystä kuin välilehtirivillä:
9
+ se vierittyy, ja vierittyvä laatikko leikkaisi fokusrenkaan reunoilta.
10
+ Sisennys kumotaan negatiivisella marginaalilla, joten taulukko alkaa
11
+ palstan reunasta. Laatikon oma fokusrengas on sivuston oletus, ulkona:
12
+ sisäpuolinen jäisi kiinnitetyn sarakkeen alle. */
13
+
14
+ .table {
15
+ margin-inline: calc(var(--space-8) * -1);
16
+ padding-inline: var(--space-8);
17
+ overflow-x: auto;
18
+ }
19
+
20
+ .table__table {
21
+ width: 100%;
22
+ border-collapse: separate;
23
+ border-spacing: 0;
24
+ font-size: var(--text-body-s);
25
+ line-height: var(--lh-body-s);
26
+ color: var(--ink);
27
+ }
28
+
29
+ .table__caption {
30
+ padding-bottom: var(--space-12);
31
+ font-weight: var(--weight-500);
32
+ text-align: left;
33
+ }
34
+
35
+ .table th,
36
+ .table td {
37
+ height: var(--tap-min);
38
+ padding: 0 var(--space-16) 0 0;
39
+ text-align: left;
40
+ vertical-align: middle;
41
+ white-space: nowrap;
42
+ }
43
+ .table th:last-child,
44
+ .table td:last-child { padding-right: 0; }
45
+
46
+ .table th {
47
+ font-family: var(--font-ui);
48
+ font-size: var(--text-meta);
49
+ line-height: var(--lh-meta);
50
+ font-weight: var(--weight-600);
51
+ letter-spacing: var(--tr-meta);
52
+ text-transform: uppercase;
53
+ color: var(--ink-muted);
54
+ border-bottom: var(--hairline) solid var(--ink);
55
+ }
56
+ .table th[aria-sort='ascending'],
57
+ .table th[aria-sort='descending'] { color: var(--ink); }
58
+
59
+ .table td { border-bottom: var(--hairline) solid var(--line); }
60
+
61
+ .table .table__num {
62
+ text-align: right;
63
+ font-variant-numeric: tabular-nums;
64
+ }
65
+ .table__muted { color: var(--ink-muted); }
66
+
67
+ /* Lajittelunappi täyttää otsikkosolun korkeuden, ja sen 8 px sivu-
68
+ sisennys kumotaan, jotta teksti pysyy sarakkeen linjassa ja
69
+ fokusrengas mahtuu napin sisään. Nuoli on näkymätön, kunnes sarake
70
+ on lajiteltu, tai näkyy haaleana hoverissa ja fokuksessa: se kertoo,
71
+ että saraketta voi lajitella. Nouseva järjestys osoittaa ylös. */
72
+ .table__sort {
73
+ display: inline-flex;
74
+ align-items: center;
75
+ gap: var(--space-4);
76
+ /* Hiusviiva on rivin alareunassa, joten nappi on sen verran matalampi. */
77
+ min-height: calc(var(--tap-min) - var(--hairline));
78
+ margin: 0 calc(var(--space-8) * -1);
79
+ padding: 0 var(--space-8);
80
+ background: none;
81
+ border: 0;
82
+ font: inherit;
83
+ letter-spacing: inherit;
84
+ text-transform: inherit;
85
+ color: inherit;
86
+ cursor: pointer;
87
+ }
88
+ .table__sort-icon { opacity: 0; }
89
+ .table__sort:hover,
90
+ .table__sort:focus-visible { color: var(--ink); }
91
+ .table__sort:hover .table__sort-icon,
92
+ .table__sort:focus-visible .table__sort-icon { opacity: 1; color: var(--ink-faint); }
93
+ .table__sort[data-direction] .table__sort-icon { opacity: 1; color: var(--ink); }
94
+ .table__sort[data-direction='ascending'] .table__sort-icon { transform: rotate(180deg); }
95
+ .table__sort:focus-visible {
96
+ outline: var(--focus-width) solid var(--ink);
97
+ outline-offset: calc(var(--focus-width) * -1);
98
+ }
99
+ @media (prefers-reduced-motion: no-preference) {
100
+ .table__sort { transition: color var(--dur-fast) var(--ease-standard); }
101
+ .table__sort-icon { transition: opacity var(--dur-fast) var(--ease-standard); }
102
+ }
103
+
104
+ /* Rivi korostuu vain osoittimen alla, ei kosketuksella: napautus ei
105
+ saa jättää riviä korostetuksi. */
106
+ @media (hover: hover) {
107
+ .table tbody tr:not(.table__empty):hover td { background: var(--paper-alt); }
108
+ }
109
+
110
+ /* Ensimmäinen sarake pysyy paikallaan, kun taulukkoa vieritetään.
111
+ Varjo täyttää laatikon 8 px sisennyksen, jotta alle vierittyvä teksti
112
+ ei näy sen läpi. Viiva tulee vasta, kun jotain on vierittynyt alle. */
113
+ .table th:first-child,
114
+ .table td:first-child {
115
+ position: sticky;
116
+ left: 0;
117
+ z-index: 1;
118
+ background: var(--paper);
119
+ box-shadow: calc(var(--space-8) * -1) 0 0 var(--paper);
120
+ }
121
+ .table[data-scrolled] th:first-child,
122
+ .table[data-scrolled] td:first-child {
123
+ box-shadow:
124
+ calc(var(--space-8) * -1) 0 0 var(--paper),
125
+ inset calc(var(--hairline) * -1) 0 0 var(--line);
126
+ }
127
+ @media (hover: hover) {
128
+ .table tbody tr:not(.table__empty):hover td:first-child {
129
+ box-shadow: calc(var(--space-8) * -1) 0 0 var(--paper);
130
+ }
131
+ .table[data-scrolled] tbody tr:not(.table__empty):hover td:first-child {
132
+ box-shadow:
133
+ calc(var(--space-8) * -1) 0 0 var(--paper),
134
+ inset calc(var(--hairline) * -1) 0 0 var(--line);
135
+ }
136
+ }
137
+
138
+ .table__empty td {
139
+ position: static;
140
+ height: auto;
141
+ padding: var(--space-32) 0;
142
+ color: var(--ink-muted);
143
+ white-space: normal;
144
+ }
145
+
146
+ /* Toiminto solussa, esim. Muokkaa. Alleviivaus tekstin omalla viivalla
147
+ eikä .btn--textin alareunalla, jotta teksti pysyy rivin muun tekstin
148
+ linjassa. Osuma-alue on rivin korkuinen. */
149
+ .table__action {
150
+ position: relative;
151
+ color: var(--ink);
152
+ text-decoration: underline;
153
+ text-decoration-thickness: var(--hairline);
154
+ text-underline-offset: var(--space-4);
155
+ }
156
+ .table__action:hover {
157
+ text-decoration: underline;
158
+ text-decoration-thickness: var(--hairline-strong);
159
+ }
160
+ .table__action::after {
161
+ content: '';
162
+ position: absolute;
163
+ top: 50%;
164
+ left: 0;
165
+ right: 0;
166
+ height: var(--tap-min);
167
+ transform: translateY(-50%);
168
+ }
169
+
170
+ /* Tila solussa: ikoni ja teksti, ei koskaan pelkkä väri. Virhe on
171
+ --danger kuten kentässä ja ilmoituksessa. */
172
+ /* Lohko eikä inline-flex: ikonilla ei ole perusviivaa, joten
173
+ inline-flex nostaisi tekstin rivin muun tekstin yläpuolelle. */
174
+ .table__status {
175
+ display: flex;
176
+ align-items: center;
177
+ gap: var(--space-8);
178
+ }
179
+ .table__status--error { color: var(--danger); }
180
+
181
+ @media (forced-colors: active) {
182
+ .table th:first-child,
183
+ .table td:first-child { box-shadow: none; }
184
+ }
package/styles.css CHANGED
@@ -12,3 +12,5 @@
12
12
  @import './styles/choice.css';
13
13
  @import './styles/select.css';
14
14
  @import './styles/tabs.css';
15
+ @import './styles/alert.css';
16
+ @import './styles/table.css';