@pokela/components 2.15.0 → 2.16.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/Dialog.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * Modaalinen dialogi: otsikko, teksti, sisältö, toiminnot ja sulkeminen.
4
+ *
5
+ * Sovelluskomponentti (päätökset 22 ja 31). Ei varjoa eikä tummaa
6
+ * verhoa: sivu peittyy paperin sävyisellä verholla, ja dialogi on
7
+ * paperia musteen hiusviivakehyksellä. Uutta väriä ei tarvita, ja
8
+ * sama ratkaisu toimii molemmissa teemoissa. Mobiilissa dialogi on
9
+ * alareunan arkki, ja napit ovat täysleveitä.
10
+ *
11
+ * Natiivi <dialog> ja showModal(): selain pitää fokuksen dialogissa,
12
+ * piilottaa muun sivun ruudunlukijalta ja sulkee Escillä. Esc ja
13
+ * verhon klikkaus kutsuvat `onClose`a, joten tila pysyy kutsujalla.
14
+ *
15
+ * Fokus menee avattaessa ensimmäiseen fokusoitavaan: lomakkeessa
16
+ * ensimmäiseen kenttään, vahvistuksessa Peruuta-nappiin, kun se on
17
+ * toiminnoissa ensimmäisenä. Sulkunappi on DOM:ssa viimeisenä, jotta
18
+ * se ei vie alkufokusta. Suljettaessa fokus palaa avanneeseen nappiin.
19
+ */
20
+ export type DialogProps = {
21
+ open: boolean;
22
+ onClose: () => void;
23
+ title: string;
24
+ /** Selittävä teksti otsikon alla. Ruudunlukija lukee sen avattaessa. */
25
+ description?: ReactNode;
26
+ /** Sisältö tekstin alla, esim. lomakkeen kentät. */
27
+ children?: ReactNode;
28
+ /** Napit oikeaan alakulmaan: vähiten vahingollinen ensin. */
29
+ actions?: ReactNode;
30
+ /** Sulkunapin nimi ruudunlukijalle. */
31
+ closeLabel?: string;
32
+ className?: string;
33
+ };
34
+ export default function Dialog({ open, onClose, title, description, children, actions, closeLabel, className, }: DialogProps): import("react/jsx-runtime").JSX.Element;
package/Dialog.js ADDED
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useId, useRef } from 'react';
4
+ import Icon from './Icon';
5
+ export default function Dialog({ open, onClose, title, description, children, actions, closeLabel = 'Sulje', className, }) {
6
+ const ref = useRef(null);
7
+ const avaaja = useRef(null);
8
+ const otsikkoId = useId();
9
+ const tekstiId = useId();
10
+ useEffect(() => {
11
+ const el = ref.current;
12
+ if (!el)
13
+ return;
14
+ if (open && !el.open) {
15
+ avaaja.current = document.activeElement;
16
+ el.showModal();
17
+ }
18
+ else if (!open && el.open) {
19
+ el.close();
20
+ avaaja.current?.focus();
21
+ }
22
+ }, [open]);
23
+ return (_jsx("dialog", { ref: ref, className: ['dialog', className].filter(Boolean).join(' '), "aria-labelledby": otsikkoId, "aria-describedby": description ? tekstiId : undefined, onCancel: (e) => {
24
+ /* Esc: selain sulkisi itse, mutta tila on kutsujalla. */
25
+ e.preventDefault();
26
+ onClose();
27
+ }, onClick: (e) => {
28
+ /* Klikkaus osuu itse dialogiin vain verhon kohdalla: sisältö on
29
+ .dialog__panelissa, joka täyttää dialogin. */
30
+ if (e.target === e.currentTarget)
31
+ onClose();
32
+ }, children: _jsxs("div", { className: "dialog__panel", children: [_jsxs("div", { className: "dialog__header", children: [_jsx("h2", { id: otsikkoId, className: "dialog__title", children: title }), description ? (_jsx("div", { id: tekstiId, className: "dialog__description", children: description })) : null] }), children ? _jsx("div", { className: "dialog__body", children: children }) : null, actions ? _jsx("div", { className: "dialog__actions", children: actions }) : null, _jsx("button", { type: "button", className: "dialog__close", "aria-label": closeLabel, onClick: onClose, children: _jsx(Icon, { name: "close" }) })] }) }));
33
+ }
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @pokela/components
2
2
 
3
- React-komponentit: **Pokela Design System**, versio 2.15.0.
3
+ React-komponentit: **Pokela Design System**, versio 2.16.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
@@ -35,6 +35,7 @@ tyylit saa myös suoraan: `@pokela/components/styles.css`.
35
35
  - `Tab`
36
36
  - `Alert`
37
37
  - `Table`
38
+ - `Dialog`
38
39
  - `Grid`
39
40
  - `Reveal`
40
41
  - `ThemeScript`
@@ -76,6 +77,7 @@ import Link from 'next/link';
76
77
  - `styles/tabs.css`
77
78
  - `styles/alert.css`
78
79
  - `styles/table.css`
80
+ - `styles/dialog.css`
79
81
 
80
82
  Luokat ovat globaaleja, eivät CSS-moduuleja. Se on tarkoituksellista: sama
81
83
  luokka on myös Figmassa komponentin nimenä, ja hajautettu nimi katkaisisi
@@ -20,6 +20,7 @@ Use these instead of building an equivalent.
20
20
  - `Tab`
21
21
  - `Alert`
22
22
  - `Table`
23
+ - `Dialog`
23
24
  - `Grid`, `Col`
24
25
  - `Reveal`
25
26
  - `ThemeScript`, `THEME_STORAGE_KEY`
@@ -184,6 +185,27 @@ row action is a `table__action` link whose name includes the row.
184
185
  />
185
186
  ```
186
187
 
188
+ `Dialog` is a modal for one decision or a short form: confirm a deletion,
189
+ invite a member. Never use it for content people read, and never stack two.
190
+ Control it with `open` and `onClose`; Esc and a click on the veil call
191
+ `onClose`. Give a short `title` (a question for confirmations), the
192
+ consequence as `description`, form fields as children, and buttons in
193
+ `actions` with the least destructive first (`btn--ghost` Cancel, then
194
+ `btn--primary`). Use `btn--s` buttons. On mobile it becomes a bottom sheet.
195
+
196
+ ```jsx
197
+ <Dialog
198
+ open={open}
199
+ onClose={() => setOpen(false)}
200
+ title="Remove Sara from the team?"
201
+ description="Access ends immediately. You can invite them again later."
202
+ actions={<>
203
+ <button className="btn btn--ghost btn--s" onClick={() => setOpen(false)}>Cancel</button>
204
+ <button className="btn btn--primary btn--s" onClick={remove}>Remove member</button>
205
+ </>}
206
+ />
207
+ ```
208
+
187
209
  ## Patterns
188
210
 
189
211
  `PageHeader` and `Section` are patterns: they add no look of their own, only
package/index.d.ts CHANGED
@@ -32,6 +32,8 @@ export { default as Alert } from './Alert.js';
32
32
  export type { AlertTone, AlertProps } from './Alert.js';
33
33
  export { default as Table } from './Table.js';
34
34
  export type { TableSortDirection, TableSort, TableColumn, TableProps } from './Table.js';
35
+ export { default as Dialog } from './Dialog.js';
36
+ export type { DialogProps } from './Dialog.js';
35
37
  export { Grid, Col } from './Grid.js';
36
38
  export { default as Reveal } from './Reveal.js';
37
39
  export { default as ThemeScript, THEME_STORAGE_KEY } from './ThemeScript.js';
package/index.js CHANGED
@@ -17,6 +17,7 @@ export { default as Tabs } from './Tabs.js';
17
17
  export { default as Tab } from './Tab.js';
18
18
  export { default as Alert } from './Alert.js';
19
19
  export { default as Table } from './Table.js';
20
+ export { default as Dialog } from './Dialog.js';
20
21
  export { Grid, Col } from './Grid.js';
21
22
  export { default as Reveal } from './Reveal.js';
22
23
  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.15.0",
3
+ "version": "2.16.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.15.0"
30
+ "@pokela/tokens": "^2.16.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "react": ">=18"
@@ -0,0 +1,125 @@
1
+ /* Dialogi (Dialog). Sovelluskomponentti, sivusto ei käytä tätä.
2
+ ==================================================================
3
+ Paperiverho ja hiusviivakehys (päätös 31). Systeemissä ei ole
4
+ varjoja eikä verhoväriä: sivu peittyy paperin sävyisellä verholla,
5
+ ja dialogi erottuu siitä musteviivalla. Verho on läpikuultavaa
6
+ paperia, joten se toimii samoin molemmissa teemoissa.
7
+
8
+ Mobiili ensin: alareunan arkki, jonka yläreunassa on viiva. 600 px
9
+ alkaen keskitetty dialogi, jonka ympärillä on kehys. */
10
+
11
+ .dialog {
12
+ inset: auto 0 0;
13
+ width: 100%;
14
+ max-width: 100%;
15
+ max-height: calc(100dvh - var(--space-32));
16
+ margin: 0;
17
+ padding: 0;
18
+ overflow-y: auto;
19
+ overscroll-behavior: contain;
20
+ background: var(--paper);
21
+ color: var(--ink);
22
+ border: 0;
23
+ border-top: var(--hairline) solid var(--ink);
24
+ }
25
+ .dialog::backdrop {
26
+ background: color-mix(in srgb, var(--paper) 88%, transparent);
27
+ }
28
+
29
+ /* Sivu ei vierity dialogin alla. */
30
+ html:has(.dialog[open]) { overflow: hidden; }
31
+
32
+ .dialog__panel {
33
+ position: relative;
34
+ display: grid;
35
+ gap: var(--space-24);
36
+ padding: var(--space-24) var(--space-16) var(--space-32);
37
+ }
38
+
39
+ .dialog__header {
40
+ display: grid;
41
+ gap: var(--space-8);
42
+ /* Tila sulkunapille, ettei pitkä otsikko mene sen alle. */
43
+ padding-right: var(--space-40);
44
+ }
45
+ .dialog__title {
46
+ margin: 0;
47
+ font-family: var(--font-ui);
48
+ font-size: var(--text-body-l);
49
+ line-height: var(--lh-body-l);
50
+ font-weight: var(--weight-500);
51
+ }
52
+ .dialog__description {
53
+ font-size: var(--text-body-s);
54
+ line-height: var(--lh-body-s);
55
+ color: var(--ink-muted);
56
+ }
57
+ .dialog__description > * { margin: 0; }
58
+
59
+ .dialog__body {
60
+ display: grid;
61
+ gap: var(--space-16);
62
+ }
63
+
64
+ /* Mobiilissa napit täysleveinä allekkain, ensisijainen (viimeinen)
65
+ ylimpänä peukalon ulottuvilla. */
66
+ .dialog__actions {
67
+ display: flex;
68
+ flex-direction: column-reverse;
69
+ gap: var(--space-12);
70
+ }
71
+ .dialog__actions > .btn { width: 100%; }
72
+
73
+ /* Sulkunappi: näkyvä 32 px, osuma-alue --tap-min, kuten Alertissa. */
74
+ .dialog__close {
75
+ position: absolute;
76
+ top: var(--space-16);
77
+ right: var(--space-8);
78
+ display: inline-flex;
79
+ align-items: center;
80
+ justify-content: center;
81
+ width: var(--space-32);
82
+ height: var(--space-32);
83
+ padding: 0;
84
+ background: none;
85
+ border: 0;
86
+ color: var(--ink-muted);
87
+ cursor: pointer;
88
+ }
89
+ .dialog__close::after {
90
+ content: '';
91
+ position: absolute;
92
+ inset: calc((var(--space-32) - var(--tap-min)) / 2);
93
+ }
94
+ .dialog__close:hover { color: var(--ink); }
95
+
96
+ @media (min-width: 600px) {
97
+ .dialog {
98
+ inset: 0;
99
+ width: calc(100% - var(--space-32));
100
+ max-width: 480px;
101
+ margin: auto;
102
+ border: var(--hairline) solid var(--ink);
103
+ }
104
+ .dialog__panel { padding: var(--space-32); }
105
+ .dialog__close { top: var(--space-24); right: var(--space-24); }
106
+ .dialog__actions {
107
+ flex-direction: row;
108
+ justify-content: flex-end;
109
+ }
110
+ .dialog__actions > .btn { width: auto; }
111
+ }
112
+
113
+ /* Liike: dialogi nousee ja verho häivyttää. Ei liikettä, jos käyttäjä
114
+ on sen kieltänyt. */
115
+ @media (prefers-reduced-motion: no-preference) {
116
+ .dialog[open] { animation: dialog-esiin var(--dur-base) var(--ease-out); }
117
+ .dialog[open]::backdrop { animation: dialog-verho var(--dur-base) var(--ease-out); }
118
+ .dialog__close { transition: color var(--dur-fast) var(--ease-standard); }
119
+ }
120
+ @keyframes dialog-esiin { from { opacity: 0; transform: translateY(var(--space-8)); } }
121
+ @keyframes dialog-verho { from { opacity: 0; } }
122
+
123
+ @media (forced-colors: active) {
124
+ .dialog::backdrop { background: Canvas; opacity: 0.88; }
125
+ }
package/styles.css CHANGED
@@ -14,3 +14,4 @@
14
14
  @import './styles/tabs.css';
15
15
  @import './styles/alert.css';
16
16
  @import './styles/table.css';
17
+ @import './styles/dialog.css';