@pokela/components 2.9.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/README.md +6 -1
- package/Radio.d.ts +17 -0
- package/Radio.js +11 -0
- package/Switch.d.ts +19 -0
- package/Switch.js +11 -0
- package/guidelines/components.md +24 -0
- package/guidelines/tokens.md +3 -2
- package/index.d.ts +8 -0
- package/index.js +4 -0
- package/package.json +2 -2
- package/styles/choice.css +195 -0
- package/styles.css +1 -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/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
|
|
@@ -26,6 +26,10 @@ tyylit saa myös suoraan: `@pokela/components/styles.css`.
|
|
|
26
26
|
- `PageHeader`
|
|
27
27
|
- `Section`
|
|
28
28
|
- `TextField`
|
|
29
|
+
- `Checkbox`
|
|
30
|
+
- `Radio`
|
|
31
|
+
- `Switch`
|
|
32
|
+
- `ChoiceGroup`
|
|
29
33
|
- `Grid`
|
|
30
34
|
- `Reveal`
|
|
31
35
|
- `ThemeScript`
|
|
@@ -62,6 +66,7 @@ import Link from 'next/link';
|
|
|
62
66
|
- `styles/exploded-view.css`
|
|
63
67
|
- `styles/patterns.css`
|
|
64
68
|
- `styles/text-field.css`
|
|
69
|
+
- `styles/choice.css`
|
|
65
70
|
|
|
66
71
|
Luokat ovat globaaleja, eivät CSS-moduuleja. Se on tarkoituksellista: sama
|
|
67
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/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/guidelines/components.md
CHANGED
|
@@ -11,6 +11,10 @@ Use these instead of building an equivalent.
|
|
|
11
11
|
- `PageHeader`
|
|
12
12
|
- `Section`
|
|
13
13
|
- `TextField`
|
|
14
|
+
- `Checkbox`
|
|
15
|
+
- `Radio`
|
|
16
|
+
- `Switch`
|
|
17
|
+
- `ChoiceGroup`
|
|
14
18
|
- `Grid`, `Col`
|
|
15
19
|
- `Reveal`
|
|
16
20
|
- `ThemeScript`, `THEME_STORAGE_KEY`
|
|
@@ -82,6 +86,26 @@ classes.
|
|
|
82
86
|
```jsx
|
|
83
87
|
<TextField label="Email" type="email" error="The address is missing a domain." />
|
|
84
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
|
+
|
|
85
109
|
## Patterns
|
|
86
110
|
|
|
87
111
|
`PageHeader` and `Section` are patterns: they add no look of their own, only
|
package/guidelines/tokens.md
CHANGED
|
@@ -9,13 +9,14 @@ 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` already
|
|
12
|
+
for emphasis, buttons or decoration. `TextField` and `ChoiceGroup` 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
|
|
16
16
|
(`--hairline`), never elevation.
|
|
17
17
|
|
|
18
|
-
**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.
|
|
19
20
|
|
|
20
21
|
<!-- generated:make-valit -->**No spacing outside the scale.** 16 steps, each named by its value:<!-- /generated -->
|
|
21
22
|
`--space-24` is 24px. If a value is missing, use the nearest step rather than
|
package/index.d.ts
CHANGED
|
@@ -14,6 +14,14 @@ export { default as Section } from './Section.js';
|
|
|
14
14
|
export type { SectionLayout } from './Section.js';
|
|
15
15
|
export { default as TextField } from './TextField.js';
|
|
16
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';
|
|
17
25
|
export { Grid, Col } from './Grid.js';
|
|
18
26
|
export { default as Reveal } from './Reveal.js';
|
|
19
27
|
export { default as ThemeScript, THEME_STORAGE_KEY } from './ThemeScript.js';
|
package/index.js
CHANGED
|
@@ -8,6 +8,10 @@ export { default as ExplodedView } from './ExplodedView.js';
|
|
|
8
8
|
export { default as PageHeader } from './PageHeader.js';
|
|
9
9
|
export { default as Section } from './Section.js';
|
|
10
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';
|
|
11
15
|
export { Grid, Col } from './Grid.js';
|
|
12
16
|
export { default as Reveal } from './Reveal.js';
|
|
13
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
|
+
}
|
package/styles.css
CHANGED