@-reddacted-/react-ui 0.1.0 → 0.2.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.
Files changed (61) hide show
  1. package/dist/components/Button.d.ts +9 -0
  2. package/dist/components/Button.d.ts.map +1 -0
  3. package/dist/components/Button.js +44 -0
  4. package/dist/components/ColorField.d.ts +9 -0
  5. package/dist/components/ColorField.d.ts.map +1 -0
  6. package/dist/components/ColorField.js +34 -0
  7. package/dist/components/ColorPicker.d.ts +11 -0
  8. package/dist/components/ColorPicker.d.ts.map +1 -0
  9. package/dist/components/ColorPicker.js +103 -0
  10. package/dist/components/ConfirmDeleteButton.d.ts +13 -0
  11. package/dist/components/ConfirmDeleteButton.d.ts.map +1 -0
  12. package/dist/components/ConfirmDeleteButton.js +25 -0
  13. package/dist/components/Modal.d.ts +25 -0
  14. package/dist/components/Modal.d.ts.map +1 -0
  15. package/dist/components/Modal.js +195 -0
  16. package/dist/components/NavBar.d.ts +20 -0
  17. package/dist/components/NavBar.d.ts.map +1 -0
  18. package/dist/components/NavBar.js +31 -0
  19. package/dist/components/PageStatus.d.ts +16 -0
  20. package/dist/components/PageStatus.d.ts.map +1 -0
  21. package/dist/components/PageStatus.js +31 -0
  22. package/dist/index.d.ts +19 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +18 -3
  25. package/dist/net/fetchJson.d.ts +2 -0
  26. package/dist/net/fetchJson.d.ts.map +1 -0
  27. package/dist/net/fetchJson.js +11 -0
  28. package/dist/net/useDocumentTitle.d.ts +2 -0
  29. package/dist/net/useDocumentTitle.d.ts.map +1 -0
  30. package/dist/net/useDocumentTitle.js +8 -0
  31. package/dist/popup/PopupHost.d.ts +8 -0
  32. package/dist/popup/PopupHost.d.ts.map +1 -0
  33. package/dist/popup/PopupHost.js +46 -0
  34. package/dist/popup/PopupProvider.d.ts +9 -0
  35. package/dist/popup/PopupProvider.d.ts.map +1 -0
  36. package/dist/popup/PopupProvider.js +28 -0
  37. package/dist/popup/errorRules.d.ts +23 -0
  38. package/dist/popup/errorRules.d.ts.map +1 -0
  39. package/dist/popup/errorRules.js +59 -0
  40. package/dist/popup/fetchInterceptor.d.ts +7 -0
  41. package/dist/popup/fetchInterceptor.d.ts.map +1 -0
  42. package/dist/popup/fetchInterceptor.js +16 -0
  43. package/dist/theme/ThemeProvider.d.ts +24 -0
  44. package/dist/theme/ThemeProvider.d.ts.map +1 -0
  45. package/dist/theme/ThemeProvider.js +63 -0
  46. package/dist/theme/ThemeSelect.d.ts +9 -0
  47. package/dist/theme/ThemeSelect.d.ts.map +1 -0
  48. package/dist/theme/ThemeSelect.js +17 -0
  49. package/dist/theme/clipboard.d.ts +6 -0
  50. package/dist/theme/clipboard.d.ts.map +1 -0
  51. package/dist/theme/clipboard.js +26 -0
  52. package/dist/theme/palette.d.ts +12 -0
  53. package/dist/theme/palette.d.ts.map +1 -0
  54. package/dist/theme/palette.js +72 -0
  55. package/dist/theme/store.d.ts +11 -0
  56. package/dist/theme/store.d.ts.map +1 -0
  57. package/dist/theme/store.js +92 -0
  58. package/dist/theme/types.d.ts +17 -0
  59. package/dist/theme/types.d.ts.map +1 -0
  60. package/dist/theme/types.js +1 -0
  61. package/package.json +9 -6
@@ -0,0 +1,9 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from "react";
2
+ export type ButtonVariant = "primary" | "secondary" | "ghost";
3
+ type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
4
+ variant?: ButtonVariant;
5
+ children: ReactNode;
6
+ };
7
+ export declare function Button({ variant, style, disabled, children, ...props }: ButtonProps): import("react").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=Button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Button.d.ts","sourceRoot":"","sources":["../../src/components/Button.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAE3E,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,WAAW,GAAG,OAAO,CAAA;AAE7D,KAAK,WAAW,GAAG,oBAAoB,CAAC,iBAAiB,CAAC,GAAG;IAC3D,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,QAAQ,EAAE,SAAS,CAAA;CACpB,CAAA;AAED,wBAAgB,MAAM,CAAC,EAAE,OAAmB,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,KAAK,EAAE,EAAE,WAAW,+BAe/F"}
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ export function Button({ variant = "primary", style, disabled, children, ...props }) {
3
+ return (_jsx("button", { disabled: disabled, style: {
4
+ ...baseStyle,
5
+ ...variantStyles[variant],
6
+ ...(disabled ? disabledStyle : undefined),
7
+ ...style,
8
+ }, ...props, children: children }));
9
+ }
10
+ const baseStyle = {
11
+ display: "inline-flex",
12
+ alignItems: "center",
13
+ justifyContent: "center",
14
+ gap: "0.5rem",
15
+ padding: "0.5rem 1rem",
16
+ borderRadius: "6px",
17
+ fontSize: "0.875rem",
18
+ fontWeight: 500,
19
+ cursor: "pointer",
20
+ border: "1px solid transparent",
21
+ lineHeight: 1,
22
+ whiteSpace: "nowrap",
23
+ };
24
+ const disabledStyle = {
25
+ opacity: 0.4,
26
+ cursor: "not-allowed",
27
+ };
28
+ const variantStyles = {
29
+ primary: {
30
+ backgroundColor: "var(--color-primary)",
31
+ color: "var(--color-on-primary)",
32
+ borderColor: "var(--color-primary)",
33
+ },
34
+ secondary: {
35
+ backgroundColor: "transparent",
36
+ color: "var(--color-primary)",
37
+ borderColor: "var(--color-primary)",
38
+ },
39
+ ghost: {
40
+ backgroundColor: "transparent",
41
+ color: "var(--color-text-muted)",
42
+ borderColor: "transparent",
43
+ },
44
+ };
@@ -0,0 +1,9 @@
1
+ type ColorFieldProps = {
2
+ label: string;
3
+ value: string;
4
+ onChange: (value: string) => void;
5
+ randomLabel?: string;
6
+ };
7
+ export declare function ColorField({ label, value, onChange, randomLabel }: ColorFieldProps): import("react").JSX.Element;
8
+ export {};
9
+ //# sourceMappingURL=ColorField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ColorField.d.ts","sourceRoot":"","sources":["../../src/components/ColorField.tsx"],"names":[],"mappings":"AAKA,KAAK,eAAe,GAAG;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB,CAAA;AAED,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,WAA4B,EAAE,EAAE,eAAe,+BAanG"}
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import ColorPicker from "./ColorPicker.js";
3
+ import { Button } from "./Button.js";
4
+ import { randomHex } from "../theme/clipboard.js";
5
+ export function ColorField({ label, value, onChange, randomLabel = "Random color" }) {
6
+ return (_jsxs("div", { style: rowStyle, children: [_jsx("label", { style: labelStyle, children: label }), _jsxs("div", { style: controlsStyle, children: [_jsx("code", { style: hexStyle, children: value }), _jsx(ColorPicker, { value: value, onChange: onChange, ariaLabel: label }), _jsx(Button, { variant: "ghost", onClick: () => onChange(randomHex()), style: randomBtnStyle, title: randomLabel, children: "\u21BB" })] })] }));
7
+ }
8
+ const rowStyle = {
9
+ display: "flex",
10
+ alignItems: "center",
11
+ justifyContent: "space-between",
12
+ gap: "1rem",
13
+ padding: "0.5rem 0",
14
+ borderBottom: "1px solid var(--color-border)",
15
+ };
16
+ const labelStyle = {
17
+ fontSize: "0.875rem",
18
+ color: "var(--color-text)",
19
+ minWidth: "140px",
20
+ };
21
+ const controlsStyle = {
22
+ display: "flex",
23
+ alignItems: "center",
24
+ gap: "0.5rem",
25
+ };
26
+ const hexStyle = {
27
+ fontSize: "0.8125rem",
28
+ fontFamily: "monospace",
29
+ color: "var(--color-text-muted)",
30
+ };
31
+ const randomBtnStyle = {
32
+ fontSize: "1rem",
33
+ padding: "0.375rem 0.5rem",
34
+ };
@@ -0,0 +1,11 @@
1
+ import type { CSSProperties } from "react";
2
+ type ColorPickerProps = {
3
+ value: string;
4
+ onChange: (value: string) => void;
5
+ className?: string;
6
+ style?: CSSProperties;
7
+ ariaLabel?: string;
8
+ };
9
+ export default function ColorPicker({ value, onChange, className, style, ariaLabel }: ColorPickerProps): import("react").JSX.Element;
10
+ export {};
11
+ //# sourceMappingURL=ColorPicker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ColorPicker.d.ts","sourceRoot":"","sources":["../../src/components/ColorPicker.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAe,aAAa,EAAE,MAAM,OAAO,CAAA;AAIvD,KAAK,gBAAgB,GAAG;IACtB,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAID,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE,gBAAgB,+BA6FrG"}
@@ -0,0 +1,103 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { HexColorPicker } from "react-colorful";
4
+ import { isHexColor } from "../theme/clipboard.js";
5
+ const safe = (value) => (isHexColor(value) ? value : "#000000");
6
+ export default function ColorPicker({ value, onChange, className, style, ariaLabel }) {
7
+ const [open, setOpen] = useState(false);
8
+ const [inputValue, setInputValue] = useState(value);
9
+ const [pos, setPos] = useState({ top: 0, left: 0 });
10
+ const [positioned, setPositioned] = useState(false);
11
+ const [hovered, setHovered] = useState(false);
12
+ const swatchRef = useRef(null);
13
+ const popoverRef = useRef(null);
14
+ useEffect(() => {
15
+ setInputValue(value);
16
+ }, [value]);
17
+ useLayoutEffect(() => {
18
+ if (!open || !popoverRef.current || !swatchRef.current) {
19
+ setPositioned(false);
20
+ return;
21
+ }
22
+ const pr = popoverRef.current.getBoundingClientRect();
23
+ const sr = swatchRef.current.getBoundingClientRect();
24
+ const vw = window.innerWidth;
25
+ const vh = window.innerHeight;
26
+ const gap = 6;
27
+ const top = sr.bottom + gap + pr.height > vh ? Math.max(4, sr.top - pr.height - gap) : sr.bottom + gap;
28
+ const left = sr.left + pr.width > vw ? Math.max(4, vw - pr.width - 8) : sr.left;
29
+ setPos({ top, left });
30
+ setPositioned(true);
31
+ }, [open]);
32
+ useEffect(() => {
33
+ if (!open)
34
+ return;
35
+ const handleMouseDown = (e) => {
36
+ if (!popoverRef.current?.contains(e.target) && !swatchRef.current?.contains(e.target)) {
37
+ setOpen(false);
38
+ }
39
+ };
40
+ document.addEventListener("mousedown", handleMouseDown);
41
+ return () => document.removeEventListener("mousedown", handleMouseDown);
42
+ }, [open]);
43
+ const toggle = () => {
44
+ if (!open)
45
+ setInputValue(value);
46
+ setOpen(o => !o);
47
+ };
48
+ const handleHexInput = (e) => {
49
+ const raw = e.target.value;
50
+ const next = raw.startsWith("#") || raw === "" ? raw : `#${raw}`;
51
+ setInputValue(next);
52
+ if (isHexColor(next))
53
+ onChange(next);
54
+ };
55
+ return (_jsxs("div", { className: className, style: { ...wrapperStyle, ...style }, children: [_jsx("button", { ref: swatchRef, type: "button", style: {
56
+ ...swatchStyle,
57
+ background: safe(value),
58
+ borderColor: hovered ? "var(--color-primary)" : "var(--color-border)",
59
+ }, onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), onClick: toggle, "aria-label": ariaLabel ?? `Color: ${value}` }), open && (_jsxs("div", { ref: popoverRef, style: {
60
+ ...popoverStyle,
61
+ top: pos.top,
62
+ left: pos.left,
63
+ visibility: positioned ? "visible" : "hidden",
64
+ }, onMouseDown: e => e.stopPropagation(), children: [_jsx(HexColorPicker, { color: safe(value), onChange: onChange }), _jsx("input", { type: "text", value: inputValue, onChange: handleHexInput, maxLength: 7, spellCheck: false, style: hexInputStyle })] }))] }));
65
+ }
66
+ const wrapperStyle = {
67
+ position: "relative",
68
+ display: "inline-flex",
69
+ };
70
+ const swatchStyle = {
71
+ width: "32px",
72
+ height: "32px",
73
+ borderRadius: "6px",
74
+ border: "2px solid var(--color-border)",
75
+ cursor: "pointer",
76
+ padding: 0,
77
+ flexShrink: 0,
78
+ transition: "border-color 0.2s",
79
+ };
80
+ const popoverStyle = {
81
+ position: "fixed",
82
+ zIndex: 9999,
83
+ backgroundColor: "var(--color-surface)",
84
+ border: "1px solid var(--color-border)",
85
+ borderRadius: "8px",
86
+ padding: "10px",
87
+ boxShadow: "0 4px 20px color-mix(in srgb, var(--color-text) 25%, transparent)",
88
+ display: "flex",
89
+ flexDirection: "column",
90
+ gap: "8px",
91
+ };
92
+ const hexInputStyle = {
93
+ width: "100%",
94
+ background: "transparent",
95
+ border: "1px solid var(--color-border)",
96
+ borderRadius: "4px",
97
+ color: "var(--color-text)",
98
+ fontSize: "0.85rem",
99
+ fontFamily: "monospace",
100
+ padding: "4px 8px",
101
+ boxSizing: "border-box",
102
+ textAlign: "center",
103
+ };
@@ -0,0 +1,13 @@
1
+ import type { CSSProperties } from "react";
2
+ type Props = {
3
+ onConfirm: () => void;
4
+ label?: string;
5
+ confirmLabel?: string;
6
+ className?: string;
7
+ style?: CSSProperties;
8
+ /** Delay in ms before the confirm state auto-resets (default: 10000) */
9
+ resetDelay?: number;
10
+ };
11
+ export default function ConfirmDeleteButton({ onConfirm, label, confirmLabel, className, style, resetDelay, }: Props): import("react").JSX.Element;
12
+ export {};
13
+ //# sourceMappingURL=ConfirmDeleteButton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ConfirmDeleteButton.d.ts","sourceRoot":"","sources":["../../src/components/ConfirmDeleteButton.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAG1C,KAAK,KAAK,GAAG;IACX,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,MAAM,CAAC,OAAO,UAAU,mBAAmB,CAAC,EAC1C,SAAS,EACT,KAAgB,EAChB,YAA8B,EAC9B,SAAS,EACT,KAAK,EACL,UAAmB,GACpB,EAAE,KAAK,+BAgCP"}
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { Button } from "./Button.js";
4
+ export default function ConfirmDeleteButton({ onConfirm, label = "Delete", confirmLabel = "Are you sure?", className, style, resetDelay = 10000, }) {
5
+ const [confirming, setConfirming] = useState(false);
6
+ useEffect(() => {
7
+ if (!confirming)
8
+ return;
9
+ const timer = setTimeout(() => setConfirming(false), resetDelay);
10
+ return () => clearTimeout(timer);
11
+ }, [confirming, resetDelay]);
12
+ const handleClick = () => {
13
+ if (!confirming) {
14
+ setConfirming(true);
15
+ return;
16
+ }
17
+ onConfirm();
18
+ };
19
+ return (_jsx(Button, { variant: "ghost", onClick: handleClick, className: className, style: {
20
+ color: "var(--color-danger)",
21
+ borderColor: "var(--color-danger)",
22
+ backgroundColor: confirming ? "var(--color-danger-soft)" : "transparent",
23
+ ...style,
24
+ }, children: confirming ? confirmLabel : label }));
25
+ }
@@ -0,0 +1,25 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ export declare function openModal(id: string): void;
3
+ export declare function closeModal(id: string): void;
4
+ type ModalProps = {
5
+ id: string;
6
+ /** Controlled mode: the parent owns visibility and the bus is ignored. */
7
+ open?: boolean;
8
+ /** Called instead of closeModal(id) when the parent owns visibility. */
9
+ onClose?: () => void;
10
+ width?: string | number;
11
+ height?: string | number;
12
+ children: ReactNode;
13
+ /** Renders the framed header with a close button. Omit for a bare card. */
14
+ title?: string;
15
+ footer?: ReactNode;
16
+ closeLabel?: string;
17
+ className?: string;
18
+ style?: CSSProperties;
19
+ preventClose?: boolean;
20
+ ariaLabelledBy?: string;
21
+ ariaDescribedBy?: string;
22
+ };
23
+ export default function Modal({ id, open, onClose, width, height, children, title, footer, closeLabel, className, style, preventClose, ariaLabelledBy, ariaDescribedBy, }: ModalProps): import("react").ReactPortal | null;
24
+ export {};
25
+ //# sourceMappingURL=Modal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Modal.d.ts","sourceRoot":"","sources":["../../src/components/Modal.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAgBrD,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,QAGnC;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,QAEpC;AAID,KAAK,UAAU,GAAG;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,0EAA0E;IAC1E,IAAI,CAAC,EAAE,OAAO,CAAA;IACd,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACvB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IACxB,QAAQ,EAAE,SAAS,CAAA;IACnB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,SAAS,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;IACrB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB,CAAA;AAED,MAAM,CAAC,OAAO,UAAU,KAAK,CAAC,EAC5B,EAAE,EACF,IAAI,EACJ,OAAO,EACP,KAAK,EACL,MAAM,EACN,QAAQ,EACR,KAAK,EACL,MAAM,EACN,UAAoB,EACpB,SAAS,EACT,KAAK,EACL,YAAoB,EACpB,cAAc,EACd,eAAe,GAChB,EAAE,UAAU,sCAkMZ"}
@@ -0,0 +1,195 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+ const listeners = new Map();
5
+ function on(event, cb) {
6
+ if (!listeners.has(event))
7
+ listeners.set(event, new Set());
8
+ listeners.get(event).add(cb);
9
+ return () => listeners.get(event).delete(cb);
10
+ }
11
+ function emit(event, payload) {
12
+ listeners.get(event)?.forEach(cb => cb(payload));
13
+ }
14
+ export function openModal(id) {
15
+ emit("modal:closeAll");
16
+ emit("modal:open", id);
17
+ }
18
+ export function closeModal(id) {
19
+ emit("modal:close", id);
20
+ }
21
+ const FOCUSABLE = `a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])`;
22
+ export default function Modal({ id, open, onClose, width, height, children, title, footer, closeLabel = "Close", className, style, preventClose = false, ariaLabelledBy, ariaDescribedBy, }) {
23
+ const [busOpen, setBusOpen] = useState(false);
24
+ const contentRef = useRef(null);
25
+ const lastFocusedEl = useRef(null);
26
+ const portalTarget = useRef(null);
27
+ const controlled = open !== undefined;
28
+ const isOpen = controlled ? open : busOpen;
29
+ const dismiss = onClose ?? (() => closeModal(id));
30
+ // Gardée dans un ref : la fonction est recréée à chaque rendu et ne doit pas
31
+ // relancer le verrou de défilement ni le placement du focus.
32
+ const dismissRef = useRef(dismiss);
33
+ dismissRef.current = dismiss;
34
+ useEffect(() => {
35
+ if (controlled)
36
+ return;
37
+ const setIsOpen = setBusOpen;
38
+ const offOpen = on("modal:open", (targetId) => {
39
+ if (targetId === id) {
40
+ lastFocusedEl.current = document.activeElement ?? null;
41
+ setIsOpen(true);
42
+ }
43
+ });
44
+ const offClose = on("modal:close", (targetId) => {
45
+ if (targetId === id)
46
+ setIsOpen(false);
47
+ });
48
+ const offCloseAll = on("modal:closeAll", () => setIsOpen(false));
49
+ return () => {
50
+ offOpen();
51
+ offClose();
52
+ offCloseAll();
53
+ };
54
+ }, [id, controlled]);
55
+ useEffect(() => {
56
+ let el = document.getElementById("__modal-root__");
57
+ if (!el) {
58
+ el = document.createElement("div");
59
+ el.id = "__modal-root__";
60
+ document.body.appendChild(el);
61
+ }
62
+ portalTarget.current = el;
63
+ }, []);
64
+ useEffect(() => {
65
+ if (!isOpen)
66
+ return;
67
+ const prevOverflow = document.body.style.overflow;
68
+ const prevPadRight = document.body.style.paddingRight;
69
+ const scrollbarW = window.innerWidth - document.documentElement.clientWidth;
70
+ if (scrollbarW > 0)
71
+ document.body.style.paddingRight = `${scrollbarW}px`;
72
+ document.body.style.overflow = "hidden";
73
+ const to = setTimeout(() => {
74
+ const container = contentRef.current;
75
+ if (!container)
76
+ return;
77
+ const focusables = container.querySelectorAll(FOCUSABLE);
78
+ (focusables[0] ?? container).focus();
79
+ }, 0);
80
+ const onKeyDown = (e) => {
81
+ if (e.key === "Escape" && !preventClose) {
82
+ dismissRef.current();
83
+ return;
84
+ }
85
+ if (e.key !== "Tab")
86
+ return;
87
+ const container = contentRef.current;
88
+ if (!container)
89
+ return;
90
+ const focusables = Array.from(container.querySelectorAll(FOCUSABLE)).filter(el => !el.hasAttribute("disabled") && !el.getAttribute("aria-hidden"));
91
+ if (focusables.length === 0)
92
+ return;
93
+ const first = focusables[0];
94
+ const last = focusables[focusables.length - 1];
95
+ const active = document.activeElement;
96
+ if (e.shiftKey) {
97
+ if (active === first || !container.contains(active)) {
98
+ last.focus();
99
+ e.preventDefault();
100
+ }
101
+ }
102
+ else {
103
+ if (active === last || !container.contains(active)) {
104
+ first.focus();
105
+ e.preventDefault();
106
+ }
107
+ }
108
+ };
109
+ const onFocusIn = (e) => {
110
+ const container = contentRef.current;
111
+ if (!container)
112
+ return;
113
+ if (!container.contains(e.target))
114
+ container.focus();
115
+ };
116
+ document.addEventListener("keydown", onKeyDown);
117
+ document.addEventListener("focusin", onFocusIn);
118
+ return () => {
119
+ document.body.style.overflow = prevOverflow;
120
+ document.body.style.paddingRight = prevPadRight;
121
+ clearTimeout(to);
122
+ document.removeEventListener("keydown", onKeyDown);
123
+ document.removeEventListener("focusin", onFocusIn);
124
+ lastFocusedEl.current?.focus?.();
125
+ lastFocusedEl.current = null;
126
+ };
127
+ }, [isOpen, preventClose]);
128
+ if (!isOpen || !portalTarget.current)
129
+ return null;
130
+ const w = typeof width === "number" ? `${width}px` : (width ?? "min(540px, 95vw)");
131
+ const h = typeof height === "number" ? `${height}px` : height;
132
+ const prefersReducedMotion = typeof window !== "undefined" && window.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
133
+ const scrimStyle = {
134
+ position: "fixed",
135
+ inset: 0,
136
+ zIndex: 9999,
137
+ display: "grid",
138
+ placeItems: "center",
139
+ padding: "1rem",
140
+ backdropFilter: "brightness(0.45) blur(2px)",
141
+ WebkitBackdropFilter: "brightness(0.45) blur(2px)",
142
+ animation: prefersReducedMotion ? "none" : "modalFade 150ms ease-out",
143
+ };
144
+ const cardStyle = {
145
+ width: w,
146
+ height: h,
147
+ maxWidth: "95vw",
148
+ maxHeight: "85vh",
149
+ display: "flex",
150
+ flexDirection: "column",
151
+ overflow: "hidden",
152
+ outline: "none",
153
+ backgroundColor: "var(--color-surface)",
154
+ border: "1px solid var(--color-border)",
155
+ borderRadius: "12px",
156
+ animation: prefersReducedMotion ? "none" : "modalPop 160ms ease-out",
157
+ };
158
+ const keyframes = (_jsx("style", { children: `
159
+ @keyframes modalFade { from { opacity: 0 } to { opacity: 1 } }
160
+ @keyframes modalPop {
161
+ from { opacity: 0; transform: translateY(4px) scale(0.98) }
162
+ to { opacity: 1; transform: translateY(0) scale(1) }
163
+ }
164
+ ` }));
165
+ const framed = title !== undefined || footer !== undefined;
166
+ return createPortal(_jsxs(_Fragment, { children: [keyframes, _jsx("div", { style: scrimStyle, onMouseDown: e => {
167
+ if (!preventClose && e.target === e.currentTarget)
168
+ dismissRef.current();
169
+ }, children: _jsxs("div", { ref: contentRef, role: "dialog", "aria-modal": "true", "aria-labelledby": ariaLabelledBy, "aria-describedby": ariaDescribedBy, tabIndex: -1, className: className, style: { ...cardStyle, ...style }, onMouseDown: e => e.stopPropagation(), children: [title !== undefined && (_jsxs("div", { style: headerStyle, children: [_jsx("span", { style: titleStyle, children: title }), _jsx("button", { type: "button", style: closeStyle, onClick: dismiss, "aria-label": closeLabel, children: "\u2715" })] })), _jsx("div", { style: framed ? bodyStyle : bareBodyStyle, children: children }), footer && _jsx("div", { style: footerStyle, children: footer })] }) })] }), portalTarget.current);
170
+ }
171
+ const headerStyle = {
172
+ display: "flex",
173
+ alignItems: "center",
174
+ justifyContent: "space-between",
175
+ padding: "1rem 1.25rem",
176
+ borderBottom: "1px solid var(--color-border)",
177
+ };
178
+ const titleStyle = { fontSize: "0.95rem", fontWeight: 600, color: "var(--color-text)" };
179
+ const closeStyle = {
180
+ border: "none",
181
+ background: "none",
182
+ cursor: "pointer",
183
+ color: "var(--color-text-muted)",
184
+ fontSize: "0.95rem",
185
+ lineHeight: 1,
186
+ };
187
+ const bodyStyle = { padding: "1.25rem", overflowY: "auto", flex: 1 };
188
+ const bareBodyStyle = { overflowY: "auto", flex: 1 };
189
+ const footerStyle = {
190
+ display: "flex",
191
+ justifyContent: "flex-end",
192
+ gap: "0.75rem",
193
+ padding: "1rem 1.25rem",
194
+ borderTop: "1px solid var(--color-border)",
195
+ };
@@ -0,0 +1,20 @@
1
+ import type { ElementType, ReactNode } from "react";
2
+ export type NavLink = {
3
+ label: string;
4
+ to: string;
5
+ };
6
+ type NavBarProps = {
7
+ brand: string;
8
+ links?: NavLink[];
9
+ /** Rendered on the right of the links: sign out, theme select, whatever the page adds. */
10
+ actions?: ReactNode;
11
+ /**
12
+ * Link component to render with. Defaults to a plain anchor so the bar works
13
+ * without a router; pass a router Link to keep client-side navigation.
14
+ */
15
+ linkAs?: ElementType;
16
+ homeTo?: string;
17
+ };
18
+ export declare function NavBar({ brand, links, actions, linkAs, homeTo }: NavBarProps): import("react").JSX.Element;
19
+ export {};
20
+ //# sourceMappingURL=NavBar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NavBar.d.ts","sourceRoot":"","sources":["../../src/components/NavBar.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,WAAW,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAElE,MAAM,MAAM,OAAO,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,CAAA;AAEnD,KAAK,WAAW,GAAG;IACjB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,OAAO,EAAE,CAAA;IACjB,0FAA0F;IAC1F,OAAO,CAAC,EAAE,SAAS,CAAA;IACnB;;;OAGG;IACH,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,CAAA;AAED,wBAAgB,MAAM,CAAC,EAAE,KAAK,EAAE,KAAU,EAAE,OAAO,EAAE,MAAM,EAAE,MAAY,EAAE,EAAE,WAAW,+BAmBvF"}
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ export function NavBar({ brand, links = [], actions, linkAs, homeTo = "/" }) {
3
+ const Link = linkAs ?? "a";
4
+ const linkProps = (to) => (linkAs ? { to } : { href: to });
5
+ return (_jsxs("nav", { style: navStyle, children: [_jsx(Link, { ...linkProps(homeTo), style: brandStyle, children: brand }), _jsxs("div", { style: rightStyle, children: [links.map(link => (_jsx(Link, { ...linkProps(link.to), style: linkStyle, children: link.label }, link.to))), actions] })] }));
6
+ }
7
+ const navStyle = {
8
+ display: "flex",
9
+ alignItems: "center",
10
+ justifyContent: "space-between",
11
+ padding: "0 1.5rem",
12
+ height: "56px",
13
+ borderBottom: "1px solid var(--color-border)",
14
+ backgroundColor: "var(--color-surface)",
15
+ };
16
+ const brandStyle = {
17
+ fontWeight: 600,
18
+ fontSize: "1rem",
19
+ color: "var(--color-text)",
20
+ textDecoration: "none",
21
+ };
22
+ const rightStyle = {
23
+ display: "flex",
24
+ alignItems: "center",
25
+ gap: "1.25rem",
26
+ };
27
+ const linkStyle = {
28
+ color: "var(--color-text-muted)",
29
+ textDecoration: "none",
30
+ fontSize: "0.875rem",
31
+ };
@@ -0,0 +1,16 @@
1
+ import type { ReactNode } from "react";
2
+ export type LoadStatus = "loading" | "ready" | "error";
3
+ export declare function PageMessage({ children }: {
4
+ children: ReactNode;
5
+ }): import("react").JSX.Element;
6
+ export declare function LoadingMessage({ label }: {
7
+ label?: string;
8
+ }): import("react").JSX.Element;
9
+ type ErrorMessageProps = {
10
+ onRetry?: () => void;
11
+ label?: string;
12
+ retryLabel?: string;
13
+ };
14
+ export declare function ErrorMessage({ onRetry, label, retryLabel, }: ErrorMessageProps): import("react").JSX.Element;
15
+ export {};
16
+ //# sourceMappingURL=PageStatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PageStatus.d.ts","sourceRoot":"","sources":["../../src/components/PageStatus.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAA;AAGrD,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,CAAA;AAEtD,wBAAgB,WAAW,CAAC,EAAE,QAAQ,EAAE,EAAE;IAAE,QAAQ,EAAE,SAAS,CAAA;CAAE,+BAEhE;AAED,wBAAgB,cAAc,CAAC,EAAE,KAAoB,EAAE,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,+BAQ1E;AAED,KAAK,iBAAiB,GAAG;IACvB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,KAAiF,EACjF,UAAwB,GACzB,EAAE,iBAAiB,+BAenB"}
@@ -0,0 +1,31 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button } from "./Button.js";
3
+ export function PageMessage({ children }) {
4
+ return _jsx("div", { style: containerStyle, children: children });
5
+ }
6
+ export function LoadingMessage({ label = "Loading..." }) {
7
+ return (_jsx(PageMessage, { children: _jsx("p", { style: mutedStyle, role: "status", children: label }) }));
8
+ }
9
+ export function ErrorMessage({ onRetry, label = "The content could not be loaded. Check your connection, then try again.", retryLabel = "Try again", }) {
10
+ return (_jsx(PageMessage, { children: _jsxs("div", { style: stackStyle, children: [_jsx("p", { style: mutedStyle, role: "alert", children: label }), onRetry && (_jsx(Button, { variant: "secondary", onClick: onRetry, children: retryLabel }))] }) }));
11
+ }
12
+ const containerStyle = {
13
+ flex: 1,
14
+ display: "flex",
15
+ alignItems: "center",
16
+ justifyContent: "center",
17
+ padding: "2rem",
18
+ backgroundColor: "var(--color-bg)",
19
+ };
20
+ const stackStyle = {
21
+ display: "flex",
22
+ flexDirection: "column",
23
+ alignItems: "center",
24
+ gap: "1rem",
25
+ textAlign: "center",
26
+ };
27
+ const mutedStyle = {
28
+ margin: 0,
29
+ fontSize: "0.9375rem",
30
+ color: "var(--color-text-muted)",
31
+ };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,20 @@
1
- export { default as Modal, openModal, closeModal } from "./Modal";
2
- export { default as ColorPicker } from "./ColorPicker";
3
- export { default as ConfirmDeleteButton } from "./ConfirmDeleteButton";
1
+ export { default as Modal, openModal, closeModal } from "./components/Modal.js";
2
+ export { default as ColorPicker } from "./components/ColorPicker.js";
3
+ export { default as ConfirmDeleteButton } from "./components/ConfirmDeleteButton.js";
4
+ export { ColorField } from "./components/ColorField.js";
5
+ export { Button, type ButtonVariant } from "./components/Button.js";
6
+ export { NavBar, type NavLink } from "./components/NavBar.js";
7
+ export { PageMessage, LoadingMessage, ErrorMessage, type LoadStatus } from "./components/PageStatus.js";
8
+ export { ThemeProvider, useTheme } from "./theme/ThemeProvider.js";
9
+ export { ThemeSelect } from "./theme/ThemeSelect.js";
10
+ export { defaultPalette, themeVariableGroups, themeVariableKeys, type ThemeVariableGroup } from "./theme/palette.js";
11
+ export { createLocalThemeStore, createHttpThemeStore, type ThemeStore } from "./theme/store.js";
12
+ export { serializeColors, parseColors, isHexColor, randomHex } from "./theme/clipboard.js";
13
+ export type { ThemeColors, ThemeDefinition, ThemeCatalog, ThemeDraft } from "./theme/types.js";
14
+ export { PopupProvider } from "./popup/PopupProvider.js";
15
+ export { PopupHost } from "./popup/PopupHost.js";
16
+ export { createInterceptedFetch, type InterceptorHandlers } from "./popup/fetchInterceptor.js";
17
+ export { createErrorRules, defaultMessages, type ErrorRules, type ErrorRulesOptions, type PopupMessages, } from "./popup/errorRules.js";
18
+ export { fetchJson } from "./net/fetchJson.js";
19
+ export { useDocumentTitle } from "./net/useDocumentTitle.js";
4
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AACjE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,eAAe,CAAA;AACtD,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,uBAAuB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAA;AAC/E,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,6BAA6B,CAAA;AACpE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,qCAAqC,CAAA;AACpF,OAAO,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AACvD,OAAO,EAAE,MAAM,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAA;AACnE,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAEvG,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AACpH,OAAO,EAAE,qBAAqB,EAAE,oBAAoB,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC/F,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAC1F,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAE9F,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAA;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,KAAK,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAC9F,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,aAAa,GACnB,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAC9C,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAA"}