@pokela/components 2.12.0 → 2.14.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 +6 -1
- package/Tab.d.ts +22 -0
- package/Tab.js +6 -0
- package/Tabs.d.ts +35 -0
- package/Tabs.js +59 -0
- package/guidelines/components.md +32 -0
- package/guidelines/styles.md +1 -1
- package/guidelines/tokens.md +1 -1
- package/index.d.ts +6 -0
- package/index.js +3 -0
- package/package.json +2 -2
- package/styles/alert.css +74 -0
- package/styles/tabs.css +83 -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.14.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
|
|
@@ -31,6 +31,9 @@ tyylit saa myös suoraan: `@pokela/components/styles.css`.
|
|
|
31
31
|
- `Switch`
|
|
32
32
|
- `ChoiceGroup`
|
|
33
33
|
- `Select`
|
|
34
|
+
- `Tabs`
|
|
35
|
+
- `Tab`
|
|
36
|
+
- `Alert`
|
|
34
37
|
- `Grid`
|
|
35
38
|
- `Reveal`
|
|
36
39
|
- `ThemeScript`
|
|
@@ -69,6 +72,8 @@ import Link from 'next/link';
|
|
|
69
72
|
- `styles/text-field.css`
|
|
70
73
|
- `styles/choice.css`
|
|
71
74
|
- `styles/select.css`
|
|
75
|
+
- `styles/tabs.css`
|
|
76
|
+
- `styles/alert.css`
|
|
72
77
|
|
|
73
78
|
Luokat ovat globaaleja, eivät CSS-moduuleja. Se on tarkoituksellista: sama
|
|
74
79
|
luokka on myös Figmassa komponentin nimenä, ja hajautettu nimi katkaisisi
|
package/Tab.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Yksi välilehti ja sen paneeli. Käytetään vain `Tabs`in lapsena.
|
|
4
|
+
*
|
|
5
|
+
* Tab ei piirrä itse mitään: `Tabs` lukee sen propsit ja rakentaa
|
|
6
|
+
* niistä välilehtirivin ja paneelit, jotta ARIA-kytkennät (id:t,
|
|
7
|
+
* aria-controls, aria-labelledby) syntyvät yhdessä paikassa. Erillinen
|
|
8
|
+
* komponentti on silti tarpeen, koska kirjoitettuna se on luettavampi
|
|
9
|
+
* kuin taulukko, ja Figman Tabs / Tab -instanssi kääntyy tähän.
|
|
10
|
+
*/
|
|
11
|
+
export type TabProps = {
|
|
12
|
+
/** Tunniste, jolla `Tabs` tietää valitun välilehden. */
|
|
13
|
+
value: string;
|
|
14
|
+
/** Välilehden teksti. */
|
|
15
|
+
label: string;
|
|
16
|
+
/** Lukumäärä tekstin perässä, esim. avoimet tehtävät. */
|
|
17
|
+
count?: number;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
/** Paneelin sisältö. */
|
|
20
|
+
children?: ReactNode;
|
|
21
|
+
};
|
|
22
|
+
export default function Tab(props: TabProps): import("react/jsx-runtime").JSX.Element;
|
package/Tab.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export default function Tab(props) {
|
|
3
|
+
/* Piirretään vain Tabsin kautta. Yksinään paneelin sisältö näkyy
|
|
4
|
+
sellaisenaan, ettei mitään katoa hiljaa. */
|
|
5
|
+
return _jsx(_Fragment, { children: props.children });
|
|
6
|
+
}
|
package/Tabs.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Välilehdet: rivi välilehtiä ja valitun paneeli.
|
|
4
|
+
*
|
|
5
|
+
* Sovelluskomponentti (päätökset 22 ja 28). Ulkoasu on navin
|
|
6
|
+
* aktiivisen linkin kieli: valittu välilehti saa musteviivan
|
|
7
|
+
* (--hairline-strong) rivin hiusviivan päälle, muut ovat haaleampia.
|
|
8
|
+
*
|
|
9
|
+
* Rakenne WAI-ARIA APG:n Tabs-mallin mukaan, automaattinen aktivointi:
|
|
10
|
+
*
|
|
11
|
+
* rivi role="tablist", aria-label
|
|
12
|
+
* välilehti role="tab", aria-selected, aria-controls. Vain valittu
|
|
13
|
+
* on sarkainjärjestyksessä (roving tabindex).
|
|
14
|
+
* paneeli role="tabpanel", aria-labelledby, tabindex=0, jotta
|
|
15
|
+
* sarkain vie paneeliin silloinkin, kun siinä ei ole
|
|
16
|
+
* fokusoitavaa.
|
|
17
|
+
*
|
|
18
|
+
* Näppäimet: vasen ja oikea nuoli siirtävät ja valitsevat (rivin
|
|
19
|
+
* päästä toiseen), Home ja End vievät päihin. Pois käytöstä olevat
|
|
20
|
+
* ohitetaan. Paneelit vaihtuvat heti, joten valinta seuraa fokusta.
|
|
21
|
+
*/
|
|
22
|
+
export type TabsProps = {
|
|
23
|
+
/** Rivin nimi ruudunlukijalle, esim. "Asetukset". Ei näy. */
|
|
24
|
+
label: string;
|
|
25
|
+
/** Ohjattu valinta. Käytä `onChange`:n kanssa. */
|
|
26
|
+
value?: string;
|
|
27
|
+
/** Alkuvalinta, kun valintaa ei ohjata. Oletus: ensimmäinen käytössä oleva. */
|
|
28
|
+
defaultValue?: string;
|
|
29
|
+
onChange?: (value: string) => void;
|
|
30
|
+
/** `Tab`-elementit. */
|
|
31
|
+
children: ReactNode;
|
|
32
|
+
id?: string;
|
|
33
|
+
className?: string;
|
|
34
|
+
};
|
|
35
|
+
export default function Tabs({ label, value: ohjattu, defaultValue, onChange, children, id: annettuId, className }: TabsProps): import("react/jsx-runtime").JSX.Element;
|
package/Tabs.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Children, isValidElement, useId, useRef, useState } from 'react';
|
|
4
|
+
import Tab from './Tab';
|
|
5
|
+
export default function Tabs({ label, value: ohjattu, defaultValue, onChange, children, id: annettuId, className }) {
|
|
6
|
+
const omaId = useId();
|
|
7
|
+
const id = annettuId ?? omaId;
|
|
8
|
+
const tabId = (i) => `${id}-tab-${i}`;
|
|
9
|
+
const paneeliId = (i) => `${id}-paneeli-${i}`;
|
|
10
|
+
const tabit = Children.toArray(children).filter((c) => isValidElement(c) && c.type === Tab);
|
|
11
|
+
const kaytossa = (i) => i >= 0 && i < tabit.length && !tabit[i].props.disabled;
|
|
12
|
+
const [oma, setOma] = useState(defaultValue);
|
|
13
|
+
const pyydetty = ohjattu !== undefined ? ohjattu : oma;
|
|
14
|
+
let valittu = tabit.findIndex((t) => t.props.value === pyydetty);
|
|
15
|
+
if (!kaytossa(valittu))
|
|
16
|
+
valittu = tabit.findIndex((_, i) => kaytossa(i));
|
|
17
|
+
const napit = useRef([]);
|
|
18
|
+
const valitse = (i) => {
|
|
19
|
+
if (!kaytossa(i))
|
|
20
|
+
return;
|
|
21
|
+
const uusi = tabit[i].props.value;
|
|
22
|
+
if (ohjattu === undefined)
|
|
23
|
+
setOma(uusi);
|
|
24
|
+
if (i !== valittu)
|
|
25
|
+
onChange?.(uusi);
|
|
26
|
+
};
|
|
27
|
+
/** Seuraava käytössä oleva suuntaan `askel`, rivin päästä toiseen. */
|
|
28
|
+
const seuraava = (alku, askel) => {
|
|
29
|
+
const n = tabit.length;
|
|
30
|
+
for (let k = 1; k <= n; k++) {
|
|
31
|
+
const i = (((alku + askel * k) % n) + n) % n;
|
|
32
|
+
if (kaytossa(i))
|
|
33
|
+
return i;
|
|
34
|
+
}
|
|
35
|
+
return alku;
|
|
36
|
+
};
|
|
37
|
+
const nappain = (e) => {
|
|
38
|
+
let kohde = -1;
|
|
39
|
+
if (e.key === 'ArrowRight')
|
|
40
|
+
kohde = seuraava(valittu, 1);
|
|
41
|
+
else if (e.key === 'ArrowLeft')
|
|
42
|
+
kohde = seuraava(valittu, -1);
|
|
43
|
+
else if (e.key === 'Home')
|
|
44
|
+
kohde = seuraava(-1, 1);
|
|
45
|
+
else if (e.key === 'End')
|
|
46
|
+
kohde = seuraava(tabit.length, -1);
|
|
47
|
+
else
|
|
48
|
+
return;
|
|
49
|
+
e.preventDefault();
|
|
50
|
+
valitse(kohde);
|
|
51
|
+
napit.current[kohde]?.focus();
|
|
52
|
+
};
|
|
53
|
+
return (_jsxs("div", { className: ['tabs', className].filter(Boolean).join(' '), children: [_jsx("div", { className: "tabs__list", role: "tablist", "aria-label": label, onKeyDown: nappain, children: tabit.map((t, i) => {
|
|
54
|
+
const on = i === valittu;
|
|
55
|
+
return (_jsxs("button", { ref: (el) => {
|
|
56
|
+
napit.current[i] = el;
|
|
57
|
+
}, type: "button", role: "tab", id: tabId(i), className: "tabs__tab", "aria-selected": on, "aria-controls": paneeliId(i), "aria-disabled": t.props.disabled || undefined, tabIndex: on ? 0 : -1, onClick: () => valitse(i), children: [t.props.label, t.props.count !== undefined ? _jsx("span", { className: "tabs__count", children: t.props.count }) : null] }, t.props.value));
|
|
58
|
+
}) }), tabit.map((t, i) => (_jsx("div", { role: "tabpanel", id: paneeliId(i), className: "tabs__panel", "aria-labelledby": tabId(i), tabIndex: 0, hidden: i !== valittu, children: t.props.children }, t.props.value)))] }));
|
|
59
|
+
}
|
package/guidelines/components.md
CHANGED
|
@@ -16,6 +16,9 @@ Use these instead of building an equivalent.
|
|
|
16
16
|
- `Switch`
|
|
17
17
|
- `ChoiceGroup`
|
|
18
18
|
- `Select`
|
|
19
|
+
- `Tabs`
|
|
20
|
+
- `Tab`
|
|
21
|
+
- `Alert`
|
|
19
22
|
- `Grid`, `Col`
|
|
20
23
|
- `Reveal`
|
|
21
24
|
- `ThemeScript`, `THEME_STORAGE_KEY`
|
|
@@ -128,6 +131,35 @@ work, so do not build a dropdown from a button and a menu.
|
|
|
128
131
|
/>
|
|
129
132
|
```
|
|
130
133
|
|
|
134
|
+
`Tabs` switches between views of the same thing, such as the sections of a
|
|
135
|
+
settings page. Give it a `label` for screen readers and one `Tab` per view with
|
|
136
|
+
`value`, `label` and the panel as children; `count` adds a number after the
|
|
137
|
+
label and `disabled` keeps a tab visible but unusable. It follows the WAI-ARIA
|
|
138
|
+
tabs pattern: arrow keys move and select, Home and End jump to the ends, and
|
|
139
|
+
Tab moves into the panel. Do not use it for site navigation (that is `Nav`) or
|
|
140
|
+
for a step-by-step flow.
|
|
141
|
+
|
|
142
|
+
```jsx
|
|
143
|
+
<Tabs label="Settings" defaultValue="team">
|
|
144
|
+
<Tab value="general" label="General">…</Tab>
|
|
145
|
+
<Tab value="team" label="Team" count={4}>…</Tab>
|
|
146
|
+
<Tab value="billing" label="Billing" disabled>…</Tab>
|
|
147
|
+
</Tabs>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`Alert` is an inline message inside the page, next to what it is about: a
|
|
151
|
+
form error summary, a saved confirmation, a billing problem. `tone` is `info`
|
|
152
|
+
(default), `success` or `error`; only `error` is coloured. Give a short
|
|
153
|
+
`title` and the explanation as children; pass `action` for one next step (a
|
|
154
|
+
`btn btn--text` link) and `onDismiss` to show a close button. Use `error` only
|
|
155
|
+
for something that went wrong, never for emphasis. There is no warning tone.
|
|
156
|
+
|
|
157
|
+
```jsx
|
|
158
|
+
<Alert tone="error" title="Payment failed" action={<a className="btn btn--text" href="/billing">Update card</a>}>
|
|
159
|
+
The card was declined. Update it to keep the subscription.
|
|
160
|
+
</Alert>
|
|
161
|
+
```
|
|
162
|
+
|
|
131
163
|
## Patterns
|
|
132
164
|
|
|
133
165
|
`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` and `Alert` 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
|
@@ -24,6 +24,12 @@ export { default as ChoiceGroup } from './ChoiceGroup.js';
|
|
|
24
24
|
export type { ChoiceGroupProps } from './ChoiceGroup.js';
|
|
25
25
|
export { default as Select } from './Select.js';
|
|
26
26
|
export type { SelectOption, SelectProps } from './Select.js';
|
|
27
|
+
export { default as Tabs } from './Tabs.js';
|
|
28
|
+
export type { TabsProps } from './Tabs.js';
|
|
29
|
+
export { default as Tab } from './Tab.js';
|
|
30
|
+
export type { TabProps } from './Tab.js';
|
|
31
|
+
export { default as Alert } from './Alert.js';
|
|
32
|
+
export type { AlertTone, AlertProps } from './Alert.js';
|
|
27
33
|
export { Grid, Col } from './Grid.js';
|
|
28
34
|
export { default as Reveal } from './Reveal.js';
|
|
29
35
|
export { default as ThemeScript, THEME_STORAGE_KEY } from './ThemeScript.js';
|
package/index.js
CHANGED
|
@@ -13,6 +13,9 @@ export { default as Radio } from './Radio.js';
|
|
|
13
13
|
export { default as Switch } from './Switch.js';
|
|
14
14
|
export { default as ChoiceGroup } from './ChoiceGroup.js';
|
|
15
15
|
export { default as Select } from './Select.js';
|
|
16
|
+
export { default as Tabs } from './Tabs.js';
|
|
17
|
+
export { default as Tab } from './Tab.js';
|
|
18
|
+
export { default as Alert } from './Alert.js';
|
|
16
19
|
export { Grid, Col } from './Grid.js';
|
|
17
20
|
export { default as Reveal } from './Reveal.js';
|
|
18
21
|
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.14.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.14.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/tabs.css
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* Välilehdet (Tabs). Sovelluskomponentti, sivusto ei käytä tätä.
|
|
2
|
+
==================================================================
|
|
3
|
+
Navin aktiivisen linkin kieli (päätös 28): rivi välilehtiä
|
|
4
|
+
hiusviivan päällä, valittu saa musteviivan --hairline-strong, joka
|
|
5
|
+
peittää hiusviivan. Valitsematon on --ink-muted ja tummuu hoverissa.
|
|
6
|
+
|
|
7
|
+
Välilehdellä on 8 px sivusisennys, jotta fokusrengas mahtuu sen
|
|
8
|
+
sisään: rivi vierittyy kapealla näytöllä, ja vierittyvä laatikko
|
|
9
|
+
leikkaisi ulkopuolisen renkaan. Sisennys kumotaan rivin
|
|
10
|
+
negatiivisella marginaalilla, joten teksti alkaa palstan reunasta.
|
|
11
|
+
Siksi hiusviiva on taustana eikä reunana: reuna jatkuisi 8 px yli. */
|
|
12
|
+
|
|
13
|
+
.tabs__list {
|
|
14
|
+
display: flex;
|
|
15
|
+
gap: var(--space-16);
|
|
16
|
+
margin-inline: calc(var(--space-8) * -1);
|
|
17
|
+
overflow-x: auto;
|
|
18
|
+
scrollbar-width: none;
|
|
19
|
+
background: linear-gradient(var(--line), var(--line)) no-repeat var(--space-8) 100% /
|
|
20
|
+
calc(100% - var(--space-16)) var(--hairline);
|
|
21
|
+
}
|
|
22
|
+
.tabs__list::-webkit-scrollbar { display: none; }
|
|
23
|
+
|
|
24
|
+
.tabs__tab {
|
|
25
|
+
position: relative;
|
|
26
|
+
flex: none;
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: var(--space-8);
|
|
30
|
+
min-height: var(--tap-min);
|
|
31
|
+
padding: 0 var(--space-8);
|
|
32
|
+
margin: 0;
|
|
33
|
+
background: none;
|
|
34
|
+
border: 0;
|
|
35
|
+
font-family: var(--font-ui);
|
|
36
|
+
font-size: var(--text-body-s);
|
|
37
|
+
font-weight: var(--weight-500);
|
|
38
|
+
line-height: 1;
|
|
39
|
+
color: var(--ink-muted);
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
}
|
|
43
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
44
|
+
.tabs__tab { transition: color var(--dur-fast) var(--ease-standard); }
|
|
45
|
+
}
|
|
46
|
+
.tabs__tab::after {
|
|
47
|
+
content: '';
|
|
48
|
+
position: absolute;
|
|
49
|
+
left: var(--space-8);
|
|
50
|
+
right: var(--space-8);
|
|
51
|
+
bottom: 0;
|
|
52
|
+
height: var(--hairline-strong);
|
|
53
|
+
}
|
|
54
|
+
.tabs__tab:hover { color: var(--ink); }
|
|
55
|
+
.tabs__tab[aria-selected='true'] { color: var(--ink); }
|
|
56
|
+
.tabs__tab[aria-selected='true']::after { background: var(--ink); }
|
|
57
|
+
.tabs__tab:focus-visible {
|
|
58
|
+
outline: var(--focus-width) solid var(--ink);
|
|
59
|
+
outline-offset: calc(var(--focus-width) * -1);
|
|
60
|
+
}
|
|
61
|
+
.tabs__tab[aria-disabled='true'] { color: var(--ink-faint); cursor: not-allowed; }
|
|
62
|
+
|
|
63
|
+
.tabs__count {
|
|
64
|
+
font-size: var(--text-meta-s);
|
|
65
|
+
font-weight: var(--weight-600);
|
|
66
|
+
font-variant-numeric: tabular-nums;
|
|
67
|
+
color: var(--ink-faint);
|
|
68
|
+
}
|
|
69
|
+
.tabs__tab[aria-selected='true'] .tabs__count { color: var(--ink-muted); }
|
|
70
|
+
|
|
71
|
+
.tabs__panel { padding-top: var(--space-24); }
|
|
72
|
+
.tabs__panel:focus-visible {
|
|
73
|
+
outline: var(--focus-width) solid var(--ink);
|
|
74
|
+
outline-offset: var(--focus-offset);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Korkean kontrastin tilassa taustat katoavat: valinnan viiva
|
|
78
|
+
piirretään järjestelmän värillä. */
|
|
79
|
+
@media (forced-colors: active) {
|
|
80
|
+
.tabs__tab::after { forced-color-adjust: none; }
|
|
81
|
+
.tabs__tab[aria-selected='true']::after { background: Highlight; }
|
|
82
|
+
.tabs__tab[aria-disabled='true'] { color: GrayText; }
|
|
83
|
+
}
|