@pokela/components 0.0.0-stage → 1.2.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/Accordion.d.ts +34 -0
- package/Accordion.js +16 -0
- package/Grid.d.ts +35 -0
- package/Grid.js +17 -0
- package/Icon.d.ts +36 -0
- package/Icon.js +16 -0
- package/LICENSE +21 -0
- package/ListRow.d.ts +47 -0
- package/ListRow.js +34 -0
- package/README.md +59 -2
- package/Reveal.d.ts +11 -0
- package/Reveal.js +32 -0
- package/ThemeScript.d.ts +12 -0
- package/ThemeScript.js +16 -0
- package/ThemeToggle.d.ts +15 -0
- package/ThemeToggle.js +49 -0
- package/index.d.ts +10 -0
- package/index.js +8 -0
- package/package.json +36 -4
- package/styles/accordion.css +41 -0
- package/styles/base.css +308 -0
- package/styles/icon.css +40 -0
- package/styles/list-row.css +206 -0
- package/styles.css +6 -0
package/Accordion.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Avautuva rivi.
|
|
4
|
+
*
|
|
5
|
+
* Vain yksi kerrallaan auki — siitä huolehtii <AccordionGroup>.
|
|
6
|
+
*
|
|
7
|
+
* Rivi itse EI ole listarivin sukulainen vaan sama rivi: se käyttää
|
|
8
|
+
* `.list-row`-runkoa modifioijalla `--expandable`. Ero on siinä että
|
|
9
|
+
* tämä on <button> eikä linkki, ja siksi se on oma komponenttinsa —
|
|
10
|
+
* avaustila, paneeli ja ryhmälogiikka eivät kuulu linkkiin.
|
|
11
|
+
*
|
|
12
|
+
* Toteutus on <button> + region, ei <details>, koska "vain yksi auki"
|
|
13
|
+
* vaatii jaetun tilan ja koska avautuva sisältö sisältää otsikoita
|
|
14
|
+
* joiden pitää pysyä dokumentin rakenteessa.
|
|
15
|
+
*/
|
|
16
|
+
export type AccordionProps = {
|
|
17
|
+
number: string;
|
|
18
|
+
title: string;
|
|
19
|
+
description: string;
|
|
20
|
+
meta: string;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
open: boolean;
|
|
23
|
+
onToggle: () => void;
|
|
24
|
+
labels: {
|
|
25
|
+
open: string;
|
|
26
|
+
close: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export declare function Accordion({ number, title, description, meta, children, open, onToggle, labels, }: AccordionProps): import("react/jsx-runtime").JSX.Element;
|
|
30
|
+
/** Pitää huolen että vain yksi rivi on auki kerrallaan. */
|
|
31
|
+
export declare function useAccordionGroup(): {
|
|
32
|
+
isOpen: (slug: string) => boolean;
|
|
33
|
+
toggle: (slug: string) => void;
|
|
34
|
+
};
|
package/Accordion.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId, useState } from 'react';
|
|
4
|
+
import Icon from './Icon';
|
|
5
|
+
export function Accordion({ number, title, description, meta, children, open, onToggle, labels, }) {
|
|
6
|
+
const id = useId();
|
|
7
|
+
return (_jsxs("div", { className: "accordion", children: [_jsxs("button", { type: "button", className: "list-row list-row--numbered list-row--expandable bleed", "aria-expanded": open, "aria-controls": id, onClick: onToggle, children: [_jsx("span", { className: "meta list-row__number", children: number }), _jsxs("span", { className: "list-row__main", children: [_jsx("span", { className: "list-row__title", children: title }), _jsx("span", { className: "list-row__description", children: description })] }), _jsx("span", { className: "meta list-row__meta", children: meta }), _jsx(Icon, { name: open ? 'minus' : 'plus', size: "l", className: "list-row__icon" }), _jsx("span", { className: "visually-hidden", children: open ? labels.close : labels.open })] }), open ? (_jsx("div", { id: id, className: "accordion__panel page", role: "region", "aria-label": title, children: children })) : null] }));
|
|
8
|
+
}
|
|
9
|
+
/** Pitää huolen että vain yksi rivi on auki kerrallaan. */
|
|
10
|
+
export function useAccordionGroup() {
|
|
11
|
+
const [openSlug, setOpenSlug] = useState(null);
|
|
12
|
+
return {
|
|
13
|
+
isOpen: (slug) => openSlug === slug,
|
|
14
|
+
toggle: (slug) => setOpenSlug((current) => (current === slug ? null : slug)),
|
|
15
|
+
};
|
|
16
|
+
}
|
package/Grid.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CSSProperties, ElementType, ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 12-sarakkeinen grid (4 → 8 → 12).
|
|
4
|
+
*
|
|
5
|
+
* Sarakemäärä tulee tokenista `--columns`, joten komponentti ei tiedä
|
|
6
|
+
* breakpointeista mitään. Sijoittelu annetaan sarakkeina, ei pikseleinä:
|
|
7
|
+
*
|
|
8
|
+
* <Col base={4} md={7}> → koko leveys mobiilissa, 7/12 työpöydällä
|
|
9
|
+
* <Col base={4} md={4} startMd={9}> → sarakkeet 9–12
|
|
10
|
+
*/
|
|
11
|
+
type GridProps = {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
as?: ElementType;
|
|
14
|
+
className?: string;
|
|
15
|
+
style?: CSSProperties;
|
|
16
|
+
};
|
|
17
|
+
export declare function Grid({ children, as: Tag, className, style }: GridProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
type ColProps = {
|
|
19
|
+
children: ReactNode;
|
|
20
|
+
/** Sarakkeita 0–599px (max 4). Oletus: koko leveys. */
|
|
21
|
+
base?: number;
|
|
22
|
+
/** Sarakkeita 600–899px (max 8). Oletus: base skaalattuna. */
|
|
23
|
+
sm?: number;
|
|
24
|
+
/** Sarakkeita ≥900px (max 12). Oletus: sm. */
|
|
25
|
+
md?: number;
|
|
26
|
+
/** Aloitussarake ≥600px. */
|
|
27
|
+
startSm?: number;
|
|
28
|
+
/** Aloitussarake ≥900px. */
|
|
29
|
+
startMd?: number;
|
|
30
|
+
as?: ElementType;
|
|
31
|
+
className?: string;
|
|
32
|
+
style?: CSSProperties;
|
|
33
|
+
};
|
|
34
|
+
export declare function Col({ children, base, sm, md, startSm, startMd, as: Tag, className, style, }: ColProps): import("react/jsx-runtime").JSX.Element;
|
|
35
|
+
export {};
|
package/Grid.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export function Grid({ children, as: Tag = 'div', className, style }) {
|
|
3
|
+
return (_jsx(Tag, { className: ['grid', className].filter(Boolean).join(' '), style: style, children: children }));
|
|
4
|
+
}
|
|
5
|
+
export function Col({ children, base = 4, sm, md, startSm, startMd, as: Tag = 'div', className, style, }) {
|
|
6
|
+
const spanSm = sm ?? Math.min(8, base * 2);
|
|
7
|
+
const spanMd = md ?? Math.min(12, spanSm);
|
|
8
|
+
const vars = {
|
|
9
|
+
'--span-base': base,
|
|
10
|
+
'--span-sm': spanSm,
|
|
11
|
+
'--span-md': spanMd,
|
|
12
|
+
...(startSm ? { '--start-sm': startSm } : {}),
|
|
13
|
+
...(startMd ? { '--start-md': startMd } : {}),
|
|
14
|
+
...style,
|
|
15
|
+
};
|
|
16
|
+
return (_jsx(Tag, { className: ['col', className].filter(Boolean).join(' '), style: vars, children: children }));
|
|
17
|
+
}
|
package/Icon.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ikoni.
|
|
3
|
+
*
|
|
4
|
+
* Systeemi ei käytä ikonikirjastoa — se piirtää omat merkkinsä. Syy on
|
|
5
|
+
* sama kuin kaikessa muussakin täällä: merkin pitää olla samaa kieltä
|
|
6
|
+
* kuin viiva, väli ja typografia, eikä kolmannen osapuolen piirtäjän
|
|
7
|
+
* tulkinta siitä. Käytännön syy tuli kaupan päälle: `↗` ja `✕` olivat
|
|
8
|
+
* ennen tekstimerkkejä, ja ne riippuivat siitä mitä Google Fontsin
|
|
9
|
+
* osajoukossa sattui olemaan. Figmassa nuolta ei löytynyt Archivosta
|
|
10
|
+
* lainkaan.
|
|
11
|
+
*
|
|
12
|
+
* Piirtosäännöt:
|
|
13
|
+
* ruudukko 16 × 16, kaikki geometria puolikkaalla pikselillä
|
|
14
|
+
* viiva --hairline, sama kuin jokainen reuna systeemissä
|
|
15
|
+
* päätteet tylpät, kulmat terävät — --radius on 0
|
|
16
|
+
* väri currentColor, ei koskaan omaa väriä
|
|
17
|
+
*
|
|
18
|
+
* Koot ovat tokeneita: --icon-s (12) tekstin seassa metan kanssa,
|
|
19
|
+
* --icon-m (16) napin sisällä, --icon-l (24) omana painikkeenaan.
|
|
20
|
+
*
|
|
21
|
+
* Nuoli on kahta lajia eikä yhtä: `arrow-right` vie sivustolla eteen-
|
|
22
|
+
* päin, `arrow-up-right` ulos. Ero on lukijalle sama kuin
|
|
23
|
+
* target="_blank" ruudunlukijalle.
|
|
24
|
+
*/
|
|
25
|
+
export type IconName = 'arrow-right' | 'arrow-up-right' | 'plus' | 'minus' | 'close' | 'menu';
|
|
26
|
+
export type IconSize = 's' | 'm' | 'l';
|
|
27
|
+
export type IconProps = {
|
|
28
|
+
name: IconName;
|
|
29
|
+
size?: IconSize;
|
|
30
|
+
/** Nimi ruudunlukijalle. Ilman tätä ikoni on koriste (aria-hidden). */
|
|
31
|
+
label?: string;
|
|
32
|
+
className?: string;
|
|
33
|
+
};
|
|
34
|
+
export default function Icon({ name, size, label, className }: IconProps): import("react/jsx-runtime").JSX.Element;
|
|
35
|
+
/** Ikonien nimet järjestyksessä — dokumentaatiosivut lukevat tästä. */
|
|
36
|
+
export declare const ICON_NAMES: IconName[];
|
package/Icon.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/* Polut 16×16-ruudukolla. Yksi merkki = yksi taulukollinen `d`-polkuja,
|
|
3
|
+
jotta jokainen viiva saa saman hiusviivan eikä täyttöä tarvita. */
|
|
4
|
+
const PATHS = {
|
|
5
|
+
'arrow-right': ['M2.5 8.5 H13.5', 'M9.5 4.5 L13.5 8.5 L9.5 12.5'],
|
|
6
|
+
'arrow-up-right': ['M3.5 12.5 L12.5 3.5', 'M5.5 3.5 H12.5 V10.5'],
|
|
7
|
+
plus: ['M8.5 2.5 V14.5', 'M2.5 8.5 H14.5'],
|
|
8
|
+
minus: ['M2.5 8.5 H14.5'],
|
|
9
|
+
close: ['M3.5 3.5 L12.5 12.5', 'M12.5 3.5 L3.5 12.5'],
|
|
10
|
+
menu: ['M1 5.5 H15', 'M1 10.5 H15'],
|
|
11
|
+
};
|
|
12
|
+
export default function Icon({ name, size = 'm', label, className }) {
|
|
13
|
+
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))) }));
|
|
14
|
+
}
|
|
15
|
+
/** Ikonien nimet järjestyksessä — dokumentaatiosivut lukevat tästä. */
|
|
16
|
+
export const ICON_NAMES = Object.keys(PATHS);
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Veli-Matti Pokela
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/ListRow.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ElementType } from 'react';
|
|
2
|
+
export type ListRowProps = {
|
|
3
|
+
/** Työn nimi. */
|
|
4
|
+
title: string;
|
|
5
|
+
/** Järjestysnumero vasempaan laitaan. Valinnainen. */
|
|
6
|
+
number?: string;
|
|
7
|
+
/** Yhden lauseen kuvaus. Valinnainen. */
|
|
8
|
+
description?: string;
|
|
9
|
+
/** Rooli tai vuosi, oikeaan laitaan metana. Valinnainen. */
|
|
10
|
+
meta?: string;
|
|
11
|
+
/** `m` on isompi otsikko ja väljempi rytmi — työlistan kärkirivit. */
|
|
12
|
+
size?: 's' | 'm';
|
|
13
|
+
href: string;
|
|
14
|
+
/** Lisäluokka, esim. `invert` kun tila näytetään staattisena. */
|
|
15
|
+
className?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Linkkikomponentti. Oletus on tavallinen `<a>`, jotta komponentti ei
|
|
18
|
+
* ole sidottu mihinkään reititykseen — se on ehto sille että tämä voi
|
|
19
|
+
* elää npm-paketissa. Next.js-sovellus antaa tähän `next/link`:n ja
|
|
20
|
+
* saa reitityksen takaisin.
|
|
21
|
+
*/
|
|
22
|
+
as?: ElementType;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Listarivi — sivuston tunnuskomponentti.
|
|
26
|
+
*
|
|
27
|
+
* Yksi rivi, kolme käyttöä. Sivustolla oli pitkään kolme lähes
|
|
28
|
+
* samanlaista riviä: etusivun listarivi, työlistan kärkirivi ja
|
|
29
|
+
* aiemman työn avautuva rivi. Niillä oli sama viiva, sama käännös ja
|
|
30
|
+
* sama sisältö mutta kolme eri toteutusta, jotka ehtivät jo ajautua
|
|
31
|
+
* erilleen. Nyt runko on yksi ja erot ovat modifioijia.
|
|
32
|
+
*
|
|
33
|
+
* Avautuva rivi on <Accordion>: se on <button> eikä linkki, joten se
|
|
34
|
+
* ei mahdu tähän propsiksi ilman että komponentista tulee kaksi
|
|
35
|
+
* komponenttia yhdessä. Se käyttää samaa `.list-row`-runkoa.
|
|
36
|
+
*
|
|
37
|
+
* Hover kääntää pinnan mustaksi. Logiikka on kokonaan CSS:ssä
|
|
38
|
+
* (styles/components/list-row.css), joten komponentti ei tiedä
|
|
39
|
+
* tiloista mitään.
|
|
40
|
+
*
|
|
41
|
+
* `.bleed` hoitaa vaakapaddingin, jotta rivi ulottuu reunaan mutta
|
|
42
|
+
* teksti pysyy .page:n linjassa.
|
|
43
|
+
*
|
|
44
|
+
* Linkki tulee `as`-propsina eikä tuontina. Siksi komponentti ei tiedä
|
|
45
|
+
* Next.js:stä mitään ja kelpaa sellaisenaan mihin tahansa projektiin.
|
|
46
|
+
*/
|
|
47
|
+
export default function ListRow({ title, number, description, meta, size, href, className, as: Link, }: ListRowProps): import("react/jsx-runtime").JSX.Element;
|
package/ListRow.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Listarivi — sivuston tunnuskomponentti.
|
|
4
|
+
*
|
|
5
|
+
* Yksi rivi, kolme käyttöä. Sivustolla oli pitkään kolme lähes
|
|
6
|
+
* samanlaista riviä: etusivun listarivi, työlistan kärkirivi ja
|
|
7
|
+
* aiemman työn avautuva rivi. Niillä oli sama viiva, sama käännös ja
|
|
8
|
+
* sama sisältö mutta kolme eri toteutusta, jotka ehtivät jo ajautua
|
|
9
|
+
* erilleen. Nyt runko on yksi ja erot ovat modifioijia.
|
|
10
|
+
*
|
|
11
|
+
* Avautuva rivi on <Accordion>: se on <button> eikä linkki, joten se
|
|
12
|
+
* ei mahdu tähän propsiksi ilman että komponentista tulee kaksi
|
|
13
|
+
* komponenttia yhdessä. Se käyttää samaa `.list-row`-runkoa.
|
|
14
|
+
*
|
|
15
|
+
* Hover kääntää pinnan mustaksi. Logiikka on kokonaan CSS:ssä
|
|
16
|
+
* (styles/components/list-row.css), joten komponentti ei tiedä
|
|
17
|
+
* tiloista mitään.
|
|
18
|
+
*
|
|
19
|
+
* `.bleed` hoitaa vaakapaddingin, jotta rivi ulottuu reunaan mutta
|
|
20
|
+
* teksti pysyy .page:n linjassa.
|
|
21
|
+
*
|
|
22
|
+
* Linkki tulee `as`-propsina eikä tuontina. Siksi komponentti ei tiedä
|
|
23
|
+
* Next.js:stä mitään ja kelpaa sellaisenaan mihin tahansa projektiin.
|
|
24
|
+
*/
|
|
25
|
+
export default function ListRow({ title, number, description, meta, size = 's', href, className, as: Link = 'a', }) {
|
|
26
|
+
return (_jsxs(Link, { href: href, className: [
|
|
27
|
+
'list-row bleed',
|
|
28
|
+
number ? 'list-row--numbered' : null,
|
|
29
|
+
size === 'm' ? 'list-row--m' : null,
|
|
30
|
+
className,
|
|
31
|
+
]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(' '), children: [number ? _jsx("span", { className: "meta list-row__number", children: number }) : null, _jsxs("span", { className: "list-row__main", children: [_jsx("span", { className: "list-row__title", children: title }), description ? _jsx("span", { className: "list-row__description", children: description }) : null] }), meta ? _jsx("span", { className: "meta list-row__meta", children: meta }) : null] }));
|
|
34
|
+
}
|
package/README.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @pokela/components
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
React-komponentit: **Pokela Design System**, versio 1.2.0.
|
|
4
|
+
|
|
5
|
+
> Generoitu hakemistosta `components/` — samoista tiedostoista joita sivusto
|
|
6
|
+
> ajaa. Älä muokkaa käsin: muutokset tehdään lähteeseen ja paketti
|
|
7
|
+
> rakennetaan uudelleen komennolla `npm run paketti`.
|
|
8
|
+
|
|
9
|
+
## Käyttö
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { ListRow, Icon, Accordion, Grid, Col, Reveal } from '@pokela/components';
|
|
13
|
+
import '@pokela/components/styles.css';
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`styles.css` tuo myös `@pokela/tokens`:n muuttujat, joten erillistä tuontia ei
|
|
17
|
+
tarvita.
|
|
18
|
+
|
|
19
|
+
## Komponentit
|
|
20
|
+
|
|
21
|
+
- `Icon`
|
|
22
|
+
- `ListRow`
|
|
23
|
+
- `Accordion`
|
|
24
|
+
- `Grid`
|
|
25
|
+
- `Reveal`
|
|
26
|
+
- `ThemeScript`
|
|
27
|
+
- `ThemeToggle`
|
|
28
|
+
|
|
29
|
+
### Linkit
|
|
30
|
+
|
|
31
|
+
`ListRow` ei ole sidottu mihinkään reititykseen. Oletuksena se renderöi
|
|
32
|
+
tavallisen `<a>`:n; Next.js-sovellus antaa oman linkkinsä propsina:
|
|
33
|
+
|
|
34
|
+
```jsx
|
|
35
|
+
import Link from 'next/link';
|
|
36
|
+
|
|
37
|
+
<ListRow as={Link} title="Työn nimi" href="/tyot/esimerkki" />
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Mitä paketissa EI ole
|
|
41
|
+
|
|
42
|
+
- **Media, LogoRow, CaseText, CaseBlock, CaseBlocks, CaseBlockDerived, ListRowShowcase** — lukevat sivuston omaa sisältöä (`@/content`, `@/lib`)
|
|
43
|
+
- **Nav, Footer** — tarvitsevat sivuston sanakirjan ja reitityksen
|
|
44
|
+
- **BeforeAfter** — rakentuu Median varaan, ja Media lukee kuvamanifestia
|
|
45
|
+
- **ComponentView, PrintCv** — ovat riippumattomia mutta sivuston ominaisuuksia — lähdekoodinäkymä ja CV:n tulostusasu — eivät design systemin osia
|
|
46
|
+
|
|
47
|
+
## Tyylit
|
|
48
|
+
|
|
49
|
+
- `styles/base.css`
|
|
50
|
+
- `styles/icon.css`
|
|
51
|
+
- `styles/list-row.css`
|
|
52
|
+
- `styles/accordion.css`
|
|
53
|
+
|
|
54
|
+
Luokat ovat globaaleja, eivät CSS-moduuleja. Se on tarkoituksellista: sama
|
|
55
|
+
luokka on myös Figmassa komponentin nimenä, ja hajautettu nimi katkaisisi
|
|
56
|
+
yhteyden.
|
|
57
|
+
|
|
58
|
+
## Lisenssi
|
|
59
|
+
|
|
60
|
+
MIT. Koskee tätä pakettia, ei sitä repoa josta se on generoitu.
|
package/Reveal.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Reveal: 12px nousu + häivytys kerran, kun lohko tulee näkyviin.
|
|
4
|
+
*
|
|
5
|
+
* prefers-reduced-motion hoidetaan CSS:ssä (.reveal-säännöt ovat
|
|
6
|
+
* no-preference-median sisällä), joten tämä ei tarvitse omaa
|
|
7
|
+
* haaraa — luokka vain ei tee mitään jos liikettä ei haluta.
|
|
8
|
+
*/
|
|
9
|
+
export default function Reveal({ children }: {
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
}): import("react/jsx-runtime").JSX.Element;
|
package/Reveal.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* Reveal: 12px nousu + häivytys kerran, kun lohko tulee näkyviin.
|
|
6
|
+
*
|
|
7
|
+
* prefers-reduced-motion hoidetaan CSS:ssä (.reveal-säännöt ovat
|
|
8
|
+
* no-preference-median sisällä), joten tämä ei tarvitse omaa
|
|
9
|
+
* haaraa — luokka vain ei tee mitään jos liikettä ei haluta.
|
|
10
|
+
*/
|
|
11
|
+
export default function Reveal({ children }) {
|
|
12
|
+
const ref = useRef(null);
|
|
13
|
+
const [shown, setShown] = useState(false);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const node = ref.current;
|
|
16
|
+
if (!node || shown)
|
|
17
|
+
return;
|
|
18
|
+
if (!('IntersectionObserver' in window)) {
|
|
19
|
+
setShown(true);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const io = new IntersectionObserver(([entry]) => {
|
|
23
|
+
if (entry.isIntersecting) {
|
|
24
|
+
setShown(true);
|
|
25
|
+
io.disconnect();
|
|
26
|
+
}
|
|
27
|
+
}, { rootMargin: '0px 0px -10% 0px' });
|
|
28
|
+
io.observe(node);
|
|
29
|
+
return () => io.disconnect();
|
|
30
|
+
}, [shown]);
|
|
31
|
+
return (_jsx("div", { ref: ref, className: shown ? 'reveal is-in' : 'reveal', children: children }));
|
|
32
|
+
}
|
package/ThemeScript.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teeman palautus ennen ensimmäistä maalausta.
|
|
3
|
+
*
|
|
4
|
+
* Ilman tätä sivu välähtäisi vaaleana ennen kuin tallennettu tumma
|
|
5
|
+
* teema ehtii päälle. Skripti on tarkoituksella pieni ja synkroninen,
|
|
6
|
+
* ja se ajetaan <head>issä ennen tyylejä.
|
|
7
|
+
*
|
|
8
|
+
* Ilman tallennettua valintaa data-themeä ei aseteta lainkaan, jolloin
|
|
9
|
+
* tokens.css:n `color-scheme: light dark` seuraa käyttöjärjestelmää.
|
|
10
|
+
*/
|
|
11
|
+
export declare const THEME_STORAGE_KEY = "pokela-theme";
|
|
12
|
+
export default function ThemeScript(): import("react/jsx-runtime").JSX.Element;
|
package/ThemeScript.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Teeman palautus ennen ensimmäistä maalausta.
|
|
4
|
+
*
|
|
5
|
+
* Ilman tätä sivu välähtäisi vaaleana ennen kuin tallennettu tumma
|
|
6
|
+
* teema ehtii päälle. Skripti on tarkoituksella pieni ja synkroninen,
|
|
7
|
+
* ja se ajetaan <head>issä ennen tyylejä.
|
|
8
|
+
*
|
|
9
|
+
* Ilman tallennettua valintaa data-themeä ei aseteta lainkaan, jolloin
|
|
10
|
+
* tokens.css:n `color-scheme: light dark` seuraa käyttöjärjestelmää.
|
|
11
|
+
*/
|
|
12
|
+
export const THEME_STORAGE_KEY = 'pokela-theme';
|
|
13
|
+
const script = `(function(){try{var t=localStorage.getItem(${JSON.stringify(THEME_STORAGE_KEY)});if(t==='light'||t==='dark'){document.documentElement.setAttribute('data-theme',t)}}catch(e){}})()`;
|
|
14
|
+
export default function ThemeScript() {
|
|
15
|
+
return _jsx("script", { dangerouslySetInnerHTML: { __html: script } });
|
|
16
|
+
}
|
package/ThemeToggle.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Teemanvaihto tekstinappina — ei ikonia, ei väriä.
|
|
3
|
+
* Kierto: järjestelmä → vaalea → tumma → järjestelmä.
|
|
4
|
+
*
|
|
5
|
+
* `data-theme` puuttuu kun valinta on "järjestelmä", jolloin
|
|
6
|
+
* tokens.css:n light-dark() seuraa prefers-color-schemeä.
|
|
7
|
+
*/
|
|
8
|
+
export default function ThemeToggle({ labels, }: {
|
|
9
|
+
labels: {
|
|
10
|
+
theme: string;
|
|
11
|
+
light: string;
|
|
12
|
+
dark: string;
|
|
13
|
+
system: string;
|
|
14
|
+
};
|
|
15
|
+
}): import("react/jsx-runtime").JSX.Element;
|
package/ThemeToggle.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { THEME_STORAGE_KEY } from './ThemeScript';
|
|
5
|
+
/**
|
|
6
|
+
* Teemanvaihto tekstinappina — ei ikonia, ei väriä.
|
|
7
|
+
* Kierto: järjestelmä → vaalea → tumma → järjestelmä.
|
|
8
|
+
*
|
|
9
|
+
* `data-theme` puuttuu kun valinta on "järjestelmä", jolloin
|
|
10
|
+
* tokens.css:n light-dark() seuraa prefers-color-schemeä.
|
|
11
|
+
*/
|
|
12
|
+
export default function ThemeToggle({ labels, }) {
|
|
13
|
+
const [theme, setTheme] = useState('system');
|
|
14
|
+
const [mounted, setMounted] = useState(false);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setMounted(true);
|
|
17
|
+
try {
|
|
18
|
+
const stored = localStorage.getItem(THEME_STORAGE_KEY);
|
|
19
|
+
if (stored === 'light' || stored === 'dark')
|
|
20
|
+
setTheme(stored);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
/* private mode — pidetään järjestelmän teema */
|
|
24
|
+
}
|
|
25
|
+
}, []);
|
|
26
|
+
function apply(next) {
|
|
27
|
+
setTheme(next);
|
|
28
|
+
const root = document.documentElement;
|
|
29
|
+
if (next === 'system')
|
|
30
|
+
root.removeAttribute('data-theme');
|
|
31
|
+
else
|
|
32
|
+
root.setAttribute('data-theme', next);
|
|
33
|
+
try {
|
|
34
|
+
if (next === 'system')
|
|
35
|
+
localStorage.removeItem(THEME_STORAGE_KEY);
|
|
36
|
+
else
|
|
37
|
+
localStorage.setItem(THEME_STORAGE_KEY, next);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
/* valinta jää tälle sivulataukselle */
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const order = ['system', 'light', 'dark'];
|
|
44
|
+
const label = { system: labels.system, light: labels.light, dark: labels.dark }[theme];
|
|
45
|
+
return (_jsx("button", { type: "button", className: "meta meta--s toggle", onClick: () => apply(order[(order.indexOf(theme) + 1) % order.length]), "aria-label": `${labels.theme}: ${label}`,
|
|
46
|
+
/* Ennen hydraatiota näytetään neutraali merkintä, jottei
|
|
47
|
+
palvelimen ja selaimen tila eroa toisistaan. */
|
|
48
|
+
suppressHydrationWarning: true, children: mounted ? label : labels.theme }));
|
|
49
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { default as Icon, ICON_NAMES } from './Icon.js';
|
|
2
|
+
export type { IconName, IconSize, IconProps } from './Icon.js';
|
|
3
|
+
export { default as ListRow } from './ListRow.js';
|
|
4
|
+
export type { ListRowProps } from './ListRow.js';
|
|
5
|
+
export { Accordion, useAccordionGroup } from './Accordion.js';
|
|
6
|
+
export type { AccordionProps } from './Accordion.js';
|
|
7
|
+
export { Grid, Col } from './Grid.js';
|
|
8
|
+
export { default as Reveal } from './Reveal.js';
|
|
9
|
+
export { default as ThemeScript, THEME_STORAGE_KEY } from './ThemeScript.js';
|
|
10
|
+
export { default as ThemeToggle } from './ThemeToggle.js';
|
package/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* @pokela/components — kokooma. Generoitu, ks. scripts/paketti-komponentit.mjs. */
|
|
2
|
+
export { default as Icon, ICON_NAMES } from './Icon.js';
|
|
3
|
+
export { default as ListRow } from './ListRow.js';
|
|
4
|
+
export { Accordion, useAccordionGroup } from './Accordion.js';
|
|
5
|
+
export { Grid, Col } from './Grid.js';
|
|
6
|
+
export { default as Reveal } from './Reveal.js';
|
|
7
|
+
export { default as ThemeScript, THEME_STORAGE_KEY } from './ThemeScript.js';
|
|
8
|
+
export { default as ThemeToggle } from './ThemeToggle.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pokela/components",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "React-komponentit: Pokela Design System. Generoitu tiedostoista components/, ei käsin ylläpidetty.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Veli-Matti Pokela",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": [
|
|
9
|
+
"*.css"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./index.d.ts",
|
|
14
|
+
"import": "./index.js"
|
|
15
|
+
},
|
|
16
|
+
"./styles.css": "./styles.css",
|
|
17
|
+
"./styles/*": "./styles/*"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"*.js",
|
|
21
|
+
"*.d.ts",
|
|
22
|
+
"styles.css",
|
|
23
|
+
"styles/",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@pokela/tokens": "^1.2.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"react": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/VeliMattiPokela/pokela.fi.git",
|
|
36
|
+
"directory": "packages/components"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/* Avautuva rivi (haitari). Runko on listarivi, tässä on paneeli. */
|
|
2
|
+
|
|
3
|
+
/* Viivat tulevat rivistä (.list-row), ei käärestä — muuten
|
|
4
|
+
yläviivoja olisi kaksi päällekkäin. Sulkeva viiva on
|
|
5
|
+
list-row.css:ssä sääntönä `.list-rows > *:last-child > .list-row`. */
|
|
6
|
+
|
|
7
|
+
/* Rivin ulkoasu tulee list-row.css:stä (.list-row--expandable).
|
|
8
|
+
Täällä on vain se mikä on tälle komponentille omaa: paneeli ja
|
|
9
|
+
sen sisältö. */
|
|
10
|
+
|
|
11
|
+
.accordion__panel { padding-block: var(--space-40) var(--space-56); }
|
|
12
|
+
|
|
13
|
+
/* ---- avautuva sisältö ------------------------------------------- */
|
|
14
|
+
|
|
15
|
+
.detail { display: flex; flex-direction: column; gap: var(--space-56); }
|
|
16
|
+
|
|
17
|
+
.detail__head { display: flex; flex-direction: column; gap: var(--space-20); }
|
|
18
|
+
.detail__intro { color: var(--ink-muted); }
|
|
19
|
+
|
|
20
|
+
.detail__role { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-32); }
|
|
21
|
+
@media (min-width: 900px) { .detail__role { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-40); } }
|
|
22
|
+
.detail__role-col { display: flex; flex-direction: column; gap: var(--space-12); }
|
|
23
|
+
.detail__role-title { margin: 0; font-size: var(--text-body-l); }
|
|
24
|
+
.detail__role-note { color: var(--ink-faint); margin: 0; }
|
|
25
|
+
|
|
26
|
+
.detail__list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-8); }
|
|
27
|
+
.detail__list li { font-size: var(--text-body-s); line-height: var(--lh-body-s); padding-left: var(--space-16); position: relative; }
|
|
28
|
+
.detail__list li::before { content: '—'; position: absolute; left: 0; color: var(--ink-faint); }
|
|
29
|
+
.detail__list--muted li { color: var(--ink-muted); }
|
|
30
|
+
|
|
31
|
+
.detail__sections { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-24); }
|
|
32
|
+
@media (min-width: 900px) { .detail__sections { grid-template-columns: 200px minmax(0, 1fr); gap: var(--space-40); } }
|
|
33
|
+
.detail__sections-body { display: flex; flex-direction: column; }
|
|
34
|
+
.detail__section { padding-block: var(--space-24); border-top: var(--hairline) solid var(--line); }
|
|
35
|
+
.detail__section:first-child { border-top: 0; padding-top: 0; }
|
|
36
|
+
.detail__section-title { margin: 0 0 var(--space-8); font-size: var(--text-body-l); font-weight: var(--weight-500); }
|
|
37
|
+
.detail__section-body { margin: 0; color: var(--ink-muted); }
|
|
38
|
+
|
|
39
|
+
.detail__media { display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--space-24); }
|
|
40
|
+
@media (min-width: 900px) { .detail__media { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
|
41
|
+
.detail__media > .compare { grid-column: 1 / -1; }
|
package/styles/base.css
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/* Pokela — layout-primitiivit
|
|
2
|
+
------------------------------------------------------------------
|
|
3
|
+
Ei uusia värejä eikä kokoja: kaikki arvot tulevat tokens.css:stä.
|
|
4
|
+
Jokaisella tämän tiedoston luokalla on vastine Storybookissa.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
8
|
+
|
|
9
|
+
html { -webkit-text-size-adjust: 100%; }
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
margin: 0;
|
|
13
|
+
background: var(--paper);
|
|
14
|
+
color: var(--ink);
|
|
15
|
+
font-family: var(--font-ui);
|
|
16
|
+
font-size: var(--text-body);
|
|
17
|
+
line-height: var(--lh-body);
|
|
18
|
+
-webkit-font-smoothing: antialiased;
|
|
19
|
+
text-rendering: optimizeLegibility;
|
|
20
|
+
|
|
21
|
+
/* Bodoni Modasta on ladattu vain leikkaukset 400 ja 500. Jos jokin
|
|
22
|
+
pyytää lihavaa — esimerkiksi selaimen oletustyyli <h1>–<h6>:lle —
|
|
23
|
+
selain venyttäisi 400:n paksummaksi. Didonessa se tuhoaa juuri
|
|
24
|
+
sen mikä tekee siitä didonen: hiusviivojen ja tolppien
|
|
25
|
+
kontrastin. Kursiivin synteesi jätetään päälle, koska
|
|
26
|
+
koodilohkojen kommentit nojaavat siihen. */
|
|
27
|
+
font-synthesis-weight: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
img, svg, video { display: block; max-width: 100%; }
|
|
31
|
+
|
|
32
|
+
/* Focus: 2px musteviiva 2px offsetilla. Yksi sääntö, koko sivustolle. */
|
|
33
|
+
:focus-visible {
|
|
34
|
+
outline: var(--focus-width) solid var(--ink);
|
|
35
|
+
outline-offset: var(--focus-offset);
|
|
36
|
+
}
|
|
37
|
+
/* Käännetyllä pinnalla focus kääntyy mukana. */
|
|
38
|
+
.invert :focus-visible { outline-color: var(--invert-ink); }
|
|
39
|
+
|
|
40
|
+
/* ---- typografia ---------------------------------------------------- */
|
|
41
|
+
|
|
42
|
+
.display-xl, .display-l, .display-m, .display-s {
|
|
43
|
+
font-family: var(--font-display);
|
|
44
|
+
font-weight: var(--weight-400);
|
|
45
|
+
margin: 0;
|
|
46
|
+
text-wrap: balance;
|
|
47
|
+
}
|
|
48
|
+
.display-xl { font-size: var(--text-display-xl); line-height: var(--lh-display-xl); letter-spacing: var(--tr-display-xl); }
|
|
49
|
+
.display-l { font-size: var(--text-display-l); line-height: var(--lh-display-l); letter-spacing: var(--tr-display-l); }
|
|
50
|
+
.display-m { font-size: var(--text-display-m); line-height: var(--lh-display-m); letter-spacing: var(--tr-display-m); }
|
|
51
|
+
.display-s { font-size: var(--text-display-s); line-height: var(--lh-display-s); letter-spacing: var(--tr-display-s); }
|
|
52
|
+
|
|
53
|
+
.body-l { font-size: var(--text-body-l); line-height: var(--lh-body-l); margin: 0; }
|
|
54
|
+
.body-s { font-size: var(--text-body-s); line-height: var(--lh-body-s); margin: 0; }
|
|
55
|
+
|
|
56
|
+
/* Leipätekstin mitta. Ei koskaan yli 62 merkkiä. */
|
|
57
|
+
.measure { max-width: var(--measure); }
|
|
58
|
+
|
|
59
|
+
.meta {
|
|
60
|
+
/* Nollaus, koska .meta osuu myös <p>- ja <h2>-elementteihin. Ilman
|
|
61
|
+
tätä selaimen oletusmarginaali (1em = 11px) livahti mukaan
|
|
62
|
+
paikkoihin joissa väli tulee vanhemman gapista. */
|
|
63
|
+
margin: 0;
|
|
64
|
+
font-family: var(--font-ui);
|
|
65
|
+
font-size: var(--text-meta);
|
|
66
|
+
line-height: var(--lh-meta);
|
|
67
|
+
font-weight: var(--weight-600);
|
|
68
|
+
letter-spacing: var(--tr-meta);
|
|
69
|
+
text-transform: uppercase;
|
|
70
|
+
color: var(--ink-faint);
|
|
71
|
+
}
|
|
72
|
+
.meta--s { font-size: var(--text-meta-s); line-height: var(--lh-meta-s); letter-spacing: var(--tr-meta-s); }
|
|
73
|
+
.meta--ink { color: var(--ink); }
|
|
74
|
+
|
|
75
|
+
a { color: inherit; text-decoration: none; }
|
|
76
|
+
a:hover { text-decoration: underline; text-underline-offset: var(--space-4); }
|
|
77
|
+
|
|
78
|
+
/* ---- sivu ja grid -------------------------------------------------- */
|
|
79
|
+
|
|
80
|
+
.page {
|
|
81
|
+
max-width: var(--max-width);
|
|
82
|
+
margin-inline: auto;
|
|
83
|
+
padding-inline: var(--page-padding);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Täysleveä kaista joka hoitaa oman sivupaddinginsa.
|
|
87
|
+
Käytetään listariveille ja käännetyille palkeille — ne ulottuvat
|
|
88
|
+
reunaan, mutta sisältö pysyy samassa linjassa .page:n kanssa. */
|
|
89
|
+
.bleed {
|
|
90
|
+
width: 100%;
|
|
91
|
+
padding-inline: max(var(--page-padding), calc((100% - var(--max-width)) / 2 + var(--page-padding)));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.section { margin-block: var(--section-gap); }
|
|
95
|
+
|
|
96
|
+
/* 12-sarakkeinen grid: 4 → 8 → 12. Sarakemäärä tulee tokenista,
|
|
97
|
+
joten yksikään komponentti ei tiedä breakpointeista mitään. */
|
|
98
|
+
.grid {
|
|
99
|
+
display: grid;
|
|
100
|
+
grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
|
|
101
|
+
column-gap: var(--gutter);
|
|
102
|
+
row-gap: var(--space-32);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* Col: leveys ja aloitussarake per breakpoint.
|
|
106
|
+
Arvot annetaan custom-propertyinä (--span-base / --span-sm / --span-md),
|
|
107
|
+
jolloin JSX kertoo sijoittelun ja CSS vain toteuttaa sen. */
|
|
108
|
+
.col { grid-column: span var(--span-base) / span var(--span-base); }
|
|
109
|
+
@media (min-width: 600px) {
|
|
110
|
+
.col { grid-column: var(--start-sm, auto) / span var(--span-sm); }
|
|
111
|
+
}
|
|
112
|
+
@media (min-width: 900px) {
|
|
113
|
+
.col { grid-column: var(--start-md, auto) / span var(--span-md); }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.rule { border: 0; border-top: var(--hairline) solid var(--line); margin: 0; }
|
|
117
|
+
.rule--strong { border-top-color: var(--line-strong); }
|
|
118
|
+
|
|
119
|
+
/* Käännetty pinta. Ainoa tehoväline — ei värejä, ei varjoja. */
|
|
120
|
+
.invert {
|
|
121
|
+
background: var(--invert-surface);
|
|
122
|
+
color: var(--invert-ink);
|
|
123
|
+
}
|
|
124
|
+
.invert .meta { color: var(--invert-faint); }
|
|
125
|
+
|
|
126
|
+
/* ---- napit --------------------------------------------------------- */
|
|
127
|
+
|
|
128
|
+
.btn {
|
|
129
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
130
|
+
gap: var(--space-8);
|
|
131
|
+
font-family: var(--font-ui);
|
|
132
|
+
font-size: var(--text-body-s);
|
|
133
|
+
font-weight: var(--weight-500);
|
|
134
|
+
line-height: 1;
|
|
135
|
+
min-height: var(--tap-min);
|
|
136
|
+
padding: var(--space-12) var(--space-24);
|
|
137
|
+
border-radius: var(--radius);
|
|
138
|
+
border: var(--hairline) solid transparent;
|
|
139
|
+
background: none;
|
|
140
|
+
color: inherit;
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
text-decoration: none;
|
|
143
|
+
transition:
|
|
144
|
+
background var(--dur-fast) var(--ease-standard),
|
|
145
|
+
border-color var(--dur-fast) var(--ease-standard),
|
|
146
|
+
color var(--dur-fast) var(--ease-standard);
|
|
147
|
+
}
|
|
148
|
+
.btn:hover { text-decoration: none; }
|
|
149
|
+
|
|
150
|
+
/* Mobiilissa päätoiminto on täysleveä, 600px:stä sisällön mukainen. */
|
|
151
|
+
.btn--block { width: 100%; }
|
|
152
|
+
@media (min-width: 600px) { .btn--block { width: auto; } }
|
|
153
|
+
|
|
154
|
+
/* Hover = käännös. Reunaviiva tulee border-colorista, ei varjosta. */
|
|
155
|
+
.btn--primary { background: var(--ink); color: var(--paper); border-color: var(--ink); }
|
|
156
|
+
.btn--primary:hover { background: var(--paper); color: var(--ink); }
|
|
157
|
+
|
|
158
|
+
.btn--ghost { border-color: var(--ink); color: var(--ink); }
|
|
159
|
+
.btn--ghost:hover { background: var(--ink); color: var(--paper); }
|
|
160
|
+
|
|
161
|
+
.btn--text {
|
|
162
|
+
min-height: 0;
|
|
163
|
+
padding: 0 0 var(--space-8);
|
|
164
|
+
border: 0;
|
|
165
|
+
border-bottom: var(--hairline) solid currentColor;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Hiljainen kytkin: teemanvaihto ja vastaavat. Ei .btn-perheen jäsen —
|
|
169
|
+
typografia tulee .meta:sta ja nappimaisuus riisutaan kokonaan, joten
|
|
170
|
+
se ei kilpaile sivun oikeiden toimintojen kanssa. Kosketusalue on
|
|
171
|
+
silti täysi: padding kasvattaa sen ja yhtä suuri negatiivinen margin
|
|
172
|
+
kumoaa vaikutuksen taittoon.
|
|
173
|
+
|
|
174
|
+
Ei ollut navigaatiokohtainen, vaikka asui aiemmin nav.css:ssä nimellä
|
|
175
|
+
.nav__toggle. Nimi esti ThemeTogglen viennin komponenttipakettiin,
|
|
176
|
+
koska komponentti olisi tarvinnut Navin tyylit. */
|
|
177
|
+
.toggle {
|
|
178
|
+
background: none;
|
|
179
|
+
border: 0;
|
|
180
|
+
padding: var(--space-12);
|
|
181
|
+
margin: calc(var(--space-12) * -1);
|
|
182
|
+
min-width: var(--tap-min);
|
|
183
|
+
min-height: var(--tap-min);
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
transition: color var(--dur-fast) var(--ease-standard);
|
|
186
|
+
}
|
|
187
|
+
.toggle:hover { color: var(--ink); }
|
|
188
|
+
|
|
189
|
+
/* Tekstinapilla ei ole pintaa jota kääntää, joten hover on sama liike
|
|
190
|
+
kuin .case-linkillä: viiva paksunee. Padding kevenee saman verran
|
|
191
|
+
kuin viiva kasvaa, jotta napin korkeus ei muutu eikä ympärillä oleva
|
|
192
|
+
teksti nytkähdä osoittimen alla. */
|
|
193
|
+
.btn--text:hover {
|
|
194
|
+
border-bottom-width: var(--hairline-strong);
|
|
195
|
+
padding-bottom: calc(var(--space-8) - (var(--hairline-strong) - var(--hairline)));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.btn[disabled], .btn[aria-disabled='true'] {
|
|
199
|
+
color: var(--ink-faint);
|
|
200
|
+
border-color: var(--line);
|
|
201
|
+
background: none;
|
|
202
|
+
cursor: not-allowed;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* Kosketuslaitteella hoveria ei ole — käännös tulee :active-tilassa.
|
|
206
|
+
Tämä lohko on viimeisenä, joten se voittaa hover-säännöt. */
|
|
207
|
+
@media (hover: none) {
|
|
208
|
+
.btn--primary:hover { background: var(--ink); color: var(--paper); }
|
|
209
|
+
.btn--ghost:hover { background: none; color: var(--ink); }
|
|
210
|
+
.btn--text:hover {
|
|
211
|
+
border-bottom-width: var(--hairline);
|
|
212
|
+
padding-bottom: var(--space-8);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.btn--primary:active { background: var(--paper); color: var(--ink); }
|
|
216
|
+
.btn--ghost:active { background: var(--ink); color: var(--paper); }
|
|
217
|
+
.btn--text:active {
|
|
218
|
+
border-bottom-width: var(--hairline-strong);
|
|
219
|
+
padding-bottom: calc(var(--space-8) - (var(--hairline-strong) - var(--hairline)));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* ---- media (kuvapaikka) ----------------------------------------------------- */
|
|
224
|
+
|
|
225
|
+
.media {
|
|
226
|
+
position: relative;
|
|
227
|
+
background: repeating-linear-gradient(
|
|
228
|
+
135deg,
|
|
229
|
+
var(--placeholder-base) 0 12px,
|
|
230
|
+
var(--placeholder-stripe) 12px 24px
|
|
231
|
+
);
|
|
232
|
+
display: flex;
|
|
233
|
+
align-items: flex-end;
|
|
234
|
+
padding: var(--space-16);
|
|
235
|
+
}
|
|
236
|
+
@media (min-width: 600px) { .media { padding: var(--space-24); } }
|
|
237
|
+
|
|
238
|
+
/* Kun paikassa on kuva, raidoitus ja sisennys eivät ole tarpeen:
|
|
239
|
+
kuva täyttää laatikon reunasta reunaan. Sama .media-luokka säilyy,
|
|
240
|
+
jotta kuvasuhde tulee samasta paikasta kummassakin tilassa eikä
|
|
241
|
+
layout liiku kun kuva ilmestyy. */
|
|
242
|
+
.media--kuva {
|
|
243
|
+
display: block;
|
|
244
|
+
padding: 0;
|
|
245
|
+
background: var(--placeholder-base);
|
|
246
|
+
overflow: hidden;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/* Kuvasuhteet: mobiilissa pystympi, työpöydällä editoriaalinen.
|
|
250
|
+
21:9 hero on mobiilissa 4:5 — muuten kuvasta ei näe mitään. */
|
|
251
|
+
.media-hero { aspect-ratio: 4 / 5; }
|
|
252
|
+
.media-4-3 { aspect-ratio: 4 / 3; }
|
|
253
|
+
.media-4-5 { aspect-ratio: 4 / 5; }
|
|
254
|
+
.media-3-4 { aspect-ratio: 3 / 4; }
|
|
255
|
+
.media-1-1 { aspect-ratio: 1 / 1; }
|
|
256
|
+
@media (min-width: 600px) { .media-hero { aspect-ratio: 16 / 9; } }
|
|
257
|
+
@media (min-width: 900px) { .media-hero { aspect-ratio: 21 / 9; } }
|
|
258
|
+
|
|
259
|
+
.media-fill { width: 100%; height: 100%; object-fit: cover; }
|
|
260
|
+
|
|
261
|
+
/* Paikanvaraajan merkintä: järjestysnumero ja tunniste. Vain
|
|
262
|
+
kehityksessä (Media.tsx päättää), ja tarkoitus on että pudotettavan
|
|
263
|
+
tiedoston nimen voi lukea suoraan sivulta ilman terminaalia.
|
|
264
|
+
|
|
265
|
+
Tyyli on sivuston oma: numerot ovat täällä .meta-kokoisia vaimeita
|
|
266
|
+
merkintöjä (ks. ListRow, CV, artefaktilista), eivät
|
|
267
|
+
display-elementtejä. Iso lihava numero nurkassa luki sisältönä eikä
|
|
268
|
+
rakennustelineenä.
|
|
269
|
+
|
|
270
|
+
Numero täydellä musteella, tunniste vaimeana: numero on se jota
|
|
271
|
+
vilkaistaan, tunniste se joka kopioidaan. Tunniste ilman
|
|
272
|
+
versaalimuunnosta, koska se on kopioitava tiedostonimi. */
|
|
273
|
+
.media__teksti { display: flex; flex-direction: column; gap: var(--space-8); }
|
|
274
|
+
.media__merkinta { display: flex; align-items: baseline; gap: var(--space-12); flex-wrap: wrap; }
|
|
275
|
+
.media__numero { color: var(--ink); font-variant-numeric: tabular-nums; }
|
|
276
|
+
.media__tunnus { color: var(--ink-faint); text-transform: none; }
|
|
277
|
+
.media__muoto { color: var(--ink-faint); margin-inline-start: auto; text-transform: none; }
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
/* ---- reveal --------------------------------------------------------- */
|
|
281
|
+
|
|
282
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
283
|
+
.reveal { opacity: 0; transform: translateY(var(--reveal-shift)); }
|
|
284
|
+
.reveal.is-in {
|
|
285
|
+
opacity: 1;
|
|
286
|
+
transform: none;
|
|
287
|
+
transition:
|
|
288
|
+
opacity var(--dur-reveal) var(--ease-out),
|
|
289
|
+
transform var(--dur-reveal) var(--ease-out);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* ---- saavutettavuus -------------------------------------------------- */
|
|
294
|
+
|
|
295
|
+
.visually-hidden {
|
|
296
|
+
position: absolute; width: 1px; height: 1px;
|
|
297
|
+
padding: 0; margin: -1px; overflow: hidden;
|
|
298
|
+
clip-path: inset(50%); white-space: nowrap; border: 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.skip-link {
|
|
302
|
+
position: absolute; left: var(--space-16); top: var(--space-16);
|
|
303
|
+
z-index: 100;
|
|
304
|
+
transform: translateY(-200%);
|
|
305
|
+
background: var(--ink); color: var(--paper);
|
|
306
|
+
padding: var(--space-12) var(--space-16);
|
|
307
|
+
}
|
|
308
|
+
.skip-link:focus { transform: none; }
|
package/styles/icon.css
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/* Ikoni — systeemin oma piirretty merkki.
|
|
2
|
+
==================================================================
|
|
3
|
+
Ei kirjastoa. Geometria on components/Icon.tsx:ssä, tässä on se
|
|
4
|
+
mikä tekee merkistä osan systeemiä: hiusviiva, currentColor ja
|
|
5
|
+
kolme kokoa tokeneista.
|
|
6
|
+
|
|
7
|
+
`vector-effect: non-scaling-stroke` on tässä olennainen. Ilman sitä
|
|
8
|
+
viivanpaksuus skaalautuisi ruudukon mukana: 16-ruudukon 1 yksikköä
|
|
9
|
+
olisi 24 pikselin ikonissa 1,5 pikseliä ja 12 pikselin ikonissa
|
|
10
|
+
0,75. Kanssa sen `stroke-width` luetaan ruudun koordinaatistossa,
|
|
11
|
+
jolloin --hairline on hiusviiva joka koossa — juuri se mitä token
|
|
12
|
+
lupaa. */
|
|
13
|
+
|
|
14
|
+
.icon {
|
|
15
|
+
display: inline-block;
|
|
16
|
+
flex: none;
|
|
17
|
+
width: var(--icon-m);
|
|
18
|
+
height: var(--icon-m);
|
|
19
|
+
/* Ikoni istuu tekstin seassa, joten se lasketaan perusviivalle.
|
|
20
|
+
`vertical-align: middle` keskittäisi sen x-korkeuden puoliväliin
|
|
21
|
+
ja nostaisi merkin liian ylös metatekstin rinnalla. */
|
|
22
|
+
vertical-align: -0.125em;
|
|
23
|
+
color: inherit;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.icon path {
|
|
27
|
+
fill: none;
|
|
28
|
+
stroke: currentColor;
|
|
29
|
+
stroke-width: var(--hairline);
|
|
30
|
+
/* Tylpät päätteet ja terävät kulmat, koska --radius on 0.
|
|
31
|
+
Nämä ovat SVG:n oletukset — ne on kirjoitettu näkyviin, koska ne
|
|
32
|
+
ovat systeemin päätös eivätkä sattuma. */
|
|
33
|
+
stroke-linecap: butt;
|
|
34
|
+
stroke-linejoin: miter;
|
|
35
|
+
vector-effect: non-scaling-stroke;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.icon--s { width: var(--icon-s); height: var(--icon-s); }
|
|
39
|
+
.icon--m { width: var(--icon-m); height: var(--icon-m); }
|
|
40
|
+
.icon--l { width: var(--icon-l); height: var(--icon-l); }
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/* Listarivi — sivuston tunnuskomponentti.
|
|
2
|
+
==================================================================
|
|
3
|
+
Yksi rivi, kolme käyttöä. Sivustolla oli pitkään kolme lähes
|
|
4
|
+
samanlaista riviä — listarivi, työlistan kärkirivi ja avautuva
|
|
5
|
+
rivi — joilla oli sama viiva, sama käännös ja sama sisältö, mutta
|
|
6
|
+
kolme eri toteutusta. Nyt runko on yksi ja erot ovat modifioijia:
|
|
7
|
+
|
|
8
|
+
.list-row nimi + kuvaus + rooli, linkkinä
|
|
9
|
+
.list-row--numbered numerosarake vasemmalle
|
|
10
|
+
.list-row--m isompi otsikko ja väljempi rytmi
|
|
11
|
+
.list-row--expandable + / − -osoitin oikeaan laitaan
|
|
12
|
+
|
|
13
|
+
Vaakapadding tulee .bleed-utilitystä, ei tästä: muuten rivi saa
|
|
14
|
+
sivupaddingin kahdesti .page-käärön sisällä.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/* ---- runko --------------------------------------------------------- */
|
|
18
|
+
|
|
19
|
+
.list-row {
|
|
20
|
+
display: grid;
|
|
21
|
+
grid-template-columns: minmax(0, 1fr);
|
|
22
|
+
grid-template-areas:
|
|
23
|
+
'main'
|
|
24
|
+
'meta';
|
|
25
|
+
align-items: baseline;
|
|
26
|
+
gap: var(--space-8) var(--space-16);
|
|
27
|
+
width: 100%;
|
|
28
|
+
padding-block: var(--space-20);
|
|
29
|
+
min-height: var(--tap-min);
|
|
30
|
+
border-top: var(--hairline) solid var(--line);
|
|
31
|
+
color: var(--ink);
|
|
32
|
+
text-align: left;
|
|
33
|
+
transition:
|
|
34
|
+
background var(--dur-base) var(--ease-standard),
|
|
35
|
+
color var(--dur-base) var(--ease-standard);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@media (min-width: 600px) {
|
|
39
|
+
.list-row {
|
|
40
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
41
|
+
grid-template-areas: 'main meta';
|
|
42
|
+
gap: var(--space-24);
|
|
43
|
+
padding-block: var(--space-28);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.list-rows > .list-row:last-child,
|
|
48
|
+
.list-rows > *:last-child > .list-row {
|
|
49
|
+
border-bottom: var(--hairline) solid var(--line);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ---- osat ---------------------------------------------------------- */
|
|
53
|
+
|
|
54
|
+
.list-row__number { grid-area: num; }
|
|
55
|
+
.list-row__meta { grid-area: meta; }
|
|
56
|
+
|
|
57
|
+
/* Nimi + kuvaus omana pinonaan, jotta rooli voi asettua oikeaan
|
|
58
|
+
laitaan nimen perusviivalle. */
|
|
59
|
+
.list-row__main {
|
|
60
|
+
grid-area: main;
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: var(--space-4);
|
|
64
|
+
min-width: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.list-row__title {
|
|
68
|
+
font-family: var(--font-display);
|
|
69
|
+
font-size: var(--text-display-s);
|
|
70
|
+
line-height: var(--lh-display-s);
|
|
71
|
+
letter-spacing: var(--tr-display-s);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.list-row__description {
|
|
75
|
+
font-size: var(--text-body-s);
|
|
76
|
+
line-height: var(--lh-body-s);
|
|
77
|
+
color: var(--ink-muted);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ---- numerollinen -------------------------------------------------- */
|
|
81
|
+
|
|
82
|
+
.list-row--numbered {
|
|
83
|
+
grid-template-columns: var(--space-36) minmax(0, 1fr);
|
|
84
|
+
/* Roolilla on oma alue myös mobiilissa. Ilman sitä `grid-area`
|
|
85
|
+
ei osu mihinkään, selain sijoittaa sen automaattisesti ja luo
|
|
86
|
+
implisiittisen sarakkeen — pääsisältö kutistui 86 pikseliin. */
|
|
87
|
+
grid-template-areas:
|
|
88
|
+
'num main'
|
|
89
|
+
'. meta';
|
|
90
|
+
}
|
|
91
|
+
@media (min-width: 600px) {
|
|
92
|
+
.list-row--numbered {
|
|
93
|
+
grid-template-columns: var(--space-56) minmax(0, 1fr) auto;
|
|
94
|
+
grid-template-areas: 'num main meta';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* ---- iso koko ------------------------------------------------------ */
|
|
99
|
+
|
|
100
|
+
.list-row--m { padding-block: var(--space-24); }
|
|
101
|
+
@media (min-width: 600px) {
|
|
102
|
+
.list-row--m { padding-block: var(--space-32); }
|
|
103
|
+
.list-row--m.list-row--numbered { grid-template-columns: var(--space-56) minmax(0, 1fr) 260px; }
|
|
104
|
+
}
|
|
105
|
+
.list-row--m .list-row__main { gap: var(--space-12); }
|
|
106
|
+
.list-row--m .list-row__title {
|
|
107
|
+
font-size: var(--text-display-m);
|
|
108
|
+
line-height: var(--lh-display-m);
|
|
109
|
+
letter-spacing: var(--tr-display-m);
|
|
110
|
+
}
|
|
111
|
+
.list-row--m .list-row__description { max-width: var(--measure); }
|
|
112
|
+
|
|
113
|
+
/* ---- avautuva ------------------------------------------------------
|
|
114
|
+
Merkki on systeemin oma ikoni (plus / minus), ei tekstimerkki eikä
|
|
115
|
+
kirjaston ikoni — ks. components/Icon.tsx. Nimi oli välillä `sign`
|
|
116
|
+
ja `indicator`; molemmat olivat kiertoilmauksia sille ettei näitä
|
|
117
|
+
merkkejä haluttu kutsua ikoneiksi. Nyt ne ovat ikoneita, ja slotin
|
|
118
|
+
nimi on `icon`. */
|
|
119
|
+
|
|
120
|
+
.list-row--expandable {
|
|
121
|
+
grid-template-columns: var(--space-36) minmax(0, 1fr) auto;
|
|
122
|
+
grid-template-areas: 'num main icon';
|
|
123
|
+
background: none;
|
|
124
|
+
border-left: 0;
|
|
125
|
+
border-right: 0;
|
|
126
|
+
border-bottom: 0;
|
|
127
|
+
font: inherit;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
}
|
|
130
|
+
/* Mobiilissa merkki vie tilan, joten rooli jää pois. */
|
|
131
|
+
.list-row--expandable .list-row__meta { display: none; }
|
|
132
|
+
|
|
133
|
+
@media (min-width: 600px) {
|
|
134
|
+
.list-row--expandable {
|
|
135
|
+
grid-template-columns: var(--space-56) minmax(0, 1fr) auto var(--space-24);
|
|
136
|
+
grid-template-areas: 'num main meta icon';
|
|
137
|
+
}
|
|
138
|
+
.list-row--expandable .list-row__meta { display: block; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.list-row__icon {
|
|
142
|
+
grid-area: icon;
|
|
143
|
+
/* Ei align-selfiä: ikoni kohdistuu rivin perusviivalle kuten numero
|
|
144
|
+
ja meta. SVG:llä ei ole perusviivaa, joten selain käyttää sen
|
|
145
|
+
alareunaa — merkki istuu otsikon riville, ei kahden tekstirivin
|
|
146
|
+
pystykeskelle. Koko tulee .icon--l:stä. */
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* ---- käännös ------------------------------------------------------- */
|
|
150
|
+
|
|
151
|
+
/* `.invert` on mukana, jotta tila voidaan näyttää myös staattisena
|
|
152
|
+
(esim. dokumentaatiossa) ilman omaa sääntöä — muuten .list-row:n
|
|
153
|
+
oma `color` voittaisi utilityn samalla spesifisyydellä ja teksti
|
|
154
|
+
jäisi näkymättömiin. `aria-expanded` on avatun rivin sama tila. */
|
|
155
|
+
.list-row:hover,
|
|
156
|
+
.list-row:focus-visible,
|
|
157
|
+
.list-row.invert,
|
|
158
|
+
.list-row[aria-expanded='true'] {
|
|
159
|
+
background: var(--invert-surface);
|
|
160
|
+
color: var(--invert-ink);
|
|
161
|
+
text-decoration: none;
|
|
162
|
+
}
|
|
163
|
+
.list-row:hover .list-row__description,
|
|
164
|
+
.list-row:focus-visible .list-row__description,
|
|
165
|
+
.list-row.invert .list-row__description,
|
|
166
|
+
.list-row[aria-expanded='true'] .list-row__description,
|
|
167
|
+
.list-row:hover .meta,
|
|
168
|
+
.list-row:focus-visible .meta,
|
|
169
|
+
.list-row.invert .meta,
|
|
170
|
+
.list-row[aria-expanded='true'] .meta { color: var(--invert-faint); }
|
|
171
|
+
|
|
172
|
+
/* Focus-renkaan pitää näkyä käännetyllä pinnalla. */
|
|
173
|
+
.list-row:focus-visible {
|
|
174
|
+
outline-color: var(--invert-ink);
|
|
175
|
+
outline-offset: calc(var(--focus-offset) * -2);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* Sisarrivit vaimennetaan hoverin aikana. */
|
|
179
|
+
.list-rows:hover .list-row:not(:hover) { color: var(--ink-faint); }
|
|
180
|
+
.list-rows:hover .list-row:not(:hover) .list-row__description { color: var(--ink-faint); }
|
|
181
|
+
|
|
182
|
+
/* Kosketus: ei hoveria, käännös :active-tilassa. Viimeisenä = voittaa. */
|
|
183
|
+
@media (hover: none) {
|
|
184
|
+
.list-row:hover {
|
|
185
|
+
background: transparent;
|
|
186
|
+
color: var(--ink);
|
|
187
|
+
}
|
|
188
|
+
.list-row:hover .list-row__description { color: var(--ink-muted); }
|
|
189
|
+
.list-row:hover .meta { color: var(--ink-faint); }
|
|
190
|
+
.list-rows:hover .list-row:not(:hover) { color: var(--ink); }
|
|
191
|
+
.list-rows:hover .list-row:not(:hover) .list-row__description { color: var(--ink-muted); }
|
|
192
|
+
|
|
193
|
+
.list-row:active,
|
|
194
|
+
.list-row[aria-expanded='true'] {
|
|
195
|
+
background: var(--invert-surface);
|
|
196
|
+
color: var(--invert-ink);
|
|
197
|
+
}
|
|
198
|
+
.list-row:active .list-row__description,
|
|
199
|
+
.list-row[aria-expanded='true'] .list-row__description,
|
|
200
|
+
.list-row:active .meta,
|
|
201
|
+
.list-row[aria-expanded='true'] .meta { color: var(--invert-faint); }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@media (prefers-reduced-motion: reduce) {
|
|
205
|
+
.list-row { transition: none; }
|
|
206
|
+
}
|