@hideyukimori/nene2-ui 0.15.0 โ 0.16.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/dist/overlay/Modal.d.ts +56 -5
- package/dist/overlay/Modal.js +36 -6
- package/dist/overlay/Modal.js.map +1 -1
- package/package.json +1 -1
- package/themes/default.css +21 -0
package/dist/overlay/Modal.d.ts
CHANGED
|
@@ -1,24 +1,75 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
|
|
2
|
+
type ModalSize = 'sm' | 'md' | 'lg';
|
|
3
|
+
interface ModalBase {
|
|
3
4
|
open: boolean;
|
|
4
|
-
/** Called for every dismissal the browser
|
|
5
|
+
/** Called for every dismissal routed through the browser's `close` event: Esc, and `close()`. */
|
|
5
6
|
onClose: () => void;
|
|
6
|
-
/** Localized title.
|
|
7
|
+
/** Localized title. Names the dialog for assistive tech, and is what the header shows. */
|
|
7
8
|
title: string;
|
|
9
|
+
/**
|
|
10
|
+
* Width ceiling. Omitted, the dialog takes the browser's own sizing โ which is what every
|
|
11
|
+
* caller got before this prop existed, so leaving it out changes nothing.
|
|
12
|
+
*/
|
|
13
|
+
size?: ModalSize;
|
|
14
|
+
/**
|
|
15
|
+
* On a narrow viewport, sit against the bottom edge with the top corners rounded.
|
|
16
|
+
* Off by default: a dialog that changes shape at a breakpoint is a decision, not a default.
|
|
17
|
+
*/
|
|
18
|
+
sheetOnMobile?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Scroll the body instead of letting tall content spill out of the dialog. Off by default.
|
|
21
|
+
*
|
|
22
|
+
* ๐ด The kit adds no height cap of its own. The UA stylesheet already caps a `showModal()`
|
|
23
|
+
* dialog to the viewport โ inventing a second ceiling here would mean a literal like `80vh`
|
|
24
|
+
* living in the theme, which the kit forbids (every slot default must be a scale reference,
|
|
25
|
+
* and the spacing scale is rem-based and cannot express a share of the viewport). What is
|
|
26
|
+
* actually missing without this prop is the scroll: at the UA cap, tall content is clipped.
|
|
27
|
+
* โ ๏ธ The cap itself is the browser's and is not asserted here โ jsdom does not implement
|
|
28
|
+
* `showModal`, so it can only be verified in a real browser (see #392, live lane).
|
|
29
|
+
*/
|
|
30
|
+
scrollable?: boolean;
|
|
8
31
|
children: ReactNode;
|
|
9
32
|
}
|
|
33
|
+
interface PlainModal extends ModalBase {
|
|
34
|
+
header?: false;
|
|
35
|
+
closeLabel?: never;
|
|
36
|
+
}
|
|
37
|
+
interface HeaderModal extends ModalBase {
|
|
38
|
+
/** Draw a header: the title, and a control that closes the dialog. */
|
|
39
|
+
header: true;
|
|
40
|
+
/**
|
|
41
|
+
* Localized name for the close control.
|
|
42
|
+
*
|
|
43
|
+
* ๐ด Required with `header`, not optional-with-a-default. A default would be an English
|
|
44
|
+
* string shipped into every product's UI โ the one thing a fleet kit must not do (I18N-2).
|
|
45
|
+
* The union makes the compiler ask for it exactly when it is going to be rendered.
|
|
46
|
+
*/
|
|
47
|
+
closeLabel: string;
|
|
48
|
+
}
|
|
49
|
+
export type ModalProps = PlainModal | HeaderModal;
|
|
10
50
|
/**
|
|
11
51
|
* A modal dialog built on the native `<dialog>` element.
|
|
12
52
|
*
|
|
13
53
|
* ๐ด Why native. Focus trapping, Esc-to-dismiss, the backdrop, and rendering above every
|
|
14
54
|
* stacking context are all things the browser already does correctly. Eight ships wrote
|
|
15
55
|
* their own modal, which means the fleet currently maintains eight focus traps โ the single
|
|
16
|
-
* hardest piece of interaction code to get right, reimplemented per product.
|
|
56
|
+
* hardest piece of interaction code to get right, reimplemented per product. nene-vault is
|
|
57
|
+
* not one of those eight: it shipped `aria-modal="true"` on a plain element, which announces
|
|
58
|
+
* "everything outside is inert" while Tab walks straight out of it (measured in production
|
|
59
|
+
* 2026-08-25, #392). A dialog that lies to assistive tech is worse than one that admits it
|
|
60
|
+
* is not modal.
|
|
17
61
|
*
|
|
18
62
|
* ๐ด `showModal()` is called imperatively rather than through the `open` attribute, because
|
|
19
63
|
* only `showModal()` puts the dialog in the top layer and traps focus; setting `open`
|
|
20
64
|
* renders it inline and non-modal. jsdom 25.0.1 does not implement `showModal` at all
|
|
21
65
|
* (measured 2026-08-23), and neither do browsers older than the feature, so the fallback
|
|
22
66
|
* sets `open` โ degraded but visible, rather than a dialog that never appears.
|
|
67
|
+
*
|
|
68
|
+
* ๐ด Every prop added in 0.16.0 (`header`, `size`, `sheetOnMobile`, `scrollable`) defaults to
|
|
69
|
+
* what the component already rendered, so a caller that passes none of them sees no change.
|
|
70
|
+
* That is only true while the defaults are a copy of the previous rendering โ the lesson
|
|
71
|
+
* `--text-x-slot-button-sm-size` taught in #380, where "the default is harmless" held right
|
|
72
|
+
* up until a consumer overrode one side of a pair.
|
|
23
73
|
*/
|
|
24
|
-
export declare function Modal(
|
|
74
|
+
export declare function Modal(props: ModalProps): import("react").JSX.Element;
|
|
75
|
+
export {};
|
package/dist/overlay/Modal.js
CHANGED
|
@@ -1,22 +1,41 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef } from 'react';
|
|
3
3
|
import { cx } from '../lib/cx.js';
|
|
4
|
+
const SIZE_CLASS = {
|
|
5
|
+
sm: 'max-w-x-slot-modal-sm',
|
|
6
|
+
md: 'max-w-x-slot-modal-md',
|
|
7
|
+
lg: 'max-w-x-slot-modal-lg',
|
|
8
|
+
};
|
|
9
|
+
/** Bottom sheet below `sm`, ordinary dialog above it. */
|
|
10
|
+
const SHEET_CLASS = 'max-sm:mt-auto max-sm:mb-0 max-sm:w-full max-sm:max-w-none max-sm:rounded-b-none';
|
|
4
11
|
/**
|
|
5
12
|
* A modal dialog built on the native `<dialog>` element.
|
|
6
13
|
*
|
|
7
14
|
* ๐ด Why native. Focus trapping, Esc-to-dismiss, the backdrop, and rendering above every
|
|
8
15
|
* stacking context are all things the browser already does correctly. Eight ships wrote
|
|
9
16
|
* their own modal, which means the fleet currently maintains eight focus traps โ the single
|
|
10
|
-
* hardest piece of interaction code to get right, reimplemented per product.
|
|
17
|
+
* hardest piece of interaction code to get right, reimplemented per product. nene-vault is
|
|
18
|
+
* not one of those eight: it shipped `aria-modal="true"` on a plain element, which announces
|
|
19
|
+
* "everything outside is inert" while Tab walks straight out of it (measured in production
|
|
20
|
+
* 2026-08-25, #392). A dialog that lies to assistive tech is worse than one that admits it
|
|
21
|
+
* is not modal.
|
|
11
22
|
*
|
|
12
23
|
* ๐ด `showModal()` is called imperatively rather than through the `open` attribute, because
|
|
13
24
|
* only `showModal()` puts the dialog in the top layer and traps focus; setting `open`
|
|
14
25
|
* renders it inline and non-modal. jsdom 25.0.1 does not implement `showModal` at all
|
|
15
26
|
* (measured 2026-08-23), and neither do browsers older than the feature, so the fallback
|
|
16
27
|
* sets `open` โ degraded but visible, rather than a dialog that never appears.
|
|
28
|
+
*
|
|
29
|
+
* ๐ด Every prop added in 0.16.0 (`header`, `size`, `sheetOnMobile`, `scrollable`) defaults to
|
|
30
|
+
* what the component already rendered, so a caller that passes none of them sees no change.
|
|
31
|
+
* That is only true while the defaults are a copy of the previous rendering โ the lesson
|
|
32
|
+
* `--text-x-slot-button-sm-size` taught in #380, where "the default is harmless" held right
|
|
33
|
+
* up until a consumer overrode one side of a pair.
|
|
17
34
|
*/
|
|
18
|
-
export function Modal(
|
|
35
|
+
export function Modal(props) {
|
|
36
|
+
const { open, onClose, title, size, sheetOnMobile, scrollable, children } = props;
|
|
19
37
|
const ref = useRef(null);
|
|
38
|
+
const titleId = useId();
|
|
20
39
|
useEffect(() => {
|
|
21
40
|
const el = ref.current;
|
|
22
41
|
if (el === null)
|
|
@@ -39,9 +58,20 @@ export function Modal({ open, onClose, title, children }) {
|
|
|
39
58
|
el.removeAttribute('open');
|
|
40
59
|
}
|
|
41
60
|
}, [open]);
|
|
42
|
-
|
|
61
|
+
// The header names the dialog through the heading it already draws; without one there is
|
|
62
|
+
// nothing on screen to point at, so the title has to be carried as a label.
|
|
63
|
+
const naming = props.header ? { 'aria-labelledby': titleId } : { 'aria-label': title };
|
|
64
|
+
return (_jsxs("dialog", { ref: ref, ...naming,
|
|
43
65
|
// The browser fires `close` for Esc as well as for close(); routing both through the
|
|
44
66
|
// caller keeps `open` from drifting out of sync with what is on screen.
|
|
45
|
-
onClose: onClose, onCancel: onClose, className: cx(
|
|
67
|
+
onClose: onClose, onCancel: onClose, className: cx(
|
|
68
|
+
// ๐ด `m-auto` is what centres a `showModal()` dialog. The UA stylesheet already says
|
|
69
|
+
// `dialog { margin: auto }`, but Tailwind's preflight (`* { margin: 0 }`, author origin)
|
|
70
|
+
// erases it, so on every Tailwind ship the dialog sat at (0,0) โ measured in nene-vault's
|
|
71
|
+
// production at 1280px, where the old hand-written modal had been at (380,119) (#417).
|
|
72
|
+
// The sheet classes below (`max-sm:mt-auto max-sm:mb-0`) always assumed this margin was
|
|
73
|
+
// there; saying it explicitly is the kit owning an assumption it had been borrowing.
|
|
74
|
+
// jsdom does not implement `showModal`, so the position itself is a live-lane check.
|
|
75
|
+
'm-auto bg-x-slot-modal-bg text-x-slot-modal-fg border border-x-slot-modal-border rounded-x-slot-modal', 'p-x-slot-modal-pad font-sans backdrop:bg-x-slot-modal-scrim/50', size !== undefined && SIZE_CLASS[size], sheetOnMobile === true && SHEET_CLASS, scrollable === true && 'flex flex-col'), children: [props.header === true && (_jsxs("header", { className: "mb-x-slot-modal-header-gap flex items-start justify-between gap-x-slot-modal-header-gap", children: [_jsx("h2", { id: titleId, className: "font-x-slot-modal-title text-x-slot-modal-title-size", children: title }), _jsx("button", { type: "button", "aria-label": props.closeLabel, onClick: onClose, className: "rounded-x-slot-control text-x-slot-modal-close-fg leading-none hover:brightness-x-slot-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-x-slot-focus-ring", children: _jsx("span", { "aria-hidden": "true", children: "\u00D7" }) })] })), scrollable === true ? _jsx("div", { className: "min-h-0 overflow-y-auto", children: children }) : children] }));
|
|
46
76
|
}
|
|
47
77
|
//# sourceMappingURL=Modal.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Modal.js","sourceRoot":"","sources":["../../src/overlay/Modal.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAkB,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"Modal.js","sourceRoot":"","sources":["../../src/overlay/Modal.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAkB,MAAM,OAAO,CAAC;AACjE,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAuDlC,MAAM,UAAU,GAA8B;IAC5C,EAAE,EAAE,uBAAuB;IAC3B,EAAE,EAAE,uBAAuB;IAC3B,EAAE,EAAE,uBAAuB;CAC5B,CAAC;AAEF,yDAAyD;AACzD,MAAM,WAAW,GACf,kFAAkF,CAAC;AAErF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,KAAK,CAAC,KAAiB;IACrC,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAClF,MAAM,GAAG,GAAG,MAAM,CAAoB,IAAI,CAAC,CAAC;IAC5C,MAAM,OAAO,GAAG,KAAK,EAAE,CAAC;IAExB,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC;QACvB,IAAI,EAAE,KAAK,IAAI;YAAE,OAAO;QAExB,IAAI,IAAI,EAAE,CAAC;YACT,IAAI,OAAO,EAAE,CAAC,SAAS,KAAK,UAAU,EAAE,CAAC;gBACvC,IAAI,CAAC,EAAE,CAAC,IAAI;oBAAE,EAAE,CAAC,SAAS,EAAE,CAAC;YAC/B,CAAC;iBAAM,CAAC;gBACN,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YAC9B,CAAC;YACD,OAAO;QACT,CAAC;QAED,IAAI,OAAO,EAAE,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YACnC,IAAI,EAAE,CAAC,IAAI;gBAAE,EAAE,CAAC,KAAK,EAAE,CAAC;QAC1B,CAAC;aAAM,CAAC;YACN,EAAE,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC;QAC7B,CAAC;IACH,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC;IAEX,yFAAyF;IACzF,4EAA4E;IAC5E,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,CAAC;IAEvF,OAAO,CACL,kBACE,GAAG,EAAE,GAAG,KACJ,MAAM;QACV,qFAAqF;QACrF,wEAAwE;QACxE,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,OAAO,EACjB,SAAS,EAAE,EAAE;QACX,qFAAqF;QACrF,yFAAyF;QACzF,0FAA0F;QAC1F,uFAAuF;QACvF,wFAAwF;QACxF,qFAAqF;QACrF,qFAAqF;QACrF,uGAAuG,EACvG,gEAAgE,EAChE,IAAI,KAAK,SAAS,IAAI,UAAU,CAAC,IAAI,CAAC,EACtC,aAAa,KAAK,IAAI,IAAI,WAAW,EACrC,UAAU,KAAK,IAAI,IAAI,eAAe,CACvC,aAEA,KAAK,CAAC,MAAM,KAAK,IAAI,IAAI,CACxB,kBAAQ,SAAS,EAAC,yFAAyF,aACzG,aAAI,EAAE,EAAE,OAAO,EAAE,SAAS,EAAC,sDAAsD,YAC9E,KAAK,GACH,EACL,iBACE,IAAI,EAAC,QAAQ,gBACD,KAAK,CAAC,UAAU,EAC5B,OAAO,EAAE,OAAO,EAChB,SAAS,EAAC,6LAA6L,YAIvM,8BAAkB,MAAM,uBAAc,GAC/B,IACF,CACV,EACA,UAAU,KAAK,IAAI,CAAC,CAAC,CAAC,cAAK,SAAS,EAAC,yBAAyB,YAAE,QAAQ,GAAO,CAAC,CAAC,CAAC,QAAQ,IACpF,CACV,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hideyukimori/nene2-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "NeNe fleet shared React UI kit โ token-driven primitives on Tailwind v4 @theme. Components carry no design of their own; themes do.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/themes/default.css
CHANGED
|
@@ -389,6 +389,27 @@
|
|
|
389
389
|
--text-x-slot-button-sm-size: inherit;
|
|
390
390
|
--text-x-slot-choice-size: inherit;
|
|
391
391
|
|
|
392
|
+
/* โโ Modal slots (0.16.0 / #392) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
393
|
+
* ๐ด Every one of these is reached only through a prop that defaults to off, so adding
|
|
394
|
+
* them changes nothing for a caller that passes none. The dialog itself is unchanged.
|
|
395
|
+
*
|
|
396
|
+
* The three widths choose Tailwind's own container steps rather than inventing lengths.
|
|
397
|
+
* nene-vault measured 432px and 520px in production; the fleet's own dialogs cluster at
|
|
398
|
+
* 560px (15 uses), 640 (6), 680 (4), 600 (4), 420 (4), 520 (3), 460 (3) โ measured across
|
|
399
|
+
* each ship's `frontend/src` on 2026-08-25. `md`/`lg`/`xl` cover those clusters, and a product
|
|
400
|
+
* whose value is not on the scale overrides the slot rather than the scale (README ยงโ ). */
|
|
401
|
+
--container-x-slot-modal-sm: var(--container-md);
|
|
402
|
+
--container-x-slot-modal-md: var(--container-lg);
|
|
403
|
+
--container-x-slot-modal-lg: var(--container-xl);
|
|
404
|
+
|
|
405
|
+
--spacing-x-slot-modal-header-gap: var(--spacing-x-xs);
|
|
406
|
+
--font-weight-x-slot-modal-title: var(--font-weight-medium);
|
|
407
|
+
/* `inherit`, so the header title takes the dialog's own size unless a product says
|
|
408
|
+
* otherwise. There is no previous rendering to copy โ the header is new โ and picking a
|
|
409
|
+
* step here would decide type for every product that turns the header on. */
|
|
410
|
+
--text-x-slot-modal-title-size: inherit;
|
|
411
|
+
--color-x-slot-modal-close-fg: var(--color-text-muted);
|
|
412
|
+
|
|
392
413
|
/* โโ Shadow slots โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
393
414
|
* ๐ด Shadow was the one dimension that never got slots (#386). 0.9.0 moved "colour, weight
|
|
394
415
|
* and the type scale" through slots and the enumeration in that sentence decided the
|