@pokela/components 2.7.0 → 2.10.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/Checkbox.d.ts +24 -0
- package/Checkbox.js +15 -0
- package/ChoiceGroup.d.ts +23 -0
- package/ChoiceGroup.js +10 -0
- package/Icon.d.ts +4 -1
- package/Icon.js +4 -0
- package/PageHeader.d.ts +18 -0
- package/PageHeader.js +16 -0
- package/README.md +11 -1
- package/Radio.d.ts +17 -0
- package/Radio.js +11 -0
- package/Section.d.ts +24 -0
- package/Section.js +8 -0
- package/Switch.d.ts +19 -0
- package/Switch.js +11 -0
- package/TextField.d.ts +35 -0
- package/TextField.js +20 -0
- package/guidelines/components.md +58 -0
- package/guidelines/setup.md +22 -0
- package/guidelines/styles.md +1 -1
- package/guidelines/tokens.md +6 -1
- package/index.d.ts +13 -0
- package/index.js +7 -0
- package/package.json +2 -2
- package/styles/choice.css +195 -0
- package/styles/patterns.css +38 -0
- package/styles/text-field.css +91 -0
- package/styles.css +3 -0
package/Checkbox.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type InputHTMLAttributes } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Valintaruutu: ruutu, label ja ohjeteksti.
|
|
4
|
+
*
|
|
5
|
+
* Sovelluskomponentti (päätös 22), sivusto ei käytä tätä. Kieli on
|
|
6
|
+
* .btn-perheen: hiusviiva --line-strong, ei pyöristystä, valittu ruutu
|
|
7
|
+
* täytetään --inkillä. Rivi on vähintään --tap-min korkea.
|
|
8
|
+
*
|
|
9
|
+
* Oikea <input> on ruudun päällä näkymättömänä, joten näppäimistö,
|
|
10
|
+
* lomake ja ruudunlukija toimivat kuten natiivissa. `indeterminate`
|
|
11
|
+
* ei ole HTML-attribuutti, joten se asetetaan elementille.
|
|
12
|
+
*
|
|
13
|
+
* Virhettä ei ole tässä: se kuuluu ryhmälle (ChoiceGroup), jonka
|
|
14
|
+
* legend kertoo, mitä puuttuu.
|
|
15
|
+
*/
|
|
16
|
+
export type CheckboxProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'className'> & {
|
|
17
|
+
label: string;
|
|
18
|
+
/** Ohjeteksti labelin alla. */
|
|
19
|
+
hint?: string;
|
|
20
|
+
/** Osittain valittu, esim. "valitse kaikki", kun osa on valittu. */
|
|
21
|
+
indeterminate?: boolean;
|
|
22
|
+
className?: string;
|
|
23
|
+
};
|
|
24
|
+
export default function Checkbox(props: CheckboxProps): import("react/jsx-runtime").JSX.Element;
|
package/Checkbox.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useRef } from 'react';
|
|
4
|
+
export default function Checkbox(props) {
|
|
5
|
+
const { label, hint, indeterminate, className, id: annettuId, ...input } = props;
|
|
6
|
+
const omaId = useId();
|
|
7
|
+
const id = annettuId ?? omaId;
|
|
8
|
+
const ohjeId = hint ? `${id}-ohje` : undefined;
|
|
9
|
+
const ref = useRef(null);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (ref.current)
|
|
12
|
+
ref.current.indeterminate = Boolean(indeterminate);
|
|
13
|
+
}, [indeterminate]);
|
|
14
|
+
return (_jsxs("div", { className: ['choice', input.disabled && 'choice--disabled', className].filter(Boolean).join(' '), children: [_jsxs("span", { className: "choice__control", children: [_jsx("input", { ...input, ref: ref, id: id, type: "checkbox", className: "choice__input", "aria-describedby": ohjeId }), _jsx("span", { className: "choice__mark", "aria-hidden": "true", children: _jsxs("svg", { viewBox: "0 0 20 20", children: [_jsx("path", { className: "choice__check", d: "M5 10 L8.5 13.5 L15 6.5" }), _jsx("path", { className: "choice__dash", d: "M5 10 H15" })] }) })] }), _jsx("label", { className: "choice__label", htmlFor: id, children: label }), hint ? (_jsx("p", { id: ohjeId, className: "choice__hint", children: hint })) : null] }));
|
|
15
|
+
}
|
package/ChoiceGroup.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Valintojen ryhmä: otsikko (legend), valinnat, ohje ja virhe.
|
|
4
|
+
*
|
|
5
|
+
* Sovelluskomponentti (päätös 22). Ryhmä on <fieldset>, joten
|
|
6
|
+
* ruudunlukija sanoo otsikon ennen jokaista valintaa. Virhe kuuluu
|
|
7
|
+
* ryhmälle eikä yksittäiselle valinnalle: "valitse yksi" koskee
|
|
8
|
+
* kaikkia. Virhetilassa jokaisen valinnan reuna on --danger, ja viesti
|
|
9
|
+
* on ikonin kanssa kuten TextFieldissä.
|
|
10
|
+
*
|
|
11
|
+
* Yksittäinenkin pakollinen valinta ("hyväksy ehdot") tehdään ryhmänä,
|
|
12
|
+
* jotta virheelle on paikka.
|
|
13
|
+
*/
|
|
14
|
+
export type ChoiceGroupProps = {
|
|
15
|
+
legend: string;
|
|
16
|
+
/** Ohjeteksti valintojen alla. */
|
|
17
|
+
hint?: string;
|
|
18
|
+
/** Virheviesti. Kun annettu, ryhmä on virhetilassa. */
|
|
19
|
+
error?: string;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
className?: string;
|
|
22
|
+
};
|
|
23
|
+
export default function ChoiceGroup({ legend, hint, error, children, className }: ChoiceGroupProps): import("react/jsx-runtime").JSX.Element;
|
package/ChoiceGroup.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
import Icon from './Icon';
|
|
4
|
+
export default function ChoiceGroup({ legend, hint, error, children, className }) {
|
|
5
|
+
const id = useId();
|
|
6
|
+
const ohjeId = hint ? `${id}-ohje` : undefined;
|
|
7
|
+
const virheId = error ? `${id}-virhe` : undefined;
|
|
8
|
+
const kuvaus = [ohjeId, virheId].filter(Boolean).join(' ') || undefined;
|
|
9
|
+
return (_jsxs("fieldset", { className: ['choice-group', error && 'choice-group--invalid', className].filter(Boolean).join(' '), "aria-describedby": kuvaus, children: [_jsx("legend", { className: "choice-group__legend", children: legend }), children, hint ? (_jsx("p", { id: ohjeId, className: "field__hint", children: hint })) : null, error ? (_jsxs("p", { id: virheId, className: "field__error", children: [_jsx(Icon, { name: "alert" }), error] })) : null] }));
|
|
10
|
+
}
|
package/Icon.d.ts
CHANGED
|
@@ -18,11 +18,14 @@
|
|
|
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ä.
|
|
23
|
+
*
|
|
21
24
|
* Nuoli on kahta lajia eikä yhtä: `arrow-right` vie sivustolla eteen-
|
|
22
25
|
* päin, `arrow-up-right` ulos. Ero on lukijalle sama kuin
|
|
23
26
|
* target="_blank" ruudunlukijalle.
|
|
24
27
|
*/
|
|
25
|
-
export type IconName = 'arrow-right' | 'arrow-up-right' | 'plus' | 'minus' | 'close' | 'menu';
|
|
28
|
+
export type IconName = 'arrow-right' | 'arrow-up-right' | 'plus' | 'minus' | 'close' | 'menu' | 'alert';
|
|
26
29
|
export type IconSize = 's' | 'm' | 'l';
|
|
27
30
|
export type IconProps = {
|
|
28
31
|
name: IconName;
|
package/Icon.js
CHANGED
|
@@ -8,6 +8,10 @@ const PATHS = {
|
|
|
8
8
|
minus: ['M2.5 8.5 H14.5'],
|
|
9
9
|
close: ['M3.5 3.5 L12.5 12.5', 'M12.5 3.5 L3.5 12.5'],
|
|
10
10
|
menu: ['M1 5.5 H15', 'M1 10.5 H15'],
|
|
11
|
+
/* Virhe. Neliö eikä kolmio tai ympyrä: kaaria ei ole, ja kolmion
|
|
12
|
+
vinot sivut eivät osu puolikkaan pikselin ruudukolle. Piste on
|
|
13
|
+
lyhyt viiva, koska täyttöä ei ole. */
|
|
14
|
+
alert: ['M2.5 2.5 H14.5 V14.5 H2.5 Z', 'M8.5 5 V10', 'M8.5 11.5 V12.5'],
|
|
11
15
|
};
|
|
12
16
|
export default function Icon({ name, size = 'm', label, className }) {
|
|
13
17
|
return (_jsx("svg", { className: ['icon', `icon--${size}`, className].filter(Boolean).join(' '), viewBox: "0 0 16 16", role: label ? 'img' : undefined, "aria-label": label, "aria-hidden": label ? undefined : true, focusable: "false", children: PATHS[name].map((d) => (_jsx("path", { d: d }, d))) }));
|
package/PageHeader.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Sivun otsikko (pattern).
|
|
4
|
+
* ---------------------------------------------------------------
|
|
5
|
+
* Patterni ei tuo omaa ulkoasua: se asettelee olemassa olevat
|
|
6
|
+
* typografialuokat aina samalla tavalla. Otsikko (`display-xl`) ja
|
|
7
|
+
* sivutieto (`meta`) ovat samalla rivillä leveämmällä ruudulla,
|
|
8
|
+
* ingressi (`body-l`) otsikon alla samassa sarakkeessa.
|
|
9
|
+
*
|
|
10
|
+
* Ennen tätä sama rakenne kirjoitettiin jokaiselle sivulle käsin, ja
|
|
11
|
+
* Figma Make arvasi sen: ingressi päätyi sarakkeisiin 9–12, irti
|
|
12
|
+
* otsikosta (päätös 21).
|
|
13
|
+
*/
|
|
14
|
+
export default function PageHeader({ title, meta, lede, }: {
|
|
15
|
+
title: ReactNode;
|
|
16
|
+
meta?: ReactNode;
|
|
17
|
+
lede?: ReactNode;
|
|
18
|
+
}): import("react/jsx-runtime").JSX.Element;
|
package/PageHeader.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Sivun otsikko (pattern).
|
|
4
|
+
* ---------------------------------------------------------------
|
|
5
|
+
* Patterni ei tuo omaa ulkoasua: se asettelee olemassa olevat
|
|
6
|
+
* typografialuokat aina samalla tavalla. Otsikko (`display-xl`) ja
|
|
7
|
+
* sivutieto (`meta`) ovat samalla rivillä leveämmällä ruudulla,
|
|
8
|
+
* ingressi (`body-l`) otsikon alla samassa sarakkeessa.
|
|
9
|
+
*
|
|
10
|
+
* Ennen tätä sama rakenne kirjoitettiin jokaiselle sivulle käsin, ja
|
|
11
|
+
* Figma Make arvasi sen: ingressi päätyi sarakkeisiin 9–12, irti
|
|
12
|
+
* otsikosta (päätös 21).
|
|
13
|
+
*/
|
|
14
|
+
export default function PageHeader({ title, meta, lede, }) {
|
|
15
|
+
return (_jsxs("header", { className: "page page-header", children: [_jsxs("div", { className: "page-head", children: [_jsx("h1", { className: "display-xl", children: title }), meta ? _jsx("p", { className: "meta", children: meta }) : null] }), lede ? _jsx("p", { className: "body-l measure page-header__lede", children: lede }) : null] }));
|
|
16
|
+
}
|
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.10.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
|
|
@@ -23,6 +23,13 @@ tyylit saa myös suoraan: `@pokela/components/styles.css`.
|
|
|
23
23
|
- `Accordion`
|
|
24
24
|
- `Timeline`
|
|
25
25
|
- `ExplodedView`
|
|
26
|
+
- `PageHeader`
|
|
27
|
+
- `Section`
|
|
28
|
+
- `TextField`
|
|
29
|
+
- `Checkbox`
|
|
30
|
+
- `Radio`
|
|
31
|
+
- `Switch`
|
|
32
|
+
- `ChoiceGroup`
|
|
26
33
|
- `Grid`
|
|
27
34
|
- `Reveal`
|
|
28
35
|
- `ThemeScript`
|
|
@@ -57,6 +64,9 @@ import Link from 'next/link';
|
|
|
57
64
|
- `styles/accordion.css`
|
|
58
65
|
- `styles/timeline.css`
|
|
59
66
|
- `styles/exploded-view.css`
|
|
67
|
+
- `styles/patterns.css`
|
|
68
|
+
- `styles/text-field.css`
|
|
69
|
+
- `styles/choice.css`
|
|
60
70
|
|
|
61
71
|
Luokat ovat globaaleja, eivät CSS-moduuleja. Se on tarkoituksellista: sama
|
|
62
72
|
luokka on myös Figmassa komponentin nimenä, ja hajautettu nimi katkaisisi
|
package/Radio.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type InputHTMLAttributes } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Radiopainike: yksi vaihtoehto ryhmästä, josta valitaan yksi.
|
|
4
|
+
*
|
|
5
|
+
* Sovelluskomponentti (päätös 22). Ympyrä on systeemin ainoa pyöreä
|
|
6
|
+
* muoto (päätös 24): ilman sitä radio ja valintaruutu näyttäisivät
|
|
7
|
+
* samalta, eikä käyttäjä näkisi, valitaanko yksi vai monta.
|
|
8
|
+
*
|
|
9
|
+
* Käytä aina ChoiceGroupin sisällä samalla `name`-arvolla.
|
|
10
|
+
*/
|
|
11
|
+
export type RadioProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'className'> & {
|
|
12
|
+
label: string;
|
|
13
|
+
/** Ohjeteksti labelin alla. */
|
|
14
|
+
hint?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
};
|
|
17
|
+
export default function Radio(props: RadioProps): import("react/jsx-runtime").JSX.Element;
|
package/Radio.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
export default function Radio(props) {
|
|
4
|
+
const { label, hint, className, id: annettuId, ...input } = props;
|
|
5
|
+
const omaId = useId();
|
|
6
|
+
const id = annettuId ?? omaId;
|
|
7
|
+
const ohjeId = hint ? `${id}-ohje` : undefined;
|
|
8
|
+
return (_jsxs("div", { className: ['choice', 'choice--radio', input.disabled && 'choice--disabled', className]
|
|
9
|
+
.filter(Boolean)
|
|
10
|
+
.join(' '), children: [_jsxs("span", { className: "choice__control", children: [_jsx("input", { ...input, id: id, type: "radio", className: "choice__input", "aria-describedby": ohjeId }), _jsx("span", { className: "choice__mark", "aria-hidden": "true" })] }), _jsx("label", { className: "choice__label", htmlFor: id, children: label }), hint ? (_jsx("p", { id: ohjeId, className: "choice__hint", children: hint })) : null] }));
|
|
11
|
+
}
|
package/Section.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Osio (pattern).
|
|
4
|
+
* ---------------------------------------------------------------
|
|
5
|
+
* Sivun osio otsikkoineen. Kaksi asettelua, molemmat sivustolta:
|
|
6
|
+
*
|
|
7
|
+
* stacked otsikko ja sivutieto rivinä, sisältö alla
|
|
8
|
+
* (työlista, Tietoa, System)
|
|
9
|
+
* aside otsikko vasemmassa sarakkeessa, sisältö oikealla;
|
|
10
|
+
* pienellä ruudulla päällekkäin (casesivujen lohkot)
|
|
11
|
+
*
|
|
12
|
+
* Otsikko on aina `h2.meta`: se nimeää osion, sisällön oma otsikko
|
|
13
|
+
* (esim. `display-m`) kuuluu `children`-osaan.
|
|
14
|
+
*/
|
|
15
|
+
export type SectionLayout = 'stacked' | 'aside';
|
|
16
|
+
export default function Section({ title, meta, id, layout, className, children, }: {
|
|
17
|
+
title: ReactNode;
|
|
18
|
+
meta?: ReactNode;
|
|
19
|
+
/** Otsikon id; osio nimetään sillä (`aria-labelledby`). */
|
|
20
|
+
id?: string;
|
|
21
|
+
layout?: SectionLayout;
|
|
22
|
+
className?: string;
|
|
23
|
+
children: ReactNode;
|
|
24
|
+
}): import("react/jsx-runtime").JSX.Element;
|
package/Section.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export default function Section({ title, meta, id, layout = 'stacked', className, children, }) {
|
|
3
|
+
const otsikko = (_jsx("h2", { id: id, className: "meta", children: title }));
|
|
4
|
+
if (layout === 'aside') {
|
|
5
|
+
return (_jsxs("section", { className: ['page section--aside', className].filter(Boolean).join(' '), "aria-labelledby": id, children: [otsikko, children] }));
|
|
6
|
+
}
|
|
7
|
+
return (_jsxs("section", { className: ['page section', className].filter(Boolean).join(' '), "aria-labelledby": id, children: [_jsxs("div", { className: "section-head", children: [otsikko, meta ? _jsx("span", { className: "meta", children: meta }) : null] }), children] }));
|
|
8
|
+
}
|
package/Switch.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type InputHTMLAttributes } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Kytkin: asetus, joka tulee voimaan heti.
|
|
4
|
+
*
|
|
5
|
+
* Sovelluskomponentti (päätös 22). Valintaruutu tallennetaan napilla,
|
|
6
|
+
* kytkin ei. Siksi kytkin on asetuslistan rivi: label vasemmalla,
|
|
7
|
+
* kytkin oikeassa reunassa. Kisko ja nuppi ovat neliöt kuten muu
|
|
8
|
+
* systeemi; päällä kisko täyttyy --inkillä.
|
|
9
|
+
*
|
|
10
|
+
* Alla on <input type="checkbox" role="switch">, joten ruudunlukija
|
|
11
|
+
* sanoo "päällä" tai "pois".
|
|
12
|
+
*/
|
|
13
|
+
export type SwitchProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'type' | 'className' | 'role'> & {
|
|
14
|
+
label: string;
|
|
15
|
+
/** Ohjeteksti labelin alla. */
|
|
16
|
+
hint?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
};
|
|
19
|
+
export default function Switch(props: SwitchProps): import("react/jsx-runtime").JSX.Element;
|
package/Switch.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
export default function Switch(props) {
|
|
4
|
+
const { label, hint, className, id: annettuId, ...input } = props;
|
|
5
|
+
const omaId = useId();
|
|
6
|
+
const id = annettuId ?? omaId;
|
|
7
|
+
const ohjeId = hint ? `${id}-ohje` : undefined;
|
|
8
|
+
return (_jsxs("div", { className: ['choice', 'choice--switch', input.disabled && 'choice--disabled', className]
|
|
9
|
+
.filter(Boolean)
|
|
10
|
+
.join(' '), children: [_jsxs("span", { className: "choice__control", children: [_jsx("input", { ...input, id: id, type: "checkbox", role: "switch", className: "choice__input", "aria-describedby": ohjeId }), _jsx("span", { className: "choice__mark", "aria-hidden": "true" })] }), _jsx("label", { className: "choice__label", htmlFor: id, children: label }), hint ? (_jsx("p", { id: ohjeId, className: "choice__hint", children: hint })) : null] }));
|
|
11
|
+
}
|
package/TextField.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type InputHTMLAttributes } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Tekstikenttä: label, kenttä, ohjeteksti ja virhe yhtenä.
|
|
4
|
+
*
|
|
5
|
+
* Sivusto ei käytä tätä. Se on kirjastossa sovellusnäkymiä varten
|
|
6
|
+
* (päätös 22), jotta Make voi rakentaa
|
|
7
|
+
* lomakkeen systeemin omista osista eikä keksi omaansa.
|
|
8
|
+
*
|
|
9
|
+
* Kieli on .btn-perheen: hiusviiva ilman pyöristystä, hover paksuntaa
|
|
10
|
+
* alaviivan kuten tekstinapissa, focus on sivuston yhteinen rengas.
|
|
11
|
+
* Virhe on systeemin ainoa tilaväri --danger, aina ikonin ja tekstin
|
|
12
|
+
* kanssa, ettei merkitys jää värin varaan.
|
|
13
|
+
*
|
|
14
|
+
* Tilat ovat propseja, eivät luokkia: `error` ja `disabled`. Hover ja
|
|
15
|
+
* focus ovat CSS:n tiloja. `multiline` vaihtaa kentän tekstialueeksi;
|
|
16
|
+
* muuten sama komponentti, koska label, ohje ja virhe ovat samat.
|
|
17
|
+
*/
|
|
18
|
+
type Yhteiset = {
|
|
19
|
+
label: string;
|
|
20
|
+
/** Ohjeteksti kentän alla. */
|
|
21
|
+
hint?: string;
|
|
22
|
+
/** Virheviesti. Kun annettu, kenttä on virhetilassa (aria-invalid). */
|
|
23
|
+
error?: string;
|
|
24
|
+
/** Valinnaisen kentän merkintä labelin perässä, esim. "(valinnainen)". */
|
|
25
|
+
optional?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
};
|
|
28
|
+
export type TextFieldProps = Yhteiset & Omit<InputHTMLAttributes<HTMLInputElement>, 'className'> & {
|
|
29
|
+
/** Tekstialue (<textarea>) yhden rivin kentän sijaan. */
|
|
30
|
+
multiline?: boolean;
|
|
31
|
+
/** Tekstialueen rivit. */
|
|
32
|
+
rows?: number;
|
|
33
|
+
};
|
|
34
|
+
export default function TextField(props: TextFieldProps): import("react/jsx-runtime").JSX.Element;
|
|
35
|
+
export {};
|
package/TextField.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from 'react';
|
|
3
|
+
import Icon from './Icon';
|
|
4
|
+
export default function TextField(props) {
|
|
5
|
+
const { label, hint, error, optional, className, multiline, rows, id: annettuId, ...kentta } = props;
|
|
6
|
+
const omaId = useId();
|
|
7
|
+
const id = annettuId ?? omaId;
|
|
8
|
+
const ohjeId = hint ? `${id}-ohje` : undefined;
|
|
9
|
+
const virheId = error ? `${id}-virhe` : undefined;
|
|
10
|
+
const kuvaus = [ohjeId, virheId].filter(Boolean).join(' ') || undefined;
|
|
11
|
+
const yhteiset = {
|
|
12
|
+
id,
|
|
13
|
+
className: 'field__control',
|
|
14
|
+
'aria-invalid': error ? true : undefined,
|
|
15
|
+
'aria-describedby': kuvaus,
|
|
16
|
+
};
|
|
17
|
+
return (_jsxs("div", { className: ['field', error && 'field--invalid', kentta.disabled && 'field--disabled', className]
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.join(' '), children: [_jsxs("label", { className: "field__label", htmlFor: id, children: [label, optional ? _jsxs("span", { className: "field__optional", children: [" ", optional] }) : null] }), multiline ? (_jsx("textarea", { ...kentta, rows: rows, ...yhteiset })) : (_jsx("input", { ...kentta, ...yhteiset })), hint ? (_jsx("p", { id: ohjeId, className: "field__hint", children: hint })) : null, error ? (_jsxs("p", { id: virheId, className: "field__error", children: [_jsx(Icon, { name: "alert" }), error] })) : null] }));
|
|
20
|
+
}
|
package/guidelines/components.md
CHANGED
|
@@ -8,6 +8,13 @@ Use these instead of building an equivalent.
|
|
|
8
8
|
- `Accordion`, `useAccordionGroup`
|
|
9
9
|
- `Timeline`, `TimelineItem`
|
|
10
10
|
- `ExplodedView`
|
|
11
|
+
- `PageHeader`
|
|
12
|
+
- `Section`
|
|
13
|
+
- `TextField`
|
|
14
|
+
- `Checkbox`
|
|
15
|
+
- `Radio`
|
|
16
|
+
- `Switch`
|
|
17
|
+
- `ChoiceGroup`
|
|
11
18
|
- `Grid`, `Col`
|
|
12
19
|
- `Reveal`
|
|
13
20
|
- `ThemeScript`, `THEME_STORAGE_KEY`
|
|
@@ -69,4 +76,55 @@ reader, described by `label`.
|
|
|
69
76
|
/>
|
|
70
77
|
```
|
|
71
78
|
|
|
79
|
+
`TextField` is the form field: label, input, hint and error in one. Use it
|
|
80
|
+
for every text input and text area (`multiline`) instead of a bare `<input>`.
|
|
81
|
+
Pass `error` with a message to show the error state; never colour a field
|
|
82
|
+
yourself. Mark an optional field with `optional="(optional)"`, not an asterisk
|
|
83
|
+
on the required ones. Put form buttons after the fields with the `.btn`
|
|
84
|
+
classes.
|
|
85
|
+
|
|
86
|
+
```jsx
|
|
87
|
+
<TextField label="Email" type="email" error="The address is missing a domain." />
|
|
88
|
+
<TextField label="Message" optional="(optional)" multiline hint="Up to 500 characters." />
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`Checkbox`, `Radio` and `Switch` are the choices; put them inside a
|
|
92
|
+
`ChoiceGroup`, which gives the group its title (`legend`), `hint` and `error`.
|
|
93
|
+
Use `Checkbox` for options saved with a button, `Radio` (same `name`) when
|
|
94
|
+
exactly one option is picked, and `Switch` for a setting that takes effect at
|
|
95
|
+
once, never in a form with a save button. An error belongs to the group, not
|
|
96
|
+
to a single choice; a single required choice ("accept the terms") is still a
|
|
97
|
+
`ChoiceGroup`.
|
|
98
|
+
|
|
99
|
+
```jsx
|
|
100
|
+
<ChoiceGroup legend="Visibility" error="Pick one.">
|
|
101
|
+
<Radio name="visibility" value="public" label="Public" hint="Anyone with the link." />
|
|
102
|
+
<Radio name="visibility" value="team" label="Team only" />
|
|
103
|
+
</ChoiceGroup>
|
|
104
|
+
<ChoiceGroup legend="Notifications">
|
|
105
|
+
<Switch label="Email notifications" defaultChecked />
|
|
106
|
+
</ChoiceGroup>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Patterns
|
|
110
|
+
|
|
111
|
+
`PageHeader` and `Section` are patterns: they add no look of their own, only
|
|
112
|
+
the layout every page uses. Build pages from them instead of arranging
|
|
113
|
+
headings and grids yourself.
|
|
114
|
+
|
|
115
|
+
`PageHeader` is the page's title with an optional `meta` and `lede`. The lede
|
|
116
|
+
always sits under the title in the same column, never beside it.
|
|
117
|
+
|
|
118
|
+
`Section` names a part of the page with a small `title` (and optional
|
|
119
|
+
`meta`). `layout="stacked"` puts the title row above the content;
|
|
120
|
+
`layout="aside"` puts the title in a narrow left column and the content
|
|
121
|
+
beside it. Use one layout per page.
|
|
122
|
+
|
|
123
|
+
```jsx
|
|
124
|
+
<PageHeader title="From idea to launch" meta="9 weeks" lede="Each phase ends in a shared decision." />
|
|
125
|
+
<Section title="Road to launch" layout="aside">
|
|
126
|
+
<Timeline>…</Timeline>
|
|
127
|
+
</Section>
|
|
128
|
+
```
|
|
129
|
+
|
|
72
130
|
`Reveal` fades its children in on scroll. Wrap sections, not single elements.
|
package/guidelines/setup.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Setup
|
|
2
2
|
|
|
3
|
+
## Before you build
|
|
4
|
+
|
|
5
|
+
First decide which of three jobs this is. If the request does not make it
|
|
6
|
+
clear, ask this one question and wait: **"Are you building a view, a new
|
|
7
|
+
component, or a change to an existing component?"**
|
|
8
|
+
|
|
9
|
+
1. **A view or prototype**, new or existing. Ask what the page is, what is
|
|
10
|
+
on it and what can be clicked. Build it only from this package: pages
|
|
11
|
+
start with `PageHeader`, content goes in `Section`s. If the view needs a
|
|
12
|
+
part the package does not have, do not invent it inside the view: build
|
|
13
|
+
it separately as in 2 and label it "Proposed component".
|
|
14
|
+
2. **A new component.** Ask its name, what it does, its states, and which
|
|
15
|
+
existing component is closest. Build it alone on an empty page, every
|
|
16
|
+
state visible side by side, using only tokens. Name its props like the
|
|
17
|
+
package's components (`variant`, `size`, `title`, `meta`), so it can
|
|
18
|
+
become a pull request and a story as is.
|
|
19
|
+
3. **A change to an existing component.** Package components cannot be
|
|
20
|
+
edited here. Show the change as a new state next to the current one and
|
|
21
|
+
label it "Proposed change".
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
3
25
|
Install **both** packages as direct dependencies, at the same, latest
|
|
4
26
|
version. `@pokela/components` uses the tokens of the same release.
|
|
5
27
|
|
package/guidelines/styles.md
CHANGED
|
@@ -30,5 +30,5 @@ 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 7 marks<!-- /generated --> on
|
|
34
34
|
a 16×16 grid with the same hairline as every border.
|
package/guidelines/tokens.md
CHANGED
|
@@ -8,10 +8,15 @@ inversion — `--invert-surface` with `--invert-ink` — or the quiet
|
|
|
8
8
|
`--badge-surface`. Do not introduce a brand blue, a primary green, or any
|
|
9
9
|
coloured button. Colour on the page comes only from photographs.
|
|
10
10
|
|
|
11
|
+
The one exception is `--danger`, a state colour for errors only. Do not use it
|
|
12
|
+
for emphasis, buttons or decoration. `TextField` and `ChoiceGroup` already apply it, always
|
|
13
|
+
with the `alert` icon and a message, so the meaning never depends on colour.
|
|
14
|
+
|
|
11
15
|
**No shadows.** There are no shadow tokens. Separation is a 1px line
|
|
12
16
|
(`--hairline`), never elevation.
|
|
13
17
|
|
|
14
|
-
**No rounded corners.** `--radius` is `0`. Everything is square.
|
|
18
|
+
**No rounded corners.** `--radius` is `0`. Everything is square. The one
|
|
19
|
+
exception is `Radio`, which is a circle so it never reads as a checkbox.
|
|
15
20
|
|
|
16
21
|
<!-- generated:make-valit -->**No spacing outside the scale.** 16 steps, each named by its value:<!-- /generated -->
|
|
17
22
|
`--space-24` is 24px. If a value is missing, use the nearest step rather than
|
package/index.d.ts
CHANGED
|
@@ -9,6 +9,19 @@ export { default as Timeline, TimelineItem } from './Timeline.js';
|
|
|
9
9
|
export type { TimelineVariant } from './Timeline.js';
|
|
10
10
|
export { default as ExplodedView } from './ExplodedView.js';
|
|
11
11
|
export type { ExplodedTila, ExplodedLayer } from './ExplodedView.js';
|
|
12
|
+
export { default as PageHeader } from './PageHeader.js';
|
|
13
|
+
export { default as Section } from './Section.js';
|
|
14
|
+
export type { SectionLayout } from './Section.js';
|
|
15
|
+
export { default as TextField } from './TextField.js';
|
|
16
|
+
export type { TextFieldProps } from './TextField.js';
|
|
17
|
+
export { default as Checkbox } from './Checkbox.js';
|
|
18
|
+
export type { CheckboxProps } from './Checkbox.js';
|
|
19
|
+
export { default as Radio } from './Radio.js';
|
|
20
|
+
export type { RadioProps } from './Radio.js';
|
|
21
|
+
export { default as Switch } from './Switch.js';
|
|
22
|
+
export type { SwitchProps } from './Switch.js';
|
|
23
|
+
export { default as ChoiceGroup } from './ChoiceGroup.js';
|
|
24
|
+
export type { ChoiceGroupProps } from './ChoiceGroup.js';
|
|
12
25
|
export { Grid, Col } from './Grid.js';
|
|
13
26
|
export { default as Reveal } from './Reveal.js';
|
|
14
27
|
export { default as ThemeScript, THEME_STORAGE_KEY } from './ThemeScript.js';
|
package/index.js
CHANGED
|
@@ -5,6 +5,13 @@ export { default as ListRow } from './ListRow.js';
|
|
|
5
5
|
export { Accordion, useAccordionGroup } from './Accordion.js';
|
|
6
6
|
export { default as Timeline, TimelineItem } from './Timeline.js';
|
|
7
7
|
export { default as ExplodedView } from './ExplodedView.js';
|
|
8
|
+
export { default as PageHeader } from './PageHeader.js';
|
|
9
|
+
export { default as Section } from './Section.js';
|
|
10
|
+
export { default as TextField } from './TextField.js';
|
|
11
|
+
export { default as Checkbox } from './Checkbox.js';
|
|
12
|
+
export { default as Radio } from './Radio.js';
|
|
13
|
+
export { default as Switch } from './Switch.js';
|
|
14
|
+
export { default as ChoiceGroup } from './ChoiceGroup.js';
|
|
8
15
|
export { Grid, Col } from './Grid.js';
|
|
9
16
|
export { default as Reveal } from './Reveal.js';
|
|
10
17
|
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.10.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.10.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": ">=18"
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/* Valinnat: Checkbox, Radio, Switch ja ChoiceGroup.
|
|
2
|
+
==================================================================
|
|
3
|
+
Sovelluskomponentteja (päätös 22), sivusto ei käytä näitä. Kieli on
|
|
4
|
+
.btn- ja .field-perheen: hiusviiva --line-strong, ei pyöristystä
|
|
5
|
+
(paitsi radio, päätös 24), valittu = täytetty --ink.
|
|
6
|
+
|
|
7
|
+
Oikea <input> on merkin päällä näkymättömänä (opacity 0), joten
|
|
8
|
+
klikkaus, näppäimistö ja lomake toimivat natiivisti, ja merkin tila
|
|
9
|
+
luetaan inputin tilasta: `.choice__input:checked + .choice__mark`.
|
|
10
|
+
|
|
11
|
+
Hover paksuntaa reunan sisäänpäin piirretyllä viivalla eikä
|
|
12
|
+
border-widthillä: kytkimen nuppi on sijoitettu reunan sisään, eikä
|
|
13
|
+
se saa nytkähtää. */
|
|
14
|
+
|
|
15
|
+
.choice {
|
|
16
|
+
display: grid;
|
|
17
|
+
grid-template-columns: auto 1fr;
|
|
18
|
+
column-gap: var(--space-12);
|
|
19
|
+
align-items: start;
|
|
20
|
+
min-height: var(--tap-min);
|
|
21
|
+
/* Yksirivinen valinta on --tap-min korkea, teksti keskellä. */
|
|
22
|
+
padding-block: calc((var(--tap-min) - var(--lh-body) * var(--text-body)) / 2);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.choice__control {
|
|
26
|
+
position: relative;
|
|
27
|
+
display: block;
|
|
28
|
+
width: var(--space-20);
|
|
29
|
+
height: var(--space-20);
|
|
30
|
+
/* Merkki ensimmäisen rivin keskelle. */
|
|
31
|
+
margin-top: calc((var(--lh-body) * var(--text-body) - var(--space-20)) / 2);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.choice__input {
|
|
35
|
+
position: absolute;
|
|
36
|
+
inset: 0;
|
|
37
|
+
width: 100%;
|
|
38
|
+
height: 100%;
|
|
39
|
+
margin: 0;
|
|
40
|
+
opacity: 0;
|
|
41
|
+
cursor: pointer;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.choice__mark {
|
|
45
|
+
position: absolute;
|
|
46
|
+
inset: 0;
|
|
47
|
+
border: var(--hairline) solid var(--line-strong);
|
|
48
|
+
border-radius: var(--radius);
|
|
49
|
+
background: var(--paper);
|
|
50
|
+
pointer-events: none;
|
|
51
|
+
transition:
|
|
52
|
+
background var(--dur-fast) var(--ease-standard),
|
|
53
|
+
border-color var(--dur-fast) var(--ease-standard),
|
|
54
|
+
box-shadow var(--dur-fast) var(--ease-standard);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.choice__mark svg { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
58
|
+
.choice__mark path {
|
|
59
|
+
fill: none;
|
|
60
|
+
stroke: var(--paper);
|
|
61
|
+
stroke-width: var(--hairline-strong);
|
|
62
|
+
stroke-linecap: butt;
|
|
63
|
+
stroke-linejoin: miter;
|
|
64
|
+
opacity: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.choice__label {
|
|
68
|
+
font-size: var(--text-body);
|
|
69
|
+
line-height: var(--lh-body);
|
|
70
|
+
color: var(--ink);
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.choice__hint {
|
|
75
|
+
grid-column: 2;
|
|
76
|
+
margin: 0;
|
|
77
|
+
font-size: var(--text-body-s);
|
|
78
|
+
line-height: var(--lh-body-s);
|
|
79
|
+
color: var(--ink-muted);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* ---- tilat ------------------------------------------------------- */
|
|
83
|
+
|
|
84
|
+
.choice:hover .choice__input:not(:disabled) + .choice__mark {
|
|
85
|
+
box-shadow: inset 0 0 0 var(--hairline) var(--line-strong);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.choice__input:focus-visible + .choice__mark {
|
|
89
|
+
outline: var(--focus-width) solid var(--ink);
|
|
90
|
+
outline-offset: var(--focus-offset);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.choice__input:checked + .choice__mark,
|
|
94
|
+
.choice__input:indeterminate + .choice__mark {
|
|
95
|
+
background: var(--ink);
|
|
96
|
+
border-color: var(--ink);
|
|
97
|
+
}
|
|
98
|
+
.choice__input:checked:not(:indeterminate) + .choice__mark .choice__check,
|
|
99
|
+
.choice__input:indeterminate + .choice__mark .choice__dash { opacity: 1; }
|
|
100
|
+
|
|
101
|
+
/* ---- radio: ympyrä ja piste --------------------------------------- */
|
|
102
|
+
|
|
103
|
+
.choice--radio .choice__mark { border-radius: 50%; }
|
|
104
|
+
.choice--radio .choice__input:checked + .choice__mark {
|
|
105
|
+
background: var(--paper);
|
|
106
|
+
box-shadow: inset 0 0 0 var(--hairline) var(--ink);
|
|
107
|
+
}
|
|
108
|
+
.choice--radio .choice__input:checked + .choice__mark::after {
|
|
109
|
+
content: '';
|
|
110
|
+
position: absolute;
|
|
111
|
+
inset: calc(var(--space-4) + var(--hairline));
|
|
112
|
+
border-radius: 50%;
|
|
113
|
+
background: var(--ink);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* ---- kytkin: kisko ja nuppi, label vasemmalla --------------------- */
|
|
117
|
+
|
|
118
|
+
.choice--switch { grid-template-columns: 1fr auto; column-gap: var(--space-24); }
|
|
119
|
+
.choice--switch .choice__control {
|
|
120
|
+
grid-column: 2;
|
|
121
|
+
grid-row: 1;
|
|
122
|
+
width: var(--space-40);
|
|
123
|
+
height: var(--space-24);
|
|
124
|
+
margin-top: calc((var(--lh-body) * var(--text-body) - var(--space-24)) / 2);
|
|
125
|
+
}
|
|
126
|
+
.choice--switch .choice__label { grid-column: 1; grid-row: 1; }
|
|
127
|
+
.choice--switch .choice__hint { grid-column: 1; }
|
|
128
|
+
|
|
129
|
+
/* Nuppi on 16 px ja istuu 4 px:n päässä kiskon ulkoreunasta. */
|
|
130
|
+
.choice--switch .choice__mark::after {
|
|
131
|
+
content: '';
|
|
132
|
+
position: absolute;
|
|
133
|
+
top: calc((var(--space-24) - var(--space-16)) / 2 - var(--hairline));
|
|
134
|
+
left: calc((var(--space-24) - var(--space-16)) / 2 - var(--hairline));
|
|
135
|
+
width: var(--space-16);
|
|
136
|
+
height: var(--space-16);
|
|
137
|
+
background: var(--ink-muted);
|
|
138
|
+
transition:
|
|
139
|
+
transform var(--dur-base) var(--ease-out),
|
|
140
|
+
background var(--dur-fast) var(--ease-standard);
|
|
141
|
+
}
|
|
142
|
+
.choice--switch .choice__input:checked + .choice__mark::after {
|
|
143
|
+
transform: translateX(var(--space-16));
|
|
144
|
+
background: var(--paper);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ---- disabled ---------------------------------------------------- */
|
|
148
|
+
|
|
149
|
+
.choice--disabled .choice__label,
|
|
150
|
+
.choice--disabled .choice__hint { color: var(--ink-faint); cursor: not-allowed; }
|
|
151
|
+
.choice__input:disabled { cursor: not-allowed; }
|
|
152
|
+
.choice__input:disabled + .choice__mark {
|
|
153
|
+
background: var(--paper-alt);
|
|
154
|
+
border-color: var(--ink-faint);
|
|
155
|
+
}
|
|
156
|
+
.choice__input:disabled:checked + .choice__mark,
|
|
157
|
+
.choice__input:disabled:indeterminate + .choice__mark {
|
|
158
|
+
background: var(--ink-faint);
|
|
159
|
+
border-color: var(--ink-faint);
|
|
160
|
+
}
|
|
161
|
+
.choice__input:disabled + .choice__mark .choice__check,
|
|
162
|
+
.choice__input:disabled + .choice__mark .choice__dash { stroke: var(--paper-alt); }
|
|
163
|
+
.choice--radio .choice__input:disabled:checked + .choice__mark { background: var(--paper-alt); box-shadow: none; }
|
|
164
|
+
.choice--radio .choice__input:disabled:checked + .choice__mark::after,
|
|
165
|
+
.choice--switch .choice__input:disabled + .choice__mark::after { background: var(--ink-faint); }
|
|
166
|
+
.choice--switch .choice__input:disabled:checked + .choice__mark::after { background: var(--paper-alt); }
|
|
167
|
+
|
|
168
|
+
/* ---- ryhmä ------------------------------------------------------- */
|
|
169
|
+
|
|
170
|
+
.choice-group {
|
|
171
|
+
display: grid;
|
|
172
|
+
margin: 0;
|
|
173
|
+
padding: 0;
|
|
174
|
+
border: 0;
|
|
175
|
+
min-width: 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Sama kuin .field__label: ryhmän otsikko on lomakkeen label. */
|
|
179
|
+
.choice-group__legend {
|
|
180
|
+
padding: 0;
|
|
181
|
+
margin-bottom: var(--space-4);
|
|
182
|
+
font-size: var(--text-body-s);
|
|
183
|
+
line-height: var(--lh-body-s);
|
|
184
|
+
font-weight: var(--weight-500);
|
|
185
|
+
color: var(--ink);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.choice-group > .field__hint,
|
|
189
|
+
.choice-group > .field__error { margin-top: var(--space-4); }
|
|
190
|
+
|
|
191
|
+
/* Virhe: jokaisen valinnan reuna --danger ja paksumpi, kuten kentässä. */
|
|
192
|
+
.choice-group--invalid .choice__input:not(:checked) + .choice__mark {
|
|
193
|
+
border-color: var(--danger);
|
|
194
|
+
box-shadow: inset 0 0 0 var(--hairline) var(--danger);
|
|
195
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* Patternit: sivun otsikko ja osio (PageHeader, Section).
|
|
2
|
+
Eivät tuo omaa ulkoasua, vain asettelun olemassa oleville
|
|
3
|
+
typografialuokille. Siirretty work.css:stä ja case.css:stä, jotta
|
|
4
|
+
ne lähtevät paketin mukana (päätös 21). */
|
|
5
|
+
|
|
6
|
+
.page-head {
|
|
7
|
+
padding-top: var(--space-40);
|
|
8
|
+
padding-bottom: var(--space-32);
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
gap: var(--space-12);
|
|
12
|
+
}
|
|
13
|
+
@media (min-width: 600px) {
|
|
14
|
+
.page-head { flex-direction: row; align-items: baseline; justify-content: space-between; gap: var(--space-24); }
|
|
15
|
+
}
|
|
16
|
+
@media (min-width: 900px) { .page-head { padding-top: var(--space-56); padding-bottom: var(--space-40); } }
|
|
17
|
+
|
|
18
|
+
/* Ingressi otsikon alla, samassa sarakkeessa. */
|
|
19
|
+
.page-header__lede { margin: 0; padding-bottom: var(--space-40); }
|
|
20
|
+
|
|
21
|
+
.section-head {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: baseline;
|
|
24
|
+
justify-content: space-between;
|
|
25
|
+
gap: var(--space-16);
|
|
26
|
+
padding-bottom: var(--space-16);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.section--aside {
|
|
30
|
+
display: grid;
|
|
31
|
+
grid-template-columns: minmax(0, 1fr);
|
|
32
|
+
gap: var(--space-20);
|
|
33
|
+
padding-block: var(--section-gap);
|
|
34
|
+
border-top: var(--hairline) solid var(--line);
|
|
35
|
+
}
|
|
36
|
+
@media (min-width: 900px) {
|
|
37
|
+
.section--aside { grid-template-columns: 200px minmax(0, 1fr); gap: var(--space-40); }
|
|
38
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* Tekstikenttä (TextField). Sovellusnäkymien komponentti, ei sivuston.
|
|
2
|
+
==================================================================
|
|
3
|
+
Sama kieli kuin .btn: hiusviiva, ei pyöristystä, ei varjoa. Kenttä
|
|
4
|
+
on 48 px, nappia (44) korkeampi: 16 px:n teksti rivivälillä 1.65
|
|
5
|
+
tarvitsee tilaa, ja kirjoitettava pinta saa olla painettavaa
|
|
6
|
+
suurempi. */
|
|
7
|
+
|
|
8
|
+
.field { display: grid; gap: var(--space-8); align-content: start; }
|
|
9
|
+
|
|
10
|
+
.field__label {
|
|
11
|
+
font-size: var(--text-body-s);
|
|
12
|
+
line-height: var(--lh-body-s);
|
|
13
|
+
font-weight: var(--weight-500);
|
|
14
|
+
color: var(--ink);
|
|
15
|
+
}
|
|
16
|
+
.field__optional { font-weight: var(--weight-400); color: var(--ink-faint); }
|
|
17
|
+
|
|
18
|
+
.field__control {
|
|
19
|
+
appearance: none;
|
|
20
|
+
width: 100%;
|
|
21
|
+
margin: 0;
|
|
22
|
+
/* 16 px eikä body-s: iOS zoomaa alle 16 px:n kentät. */
|
|
23
|
+
font: inherit;
|
|
24
|
+
font-size: var(--text-body);
|
|
25
|
+
line-height: var(--lh-body);
|
|
26
|
+
color: var(--ink);
|
|
27
|
+
background: var(--paper);
|
|
28
|
+
height: var(--space-48);
|
|
29
|
+
padding: 0 var(--space-16);
|
|
30
|
+
border: var(--hairline) solid var(--line-strong);
|
|
31
|
+
border-radius: var(--radius);
|
|
32
|
+
transition: border-color var(--dur-fast) var(--ease-standard);
|
|
33
|
+
}
|
|
34
|
+
.field__control::placeholder { color: var(--ink-faint); opacity: 1; }
|
|
35
|
+
|
|
36
|
+
textarea.field__control {
|
|
37
|
+
height: auto;
|
|
38
|
+
min-height: var(--space-128);
|
|
39
|
+
padding: var(--space-12) var(--space-16);
|
|
40
|
+
resize: vertical;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Hover = alaviiva paksunee, sama liike kuin .btn--text:llä. Kenttä
|
|
44
|
+
on border-box ja kiinteän korkuinen, joten mitään ei nytkähdä. */
|
|
45
|
+
.field__control:hover:not(:disabled) { border-bottom-width: var(--hairline-strong); }
|
|
46
|
+
|
|
47
|
+
.field__hint {
|
|
48
|
+
margin: 0;
|
|
49
|
+
font-size: var(--text-body-s);
|
|
50
|
+
line-height: var(--lh-body-s);
|
|
51
|
+
color: var(--ink-muted);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ---- virhe ------------------------------------------------------- */
|
|
55
|
+
|
|
56
|
+
/* Koko reuna paksunee, ei vain alaviiva: virhe ei saa näyttää
|
|
57
|
+
hoverilta. Padding kevenee saman verran, joten teksti pysyy
|
|
58
|
+
paikallaan. */
|
|
59
|
+
.field--invalid .field__control {
|
|
60
|
+
border-color: var(--danger);
|
|
61
|
+
border-width: var(--hairline-strong);
|
|
62
|
+
padding-inline: calc(var(--space-16) - (var(--hairline-strong) - var(--hairline)));
|
|
63
|
+
}
|
|
64
|
+
.field--invalid textarea.field__control {
|
|
65
|
+
padding-block: calc(var(--space-12) - (var(--hairline-strong) - var(--hairline)));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.field__error {
|
|
69
|
+
margin: 0;
|
|
70
|
+
display: flex;
|
|
71
|
+
gap: var(--space-8);
|
|
72
|
+
align-items: flex-start;
|
|
73
|
+
font-size: var(--text-body-s);
|
|
74
|
+
line-height: var(--lh-body-s);
|
|
75
|
+
font-weight: var(--weight-500);
|
|
76
|
+
color: var(--danger);
|
|
77
|
+
}
|
|
78
|
+
/* Ikoni ensimmäisen rivin keskelle: (rivinkorkeus − ikoni) / 2. */
|
|
79
|
+
.field__error .icon { margin-top: calc((var(--lh-body-s) * var(--text-body-s) - var(--icon-m)) / 2); }
|
|
80
|
+
|
|
81
|
+
/* ---- disabled ---------------------------------------------------- */
|
|
82
|
+
|
|
83
|
+
/* Sama kuin .btn[disabled]: viiva haalenee, teksti faint. Pinta on
|
|
84
|
+
paper-alt, jotta tyhjäkin kenttä erottuu käytössä olevasta. */
|
|
85
|
+
.field--disabled .field__label { color: var(--ink-faint); }
|
|
86
|
+
.field__control:disabled {
|
|
87
|
+
background: var(--paper-alt);
|
|
88
|
+
border-color: var(--line);
|
|
89
|
+
color: var(--ink-faint);
|
|
90
|
+
cursor: not-allowed;
|
|
91
|
+
}
|