@pokela/components 2.7.0 → 2.9.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/Icon.d.ts +4 -1
- package/Icon.js +4 -0
- package/PageHeader.d.ts +18 -0
- package/PageHeader.js +16 -0
- package/README.md +6 -1
- package/Section.d.ts +24 -0
- package/Section.js +8 -0
- package/TextField.d.ts +35 -0
- package/TextField.js +20 -0
- package/guidelines/components.md +34 -0
- package/guidelines/setup.md +22 -0
- package/guidelines/styles.md +1 -1
- package/guidelines/tokens.md +4 -0
- package/index.d.ts +5 -0
- package/index.js +3 -0
- package/package.json +2 -2
- package/styles/patterns.css +38 -0
- package/styles/text-field.css +91 -0
- package/styles.css +2 -0
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.9.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,9 @@ tyylit saa myös suoraan: `@pokela/components/styles.css`.
|
|
|
23
23
|
- `Accordion`
|
|
24
24
|
- `Timeline`
|
|
25
25
|
- `ExplodedView`
|
|
26
|
+
- `PageHeader`
|
|
27
|
+
- `Section`
|
|
28
|
+
- `TextField`
|
|
26
29
|
- `Grid`
|
|
27
30
|
- `Reveal`
|
|
28
31
|
- `ThemeScript`
|
|
@@ -57,6 +60,8 @@ import Link from 'next/link';
|
|
|
57
60
|
- `styles/accordion.css`
|
|
58
61
|
- `styles/timeline.css`
|
|
59
62
|
- `styles/exploded-view.css`
|
|
63
|
+
- `styles/patterns.css`
|
|
64
|
+
- `styles/text-field.css`
|
|
60
65
|
|
|
61
66
|
Luokat ovat globaaleja, eivät CSS-moduuleja. Se on tarkoituksellista: sama
|
|
62
67
|
luokka on myös Figmassa komponentin nimenä, ja hajautettu nimi katkaisisi
|
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/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,9 @@ Use these instead of building an equivalent.
|
|
|
8
8
|
- `Accordion`, `useAccordionGroup`
|
|
9
9
|
- `Timeline`, `TimelineItem`
|
|
10
10
|
- `ExplodedView`
|
|
11
|
+
- `PageHeader`
|
|
12
|
+
- `Section`
|
|
13
|
+
- `TextField`
|
|
11
14
|
- `Grid`, `Col`
|
|
12
15
|
- `Reveal`
|
|
13
16
|
- `ThemeScript`, `THEME_STORAGE_KEY`
|
|
@@ -69,4 +72,35 @@ reader, described by `label`.
|
|
|
69
72
|
/>
|
|
70
73
|
```
|
|
71
74
|
|
|
75
|
+
`TextField` is the form field: label, input, hint and error in one. Use it
|
|
76
|
+
for every text input and text area (`multiline`) instead of a bare `<input>`.
|
|
77
|
+
Pass `error` with a message to show the error state; never colour a field
|
|
78
|
+
yourself. Mark an optional field with `optional="(optional)"`, not an asterisk
|
|
79
|
+
on the required ones. Put form buttons after the fields with the `.btn`
|
|
80
|
+
classes.
|
|
81
|
+
|
|
82
|
+
```jsx
|
|
83
|
+
<TextField label="Email" type="email" error="The address is missing a domain." />
|
|
84
|
+
<TextField label="Message" optional="(optional)" multiline hint="Up to 500 characters." />
|
|
85
|
+
## Patterns
|
|
86
|
+
|
|
87
|
+
`PageHeader` and `Section` are patterns: they add no look of their own, only
|
|
88
|
+
the layout every page uses. Build pages from them instead of arranging
|
|
89
|
+
headings and grids yourself.
|
|
90
|
+
|
|
91
|
+
`PageHeader` is the page's title with an optional `meta` and `lede`. The lede
|
|
92
|
+
always sits under the title in the same column, never beside it.
|
|
93
|
+
|
|
94
|
+
`Section` names a part of the page with a small `title` (and optional
|
|
95
|
+
`meta`). `layout="stacked"` puts the title row above the content;
|
|
96
|
+
`layout="aside"` puts the title in a narrow left column and the content
|
|
97
|
+
beside it. Use one layout per page.
|
|
98
|
+
|
|
99
|
+
```jsx
|
|
100
|
+
<PageHeader title="From idea to launch" meta="9 weeks" lede="Each phase ends in a shared decision." />
|
|
101
|
+
<Section title="Road to launch" layout="aside">
|
|
102
|
+
<Timeline>…</Timeline>
|
|
103
|
+
</Section>
|
|
104
|
+
```
|
|
105
|
+
|
|
72
106
|
`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,6 +8,10 @@ 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` already applies 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
|
|
package/index.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ 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';
|
|
12
17
|
export { Grid, Col } from './Grid.js';
|
|
13
18
|
export { default as Reveal } from './Reveal.js';
|
|
14
19
|
export { default as ThemeScript, THEME_STORAGE_KEY } from './ThemeScript.js';
|
package/index.js
CHANGED
|
@@ -5,6 +5,9 @@ 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';
|
|
8
11
|
export { Grid, Col } from './Grid.js';
|
|
9
12
|
export { default as Reveal } from './Reveal.js';
|
|
10
13
|
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.9.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.9.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": ">=18"
|
|
@@ -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
|
+
}
|