@pokela/components 2.5.2 → 2.6.1
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 +45 -0
- package/ExplodedView.js +172 -0
- package/README.md +4 -1
- package/guidelines/components.md +19 -0
- package/index.d.ts +2 -0
- package/index.js +1 -0
- package/package.json +2 -2
- package/styles/exploded-view.css +77 -0
- package/styles.css +1 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* ExplodedView: räjäytyskuva. Kerrokset levyinä 3D-tilassa, päällekkäin
|
|
4
|
+
* tai viuhkana, niin että näkee mistä valmis pinta koostuu.
|
|
5
|
+
*
|
|
6
|
+
* Kerrokset ovat oikeaa HTML:ää, eivät kuvia: teksti pysyy terävänä ja
|
|
7
|
+
* sisältö on samaa koodia kuin muualla. Ei WebGL:ää eikä kirjastoja,
|
|
8
|
+
* pelkät CSS 3D -muunnokset.
|
|
9
|
+
*
|
|
10
|
+
* - Osoitin kääntää pinoa ja erkanee kerroksia.
|
|
11
|
+
* - Elementit, joilla on sama `data-kohde`, syttyvät yhdessä: osoitin
|
|
12
|
+
* komponentin päällä näyttää sen jokaisessa kerroksessa.
|
|
13
|
+
* - Klikkaus nostaa kerroksen kerrallaan esiin ja lopuksi palauttaa
|
|
14
|
+
* koko pinon.
|
|
15
|
+
* - Tilassa `auki` vieritys kokoaa kerrokset yhdeksi suoraksi
|
|
16
|
+
* pinnaksi, kun kuva poistuu näkymän yläreunasta.
|
|
17
|
+
*
|
|
18
|
+
* Silmukka pyörii vain kun jokin liikkuu, ja pysähtyy kun kuva ei näy.
|
|
19
|
+
* Pysyvää liikettä ei ole, joten taukonappia ei tarvita (WCAG 2.2.2).
|
|
20
|
+
* Vähemmän liikettä pyytäneelle pino näkyy paikallaan alkutilassaan.
|
|
21
|
+
*
|
|
22
|
+
* Kuva on koriste, jonka sisältö on muualla sivulla: ruudunlukija lukee
|
|
23
|
+
* vain `label`-kuvauksen. Kerroksiin ei laiteta linkkejä eikä nappeja:
|
|
24
|
+
* ne olisivat näppäimistölle piilotettuja kohteita.
|
|
25
|
+
*
|
|
26
|
+
* `Tila` on Figmassa variantti: `auki` on pino viuhkana, `koottu`
|
|
27
|
+
* kerrokset yhdeksi pinnaksi koottuna. Ks. ExplodedView.figma.ts.
|
|
28
|
+
*/
|
|
29
|
+
export type ExplodedTila = 'auki' | 'koottu';
|
|
30
|
+
export type ExplodedLayer = {
|
|
31
|
+
/** Kerroksen nimi levyn yläpuolella, esim. "01 Tokenit". */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Mistä kerros tulee, esim. "tokens.json". Valinnainen. */
|
|
34
|
+
source?: string;
|
|
35
|
+
content: ReactNode;
|
|
36
|
+
};
|
|
37
|
+
export default function ExplodedView({ layers, label, tila, className, }: {
|
|
38
|
+
/** Alimmasta ylimpään. Ylin on valmis pinta. */
|
|
39
|
+
layers: ExplodedLayer[];
|
|
40
|
+
/** Kuvan kuvaus ruudunlukijalle. */
|
|
41
|
+
label: string;
|
|
42
|
+
/** Alkutila. `koottu` näyttää valmiin pinnan; klikkaus avaa pinon. */
|
|
43
|
+
tila?: ExplodedTila;
|
|
44
|
+
className?: string;
|
|
45
|
+
}): import("react/jsx-runtime").JSX.Element;
|
package/ExplodedView.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
const lerp = (a, b, k) => a + (b - a) * k;
|
|
5
|
+
export default function ExplodedView({ layers, label, tila = 'auki', className, }) {
|
|
6
|
+
const kehys = useRef(null);
|
|
7
|
+
const pino = useRef(null);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
const hero = kehys.current;
|
|
10
|
+
const el = pino.current;
|
|
11
|
+
if (!hero || !el)
|
|
12
|
+
return;
|
|
13
|
+
const kerrokset = [...el.querySelectorAll('.exploded__layer')];
|
|
14
|
+
const vahemman = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
15
|
+
const n = kerrokset.length;
|
|
16
|
+
/* Levyn mitat luetaan CSS:stä, jotta ne ovat yhdessä paikassa. */
|
|
17
|
+
const levyL = el.offsetWidth || 960;
|
|
18
|
+
const levyK = el.offsetHeight || 600;
|
|
19
|
+
/* x, y: osoitin -1…1 jousella. p: 1 = auki, 0 = koottu. */
|
|
20
|
+
const t = {
|
|
21
|
+
x: 0, y: 0, vx: 0, vy: 0, kx: 0, ky: 0,
|
|
22
|
+
p: tila === 'auki' ? 1 : 0,
|
|
23
|
+
auki: tila === 'auki',
|
|
24
|
+
vieritys: 1,
|
|
25
|
+
nakyy: true, raf: 0, edellinen: 0,
|
|
26
|
+
mitta: 1, kapea: false,
|
|
27
|
+
};
|
|
28
|
+
function koko() {
|
|
29
|
+
const r = hero.getBoundingClientRect();
|
|
30
|
+
t.kapea = r.width < 560;
|
|
31
|
+
/* Vino pino vie enemmän tilaa kuin levy itse. */
|
|
32
|
+
const tarveL = levyL * (t.kapea ? 1.3 : 2.1);
|
|
33
|
+
const tarveK = levyK * (t.kapea ? 2.6 : 2.1);
|
|
34
|
+
t.mitta = Math.min(r.width / tarveL, r.height / tarveK);
|
|
35
|
+
piirra();
|
|
36
|
+
}
|
|
37
|
+
function piirra() {
|
|
38
|
+
const p = t.p;
|
|
39
|
+
const rx = t.kapea ? lerp(0, 58, p) - t.y * 6 * p : lerp(0, 12, p) - t.y * 8 * p;
|
|
40
|
+
const ry = t.kapea ? t.x * 3 * p : lerp(0, 52, p) + t.x * 12 * p;
|
|
41
|
+
const rz = t.kapea ? t.x * 4 * p : lerp(0, -4, p);
|
|
42
|
+
const vali = (t.kapea ? 0.36 : 0.42) * levyL * p * (1 + 0.1 * Math.abs(t.x));
|
|
43
|
+
const r = hero.getBoundingClientRect();
|
|
44
|
+
const tayttava = Math.min(r.width / levyL, r.height / levyK) * 0.92;
|
|
45
|
+
el.style.setProperty('--rx', `${rx}deg`);
|
|
46
|
+
el.style.setProperty('--ry', `${ry}deg`);
|
|
47
|
+
el.style.setProperty('--rz', `${rz}deg`);
|
|
48
|
+
el.style.setProperty('--vali', `${vali}px`);
|
|
49
|
+
el.style.setProperty('--nosto', `${(t.kapea ? 0 : 0.05) * levyK * p}px`);
|
|
50
|
+
el.style.setProperty('--s', String(lerp(tayttava, t.mitta, p)));
|
|
51
|
+
/* Koottuna alemmat levyt piiloon, ettei niiden reuna näy ylimmän alta. */
|
|
52
|
+
kerrokset.forEach((k, i) => k.classList.toggle('exploded__layer--piilossa', p < 0.02 && i < n - 1));
|
|
53
|
+
}
|
|
54
|
+
const kohde = () => (t.auki ? t.vieritys : 0);
|
|
55
|
+
function vaihe(nyt) {
|
|
56
|
+
t.raf = 0;
|
|
57
|
+
const dt = Math.min(50, t.edellinen ? nyt - t.edellinen : 16.7);
|
|
58
|
+
t.edellinen = nyt;
|
|
59
|
+
const f = dt / 16.7;
|
|
60
|
+
const k = 0.07 * f;
|
|
61
|
+
t.vx = (t.vx + (t.kx - t.x) * k) * Math.pow(0.8, f);
|
|
62
|
+
t.vy = (t.vy + (t.ky - t.y) * k) * Math.pow(0.8, f);
|
|
63
|
+
t.x += t.vx * f;
|
|
64
|
+
t.y += t.vy * f;
|
|
65
|
+
t.p += (kohde() - t.p) * Math.min(1, 0.12 * f);
|
|
66
|
+
piirra();
|
|
67
|
+
const lepaa = Math.abs(t.vx) + Math.abs(t.vy) + Math.abs(t.kx - t.x) + Math.abs(t.ky - t.y) + Math.abs(kohde() - t.p) <
|
|
68
|
+
0.0005;
|
|
69
|
+
if (t.nakyy && !document.hidden && !lepaa)
|
|
70
|
+
t.raf = requestAnimationFrame(vaihe);
|
|
71
|
+
else
|
|
72
|
+
t.edellinen = 0;
|
|
73
|
+
}
|
|
74
|
+
const herata = () => {
|
|
75
|
+
if (!t.raf && t.nakyy)
|
|
76
|
+
t.raf = requestAnimationFrame(vaihe);
|
|
77
|
+
};
|
|
78
|
+
const asetu = () => {
|
|
79
|
+
t.p = kohde();
|
|
80
|
+
piirra();
|
|
81
|
+
};
|
|
82
|
+
const liiku = vahemman ? asetu : herata;
|
|
83
|
+
const osoitin = (e) => {
|
|
84
|
+
if (e.pointerType !== 'mouse' && e.pointerType !== 'pen')
|
|
85
|
+
return;
|
|
86
|
+
const r = hero.getBoundingClientRect();
|
|
87
|
+
if (e.clientY < r.top - r.height || e.clientY > r.bottom + r.height)
|
|
88
|
+
return;
|
|
89
|
+
t.kx = Math.max(-1, Math.min(1, ((e.clientX - r.left) / r.width) * 2 - 1));
|
|
90
|
+
t.ky = Math.max(-1, Math.min(1, ((e.clientY - r.top) / r.height) * 2 - 1));
|
|
91
|
+
herata();
|
|
92
|
+
};
|
|
93
|
+
const poistu = () => {
|
|
94
|
+
t.kx = 0;
|
|
95
|
+
t.ky = 0;
|
|
96
|
+
herata();
|
|
97
|
+
};
|
|
98
|
+
/* Kohdistus: klikkaus kiertää kerrokset alimmasta ylimpään. Koottu
|
|
99
|
+
pino avautuu ensin, ja kierroksen lopussa palaa alkutilaansa. */
|
|
100
|
+
let aktiivinen = -1;
|
|
101
|
+
function kohdista(i) {
|
|
102
|
+
aktiivinen = i;
|
|
103
|
+
el.classList.toggle('exploded__stack--kohdistus', i >= 0);
|
|
104
|
+
kerrokset.forEach((k, j) => k.classList.toggle('exploded__layer--aktiivinen', j === i));
|
|
105
|
+
}
|
|
106
|
+
const klikkaus = () => {
|
|
107
|
+
if (!t.auki) {
|
|
108
|
+
t.auki = true;
|
|
109
|
+
kohdista(-1);
|
|
110
|
+
}
|
|
111
|
+
else if (aktiivinen >= n - 1) {
|
|
112
|
+
kohdista(-1);
|
|
113
|
+
if (tila === 'koottu')
|
|
114
|
+
t.auki = false;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
kohdista(aktiivinen + 1);
|
|
118
|
+
}
|
|
119
|
+
liiku();
|
|
120
|
+
};
|
|
121
|
+
const valaise = (e) => {
|
|
122
|
+
el.querySelectorAll('.exploded--valaistu').forEach((x) => x.classList.remove('exploded--valaistu'));
|
|
123
|
+
const k = e.target.closest('[data-kohde]');
|
|
124
|
+
if (!k)
|
|
125
|
+
return;
|
|
126
|
+
el.querySelectorAll(`[data-kohde="${k.dataset.kohde}"]`).forEach((x) => x.classList.add('exploded--valaistu'));
|
|
127
|
+
};
|
|
128
|
+
const sammuta = () => el.querySelectorAll('.exploded--valaistu').forEach((x) => x.classList.remove('exploded--valaistu'));
|
|
129
|
+
/* Vieritys kokoaa, kun kuva poistuu yläreunasta. */
|
|
130
|
+
const vieritys = () => {
|
|
131
|
+
const r = hero.getBoundingClientRect();
|
|
132
|
+
const q = Math.max(0, Math.min(1, -r.top / (r.height * 0.7)));
|
|
133
|
+
t.vieritys = 1 - q * q * (3 - 2 * q);
|
|
134
|
+
if (t.auki && tila === 'auki')
|
|
135
|
+
liiku();
|
|
136
|
+
};
|
|
137
|
+
const nakyvyys = new IntersectionObserver(([e]) => {
|
|
138
|
+
t.nakyy = e.isIntersecting;
|
|
139
|
+
if (t.nakyy)
|
|
140
|
+
herata();
|
|
141
|
+
});
|
|
142
|
+
nakyvyys.observe(hero);
|
|
143
|
+
const mitat = new ResizeObserver(koko);
|
|
144
|
+
mitat.observe(hero);
|
|
145
|
+
if (!vahemman) {
|
|
146
|
+
window.addEventListener('pointermove', osoitin, { passive: true });
|
|
147
|
+
hero.addEventListener('pointerleave', poistu);
|
|
148
|
+
window.addEventListener('scroll', vieritys, { passive: true });
|
|
149
|
+
}
|
|
150
|
+
hero.addEventListener('click', klikkaus);
|
|
151
|
+
el.addEventListener('pointerover', valaise);
|
|
152
|
+
el.addEventListener('pointerleave', sammuta);
|
|
153
|
+
koko();
|
|
154
|
+
if (!vahemman)
|
|
155
|
+
vieritys();
|
|
156
|
+
asetu();
|
|
157
|
+
/* Ennen tätä pinolla ei ole mittoja, joten se on piilossa. */
|
|
158
|
+
hero.classList.add('exploded--valmis');
|
|
159
|
+
return () => {
|
|
160
|
+
cancelAnimationFrame(t.raf);
|
|
161
|
+
nakyvyys.disconnect();
|
|
162
|
+
mitat.disconnect();
|
|
163
|
+
window.removeEventListener('pointermove', osoitin);
|
|
164
|
+
hero.removeEventListener('pointerleave', poistu);
|
|
165
|
+
window.removeEventListener('scroll', vieritys);
|
|
166
|
+
hero.removeEventListener('click', klikkaus);
|
|
167
|
+
el.removeEventListener('pointerover', valaise);
|
|
168
|
+
el.removeEventListener('pointerleave', sammuta);
|
|
169
|
+
};
|
|
170
|
+
}, [tila]);
|
|
171
|
+
return (_jsx("div", { ref: kehys, className: ['exploded', `exploded--${tila}`, className].filter(Boolean).join(' '), role: "img", "aria-label": label, children: _jsx("div", { className: "exploded__stage", "aria-hidden": "true", children: _jsx("div", { ref: pino, className: "exploded__stack", children: layers.map((layer, i) => (_jsxs("div", { className: "exploded__layer", style: { '--i': i }, children: [_jsx("div", { className: "exploded__plate", children: layer.content }), _jsxs("div", { className: "exploded__name", children: [_jsx("span", { className: "meta ink", children: layer.name }), layer.source ? _jsx("span", { className: "meta meta--s", children: layer.source }) : null] })] }, layer.name))) }) }) }));
|
|
172
|
+
}
|
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.6.1.
|
|
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
|
|
@@ -22,6 +22,7 @@ tyylit saa myös suoraan: `@pokela/components/styles.css`.
|
|
|
22
22
|
- `ListRow`
|
|
23
23
|
- `Accordion`
|
|
24
24
|
- `Timeline`
|
|
25
|
+
- `ExplodedView`
|
|
25
26
|
- `Grid`
|
|
26
27
|
- `Reveal`
|
|
27
28
|
- `ThemeScript`
|
|
@@ -44,6 +45,7 @@ import Link from 'next/link';
|
|
|
44
45
|
- **Nav, Footer** — tarvitsevat sivuston sanakirjan ja reitityksen
|
|
45
46
|
- **BeforeAfter, Video** — rakentuvat Median varaan, ja Media lukee kuvamanifestia
|
|
46
47
|
- **HeroName** — piirtää sivuston omaa nimeä fontin ääriviivoista (`@/content/nimi.generated.json`)
|
|
48
|
+
- **SivuKerroksina** — kokoaa tämän sivuston räjäytyskuvan sen omista tokeneista ja sisällöstä; yleinen osa on ExplodedView
|
|
47
49
|
- **HeroIntro, HeroIntroLiike** — ovat etusivun intro: sivuston ominaisuus, ei design systemin osa
|
|
48
50
|
- **ComponentView, PrintCv** — ovat riippumattomia mutta sivuston ominaisuuksia — lähdekoodinäkymä ja CV:n tulostusasu — eivät design systemin osia
|
|
49
51
|
|
|
@@ -54,6 +56,7 @@ import Link from 'next/link';
|
|
|
54
56
|
- `styles/list-row.css`
|
|
55
57
|
- `styles/accordion.css`
|
|
56
58
|
- `styles/timeline.css`
|
|
59
|
+
- `styles/exploded-view.css`
|
|
57
60
|
|
|
58
61
|
Luokat ovat globaaleja, eivät CSS-moduuleja. Se on tarkoituksellista: sama
|
|
59
62
|
luokka on myös Figmassa komponentin nimenä, ja hajautettu nimi katkaisisi
|
package/guidelines/components.md
CHANGED
|
@@ -7,6 +7,7 @@ Use these instead of building an equivalent.
|
|
|
7
7
|
- `ListRow`
|
|
8
8
|
- `Accordion`, `useAccordionGroup`
|
|
9
9
|
- `Timeline`, `TimelineItem`
|
|
10
|
+
- `ExplodedView`
|
|
10
11
|
- `Grid`, `Col`
|
|
11
12
|
- `Reveal`
|
|
12
13
|
- `ThemeScript`, `THEME_STORAGE_KEY`
|
|
@@ -46,4 +47,22 @@ the total duration.
|
|
|
46
47
|
</Timeline>
|
|
47
48
|
```
|
|
48
49
|
|
|
50
|
+
`ExplodedView` shows layers as plates in 3D space: how a finished surface is
|
|
51
|
+
built from its parts. Each layer is real markup, not an image. Give elements
|
|
52
|
+
the same `data-kohde` in several layers and they light up together on hover.
|
|
53
|
+
`tila="koottu"` starts with the layers collapsed into one surface. Put no
|
|
54
|
+
links or buttons inside the layers; the whole view is one image to a screen
|
|
55
|
+
reader, described by `label`.
|
|
56
|
+
|
|
57
|
+
```jsx
|
|
58
|
+
<ExplodedView
|
|
59
|
+
label="The page as three layers: tokens, components and the finished page."
|
|
60
|
+
layers={[
|
|
61
|
+
{ name: '01 Tokens', source: 'tokens.json', content: <Tokens /> },
|
|
62
|
+
{ name: '02 Components', content: <Components /> },
|
|
63
|
+
{ name: '03 Page', content: <Page /> },
|
|
64
|
+
]}
|
|
65
|
+
/>
|
|
66
|
+
```
|
|
67
|
+
|
|
49
68
|
`Reveal` fades its children in on scroll. Wrap sections, not single elements.
|
package/index.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export { Accordion, useAccordionGroup } from './Accordion.js';
|
|
|
7
7
|
export type { AccordionProps } from './Accordion.js';
|
|
8
8
|
export { default as Timeline, TimelineItem } from './Timeline.js';
|
|
9
9
|
export type { TimelineVariant } from './Timeline.js';
|
|
10
|
+
export { default as ExplodedView } from './ExplodedView.js';
|
|
11
|
+
export type { ExplodedTila, ExplodedLayer } from './ExplodedView.js';
|
|
10
12
|
export { Grid, Col } from './Grid.js';
|
|
11
13
|
export { default as Reveal } from './Reveal.js';
|
|
12
14
|
export { default as ThemeScript, THEME_STORAGE_KEY } from './ThemeScript.js';
|
package/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export { default as Icon, ICON_NAMES } from './Icon.js';
|
|
|
4
4
|
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
|
+
export { default as ExplodedView } from './ExplodedView.js';
|
|
7
8
|
export { Grid, Col } from './Grid.js';
|
|
8
9
|
export { default as Reveal } from './Reveal.js';
|
|
9
10
|
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.6.1",
|
|
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.6.1"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
33
|
"react": ">=18"
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* Räjäytyskuva (components/ExplodedView.tsx). Levyt erottaa toisistaan
|
|
2
|
+
viiva ja taustan sävy, ei varjo: järjestelmässä ei ole varjoja.
|
|
3
|
+
Näyttämö on kiinteän kokoinen ja skaalataan
|
|
4
|
+
paikkaansa (--s), joten levyn sisältö käyttää sivun omia mittoja.
|
|
5
|
+
Kulmat, väli ja mittakaava tulevat skriptistä. */
|
|
6
|
+
.exploded {
|
|
7
|
+
/* Levyn mitat: näyttämön koordinaatisto, ei layout-mitta. */
|
|
8
|
+
--levy-leveys: 960px;
|
|
9
|
+
--levy-korkeus: 600px;
|
|
10
|
+
position: relative;
|
|
11
|
+
aspect-ratio: 4 / 5;
|
|
12
|
+
background: var(--badge-surface);
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
cursor: pointer;
|
|
15
|
+
touch-action: pan-y;
|
|
16
|
+
-webkit-tap-highlight-color: transparent;
|
|
17
|
+
}
|
|
18
|
+
@media (min-width: 600px) { .exploded { aspect-ratio: 16 / 9; } }
|
|
19
|
+
@media (min-width: 900px) { .exploded { aspect-ratio: 21 / 9; } }
|
|
20
|
+
|
|
21
|
+
.exploded__stage {
|
|
22
|
+
position: absolute; inset: 0;
|
|
23
|
+
perspective: calc(var(--levy-leveys) * 2.3);
|
|
24
|
+
perspective-origin: 50% 40%;
|
|
25
|
+
opacity: 0;
|
|
26
|
+
transition: opacity var(--dur-slow) var(--ease-standard);
|
|
27
|
+
}
|
|
28
|
+
.exploded--valmis .exploded__stage { opacity: 1; }
|
|
29
|
+
|
|
30
|
+
.exploded__stack {
|
|
31
|
+
--rx: 0deg; --ry: 0deg; --rz: 0deg; --vali: 0px; --nosto: 0px; --s: 1;
|
|
32
|
+
position: absolute; left: 50%; top: 50%;
|
|
33
|
+
width: var(--levy-leveys); height: var(--levy-korkeus);
|
|
34
|
+
margin-left: calc(var(--levy-leveys) / -2);
|
|
35
|
+
margin-top: calc(var(--levy-korkeus) / -2);
|
|
36
|
+
transform-style: preserve-3d;
|
|
37
|
+
transform: scale(var(--s)) translateY(var(--nosto)) rotateX(var(--rx)) rotateY(var(--ry)) rotateZ(var(--rz))
|
|
38
|
+
translateZ(calc(var(--vali) * -1));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.exploded__layer {
|
|
42
|
+
--i: 0;
|
|
43
|
+
position: absolute; inset: 0;
|
|
44
|
+
transform-style: preserve-3d;
|
|
45
|
+
transform: translateZ(calc(var(--i) * var(--vali) + var(--korotus, 0px)));
|
|
46
|
+
transition: transform var(--dur-base) var(--ease-out);
|
|
47
|
+
}
|
|
48
|
+
.exploded__layer--aktiivinen { --korotus: var(--space-40); }
|
|
49
|
+
.exploded__layer--piilossa { visibility: hidden; }
|
|
50
|
+
|
|
51
|
+
.exploded__plate {
|
|
52
|
+
position: absolute; inset: 0; overflow: hidden;
|
|
53
|
+
background: var(--paper); color: var(--ink);
|
|
54
|
+
border: var(--hairline) solid var(--line);
|
|
55
|
+
transition: opacity var(--dur-slow) var(--ease-standard);
|
|
56
|
+
}
|
|
57
|
+
.exploded__name {
|
|
58
|
+
position: absolute; left: 0; bottom: 100%;
|
|
59
|
+
display: grid; gap: var(--space-4);
|
|
60
|
+
padding-bottom: var(--space-16);
|
|
61
|
+
white-space: nowrap;
|
|
62
|
+
transition: opacity var(--dur-slow) var(--ease-standard);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Lähde on taustapinnalla, ei paperilla: ink-faint jää siinä alle 4.5:1. */
|
|
66
|
+
.exploded__name .meta--s { color: var(--ink-muted); }
|
|
67
|
+
|
|
68
|
+
.exploded__stack--kohdistus .exploded__layer:not(.exploded__layer--aktiivinen) .exploded__plate,
|
|
69
|
+
.exploded__stack--kohdistus .exploded__layer:not(.exploded__layer--aktiivinen) .exploded__name { opacity: 0.35; }
|
|
70
|
+
|
|
71
|
+
/* Sama kohde kaikissa kerroksissa: katkoviiva muuttuu yhtenäiseksi. */
|
|
72
|
+
.exploded [data-kohde] {
|
|
73
|
+
outline: var(--hairline) dashed transparent;
|
|
74
|
+
outline-offset: var(--space-4);
|
|
75
|
+
transition: outline-color var(--dur-fast) var(--ease-standard);
|
|
76
|
+
}
|
|
77
|
+
.exploded--valaistu { outline: var(--hairline-strong) solid var(--ink); }
|
package/styles.css
CHANGED