@pokela/components 2.6.1 → 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/ExplodedView.d.ts +6 -0
- package/ExplodedView.js +48 -5
- 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 +38 -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/exploded-view.css +11 -5
- package/styles/patterns.css +38 -0
- package/styles/text-field.css +91 -0
- package/styles.css +2 -0
package/ExplodedView.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ import { type ReactNode } from 'react';
|
|
|
10
10
|
* - Osoitin kääntää pinoa ja erkanee kerroksia.
|
|
11
11
|
* - Elementit, joilla on sama `data-kohde`, syttyvät yhdessä: osoitin
|
|
12
12
|
* komponentin päällä näyttää sen jokaisessa kerroksessa.
|
|
13
|
+
* - `data-token="ink"` (väri `--ink`) tai `data-token="display-l"`
|
|
14
|
+
* (tyyppi `--text-display-l`) merkitsee tokenin. Osoitin tokenin päällä
|
|
15
|
+
* sytyttää muiden kerrosten elementit, joiden laskettu tyyli käyttää
|
|
16
|
+
* sen arvoa, ja osoitin elementin päällä sytyttää sen tokenit. Käyttöä
|
|
17
|
+
* ei merkitä käsin: se luetaan selaimen lasketusta tyylistä, joten se
|
|
18
|
+
* pysyy totena kun tyylit muuttuvat.
|
|
13
19
|
* - Klikkaus nostaa kerroksen kerrallaan esiin ja lopuksi palauttaa
|
|
14
20
|
* koko pinon.
|
|
15
21
|
* - Tilassa `auki` vieritys kokoaa kerrokset yhdeksi suoraksi
|
package/ExplodedView.js
CHANGED
|
@@ -118,14 +118,57 @@ export default function ExplodedView({ layers, label, tila = 'auki', className,
|
|
|
118
118
|
}
|
|
119
119
|
liiku();
|
|
120
120
|
};
|
|
121
|
+
/* Tokenin arvo selaimen laskemana: väri rgb-muodossa, tyyppi pikseleinä. */
|
|
122
|
+
function tokeninArvo(nimi) {
|
|
123
|
+
const apu = document.createElement('i');
|
|
124
|
+
apu.style.color = `var(--${nimi})`;
|
|
125
|
+
apu.style.fontSize = `var(--text-${nimi})`;
|
|
126
|
+
hero.append(apu);
|
|
127
|
+
const s = getComputedStyle(apu);
|
|
128
|
+
const tyyppi = getComputedStyle(hero).getPropertyValue(`--text-${nimi}`).trim() !== '';
|
|
129
|
+
const arvo = tyyppi ? { tyyppi: s.fontSize } : { vari: s.color };
|
|
130
|
+
apu.remove();
|
|
131
|
+
return arvo;
|
|
132
|
+
}
|
|
133
|
+
/* Käyttääkö elementti arvoa itse: teksti omana tekstinään, väri
|
|
134
|
+
myös taustana tai viivana. */
|
|
135
|
+
function kayttaa(x, arvo) {
|
|
136
|
+
const s = getComputedStyle(x);
|
|
137
|
+
const teksti = [...x.childNodes].some((c) => c.nodeType === 3 && c.textContent?.trim());
|
|
138
|
+
if (arvo.tyyppi)
|
|
139
|
+
return teksti && s.fontSize === arvo.tyyppi;
|
|
140
|
+
return ((teksti && s.color === arvo.vari) ||
|
|
141
|
+
s.backgroundColor === arvo.vari ||
|
|
142
|
+
(parseFloat(s.borderTopWidth) > 0 && s.borderTopColor === arvo.vari) ||
|
|
143
|
+
(parseFloat(s.borderBottomWidth) > 0 && s.borderBottomColor === arvo.vari));
|
|
144
|
+
}
|
|
145
|
+
const sammuta = () => el.querySelectorAll('.exploded--valaistu').forEach((x) => x.classList.remove('exploded--valaistu'));
|
|
121
146
|
const valaise = (e) => {
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
|
|
147
|
+
sammuta();
|
|
148
|
+
const kohde = e.target;
|
|
149
|
+
const token = kohde.closest('[data-token]');
|
|
150
|
+
const tokenit = [...el.querySelectorAll('[data-token]')];
|
|
151
|
+
if (token) {
|
|
152
|
+
/* Token: sen käyttäjät muissa kerroksissa. */
|
|
153
|
+
const arvo = tokeninArvo(token.dataset.token);
|
|
154
|
+
const oma = token.closest('.exploded__layer');
|
|
155
|
+
token.classList.add('exploded--valaistu');
|
|
156
|
+
el.querySelectorAll('.exploded__plate, .exploded__plate *').forEach((x) => {
|
|
157
|
+
if (x.closest('.exploded__layer') !== oma && !x.closest('[data-token]') && kayttaa(x, arvo))
|
|
158
|
+
x.classList.add('exploded--valaistu');
|
|
159
|
+
});
|
|
125
160
|
return;
|
|
126
|
-
|
|
161
|
+
}
|
|
162
|
+
/* Komponentti kaikissa kerroksissa ja elementin omat tokenit. */
|
|
163
|
+
const k = kohde.closest('[data-kohde]');
|
|
164
|
+
if (k)
|
|
165
|
+
el.querySelectorAll(`[data-kohde="${k.dataset.kohde}"]`).forEach((x) => x.classList.add('exploded--valaistu'));
|
|
166
|
+
if (kohde.closest('.exploded__plate'))
|
|
167
|
+
tokenit.forEach((t) => {
|
|
168
|
+
if (kayttaa(kohde, tokeninArvo(t.dataset.token)))
|
|
169
|
+
t.classList.add('exploded--valaistu');
|
|
170
|
+
});
|
|
127
171
|
};
|
|
128
|
-
const sammuta = () => el.querySelectorAll('.exploded--valaistu').forEach((x) => x.classList.remove('exploded--valaistu'));
|
|
129
172
|
/* Vieritys kokoaa, kun kuva poistuu yläreunasta. */
|
|
130
173
|
const vieritys = () => {
|
|
131
174
|
const r = hero.getBoundingClientRect();
|
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`
|
|
@@ -50,6 +53,10 @@ the total duration.
|
|
|
50
53
|
`ExplodedView` shows layers as plates in 3D space: how a finished surface is
|
|
51
54
|
built from its parts. Each layer is real markup, not an image. Give elements
|
|
52
55
|
the same `data-kohde` in several layers and they light up together on hover.
|
|
56
|
+
Mark a token with `data-token` (`"ink"` for `--ink`, `"display-l"` for
|
|
57
|
+
`--text-display-l`): hovering it lights every element in the other layers
|
|
58
|
+
whose computed style uses that value, and hovering an element lights its
|
|
59
|
+
tokens. Usage is read from computed styles, never marked by hand.
|
|
53
60
|
`tila="koottu"` starts with the layers collapsed into one surface. Put no
|
|
54
61
|
links or buttons inside the layers; the whole view is one image to a screen
|
|
55
62
|
reader, described by `label`.
|
|
@@ -65,4 +72,35 @@ reader, described by `label`.
|
|
|
65
72
|
/>
|
|
66
73
|
```
|
|
67
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
|
+
|
|
68
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"
|
package/styles/exploded-view.css
CHANGED
|
@@ -67,11 +67,17 @@
|
|
|
67
67
|
|
|
68
68
|
.exploded__stack--kohdistus .exploded__layer:not(.exploded__layer--aktiivinen) .exploded__plate,
|
|
69
69
|
.exploded__stack--kohdistus .exploded__layer:not(.exploded__layer--aktiivinen) .exploded__name { opacity: 0.35; }
|
|
70
|
+
/* Himmennetty levy ei ota osoitinta: nostetun levyn peitetyt osat,
|
|
71
|
+
kuten viuhkassa piiloon jäävät tokenit, saa osoitettua. */
|
|
72
|
+
.exploded__stack--kohdistus .exploded__layer:not(.exploded__layer--aktiivinen) { pointer-events: none; }
|
|
70
73
|
|
|
71
|
-
/* Sama kohde kaikissa kerroksissa
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
/* Sama kohde tai token kaikissa kerroksissa saa yhtenäisen viivan.
|
|
75
|
+
Korostuksen on oltava pohjaa spesifisempi: kun se ei ollut,
|
|
76
|
+
luokka vaihtui mutta viiva jäi läpinäkyväksi. */
|
|
77
|
+
.exploded__plate,
|
|
78
|
+
.exploded__plate * {
|
|
79
|
+
outline: var(--hairline) solid transparent;
|
|
74
80
|
outline-offset: var(--space-4);
|
|
75
|
-
transition: outline-color var(--dur-fast) var(--ease-standard);
|
|
76
81
|
}
|
|
77
|
-
.
|
|
82
|
+
.exploded__plate * { transition: outline-color var(--dur-fast) var(--ease-standard); }
|
|
83
|
+
.exploded .exploded--valaistu { outline: var(--hairline-strong) solid var(--ink); }
|
|
@@ -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
|
+
}
|