@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 +31 -0
- package/Alert.js +6 -0
- package/Icon.d.ts +4 -3
- package/Icon.js +3 -0
- package/README.md +5 -1
- package/Table.d.ts +57 -0
- package/Table.js +62 -0
- package/guidelines/components.md +38 -0
- package/guidelines/styles.md +1 -1
- package/guidelines/tokens.md +1 -1
- package/index.d.ts +4 -0
- package/index.js +2 -0
- package/package.json +2 -2
- package/styles/alert.css +74 -0
- package/styles/table.css +184 -0
- package/styles.css +2 -0
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`
|
|
22
|
-
* tekstissä, ikoni ja --danger vain
|
|
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.
|
|
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
|
+
}
|
package/guidelines/components.md
CHANGED
|
@@ -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
|
package/guidelines/styles.md
CHANGED
|
@@ -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
|
|
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
|
package/guidelines/tokens.md
CHANGED
|
@@ -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 `
|
|
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.
|
|
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.
|
|
30
|
+
"@pokela/tokens": "^2.15.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": ">=18"
|
package/styles/alert.css
ADDED
|
@@ -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
|
+
}
|
package/styles/table.css
ADDED
|
@@ -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
|
+
}
|