@-reddacted-/react-ui 0.3.1 → 0.4.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.
Files changed (136) hide show
  1. package/dist/components/Accordion.d.ts +27 -0
  2. package/dist/components/Accordion.d.ts.map +1 -0
  3. package/dist/components/Accordion.js +35 -0
  4. package/dist/components/Badge.d.ts +15 -0
  5. package/dist/components/Badge.d.ts.map +1 -0
  6. package/dist/components/Badge.js +7 -0
  7. package/dist/components/Button.d.ts +15 -2
  8. package/dist/components/Button.d.ts.map +1 -1
  9. package/dist/components/Button.js +76 -44
  10. package/dist/components/Card.d.ts +21 -0
  11. package/dist/components/Card.d.ts.map +1 -0
  12. package/dist/components/Card.js +17 -0
  13. package/dist/components/ColorField.d.ts.map +1 -1
  14. package/dist/components/ColorField.js +1 -1
  15. package/dist/components/ConfirmDeleteButton.d.ts +4 -1
  16. package/dist/components/ConfirmDeleteButton.d.ts.map +1 -1
  17. package/dist/components/ConfirmDeleteButton.js +6 -7
  18. package/dist/components/Drawer.d.ts +33 -0
  19. package/dist/components/Drawer.d.ts.map +1 -0
  20. package/dist/components/Drawer.js +123 -0
  21. package/dist/components/DropdownMenu.d.ts +61 -0
  22. package/dist/components/DropdownMenu.d.ts.map +1 -0
  23. package/dist/components/DropdownMenu.js +302 -0
  24. package/dist/components/Modal.d.ts.map +1 -1
  25. package/dist/components/Modal.js +102 -134
  26. package/dist/components/NavBar.d.ts.map +1 -1
  27. package/dist/components/NavBar.js +4 -2
  28. package/dist/components/PageStatus.d.ts +12 -12
  29. package/dist/components/PageStatus.d.ts.map +1 -1
  30. package/dist/components/PageStatus.js +9 -29
  31. package/dist/components/Pagination.d.ts +44 -0
  32. package/dist/components/Pagination.d.ts.map +1 -0
  33. package/dist/components/Pagination.js +57 -0
  34. package/dist/components/Skeleton.d.ts +13 -0
  35. package/dist/components/Skeleton.d.ts.map +1 -0
  36. package/dist/components/Skeleton.js +15 -0
  37. package/dist/components/Spinner.d.ts +14 -0
  38. package/dist/components/Spinner.d.ts.map +1 -0
  39. package/dist/components/Spinner.js +14 -0
  40. package/dist/components/Stack.d.ts +13 -0
  41. package/dist/components/Stack.d.ts.map +1 -0
  42. package/dist/components/Stack.js +15 -0
  43. package/dist/components/Table.d.ts +54 -0
  44. package/dist/components/Table.d.ts.map +1 -0
  45. package/dist/components/Table.js +101 -0
  46. package/dist/components/Tabs.d.ts +34 -0
  47. package/dist/components/Tabs.d.ts.map +1 -0
  48. package/dist/components/Tabs.js +82 -0
  49. package/dist/components/Tag.d.ts +10 -0
  50. package/dist/components/Tag.d.ts.map +1 -0
  51. package/dist/components/Tag.js +7 -0
  52. package/dist/components/Tooltip.d.ts +22 -0
  53. package/dist/components/Tooltip.d.ts.map +1 -0
  54. package/dist/components/Tooltip.js +76 -0
  55. package/dist/components/drawerStore.d.ts +11 -0
  56. package/dist/components/drawerStore.d.ts.map +1 -0
  57. package/dist/components/drawerStore.js +40 -0
  58. package/dist/components/paginationItems.d.ts +18 -0
  59. package/dist/components/paginationItems.d.ts.map +1 -0
  60. package/dist/components/paginationItems.js +32 -0
  61. package/dist/index.d.ts +22 -6
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +16 -2
  64. package/dist/popup/PopupHost.d.ts +19 -3
  65. package/dist/popup/PopupHost.d.ts.map +1 -1
  66. package/dist/popup/PopupHost.js +65 -44
  67. package/dist/popup/PopupProvider.d.ts +19 -2
  68. package/dist/popup/PopupProvider.d.ts.map +1 -1
  69. package/dist/popup/PopupProvider.js +69 -17
  70. package/dist/popup/usePopup.d.ts +11 -4
  71. package/dist/popup/usePopup.d.ts.map +1 -1
  72. package/dist/style/accordionCss.d.ts +2 -0
  73. package/dist/style/accordionCss.d.ts.map +1 -0
  74. package/dist/style/accordionCss.js +101 -0
  75. package/dist/style/badgeCss.d.ts +5 -0
  76. package/dist/style/badgeCss.d.ts.map +1 -0
  77. package/dist/style/badgeCss.js +68 -0
  78. package/dist/style/breadcrumbCss.d.ts +2 -0
  79. package/dist/style/breadcrumbCss.d.ts.map +1 -0
  80. package/dist/style/breadcrumbCss.js +53 -0
  81. package/dist/style/cardCss.d.ts +2 -0
  82. package/dist/style/cardCss.d.ts.map +1 -0
  83. package/dist/style/cardCss.js +49 -0
  84. package/dist/style/inputCss.d.ts +2 -0
  85. package/dist/style/inputCss.d.ts.map +1 -0
  86. package/dist/style/inputCss.js +107 -0
  87. package/dist/style/loaderCss.d.ts +2 -0
  88. package/dist/style/loaderCss.d.ts.map +1 -0
  89. package/dist/style/loaderCss.js +39 -0
  90. package/dist/style/menuCss.d.ts +2 -0
  91. package/dist/style/menuCss.d.ts.map +1 -0
  92. package/dist/style/menuCss.js +71 -0
  93. package/dist/style/paginationCss.d.ts +2 -0
  94. package/dist/style/paginationCss.d.ts.map +1 -0
  95. package/dist/style/paginationCss.js +57 -0
  96. package/dist/style/resolveColor.d.ts +8 -0
  97. package/dist/style/resolveColor.d.ts.map +1 -0
  98. package/dist/style/resolveColor.js +13 -0
  99. package/dist/style/stackCss.d.ts +2 -0
  100. package/dist/style/stackCss.d.ts.map +1 -0
  101. package/dist/style/stackCss.js +21 -0
  102. package/dist/style/statusCss.d.ts +2 -0
  103. package/dist/style/statusCss.d.ts.map +1 -0
  104. package/dist/style/statusCss.js +38 -0
  105. package/dist/style/tableCss.d.ts +2 -0
  106. package/dist/style/tableCss.d.ts.map +1 -0
  107. package/dist/style/tableCss.js +92 -0
  108. package/dist/style/tabsCss.d.ts +2 -0
  109. package/dist/style/tabsCss.d.ts.map +1 -0
  110. package/dist/style/tabsCss.js +72 -0
  111. package/dist/style/toLength.d.ts +4 -0
  112. package/dist/style/toLength.d.ts.map +1 -0
  113. package/dist/style/toLength.js +3 -0
  114. package/dist/style/tooltipCss.d.ts +2 -0
  115. package/dist/style/tooltipCss.d.ts.map +1 -0
  116. package/dist/style/tooltipCss.js +44 -0
  117. package/dist/style/useStyle.d.ts +7 -0
  118. package/dist/style/useStyle.d.ts.map +1 -0
  119. package/dist/style/useStyle.js +16 -0
  120. package/dist/theme/ThemeProvider.d.ts.map +1 -1
  121. package/dist/theme/ThemeProvider.js +32 -0
  122. package/dist/theme/palette.d.ts.map +1 -1
  123. package/dist/theme/palette.js +0 -2
  124. package/dist/tooltip/attributeTooltips.d.ts +7 -0
  125. package/dist/tooltip/attributeTooltips.d.ts.map +1 -0
  126. package/dist/tooltip/attributeTooltips.js +76 -0
  127. package/dist/tooltip/tooltipEngine.d.ts +41 -0
  128. package/dist/tooltip/tooltipEngine.d.ts.map +1 -0
  129. package/dist/tooltip/tooltipEngine.js +128 -0
  130. package/dist/utils/childRef.d.ts +5 -0
  131. package/dist/utils/childRef.d.ts.map +1 -0
  132. package/dist/utils/childRef.js +14 -0
  133. package/dist/utils/isDev.d.ts +3 -0
  134. package/dist/utils/isDev.d.ts.map +1 -0
  135. package/dist/utils/isDev.js +4 -0
  136. package/package.json +5 -4
@@ -0,0 +1,76 @@
1
+ import { Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Children, cloneElement, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { tooltipCss } from "../style/tooltipCss.js";
5
+ import { useStyle } from "../style/useStyle.js";
6
+ import { TooltipEngine } from "../tooltip/tooltipEngine.js";
7
+ import { assignRef, getElementRef } from "../utils/childRef.js";
8
+ import { isDev } from "../utils/isDev.js";
9
+ const INTERACTIVE = "a[href], button, input, select, textarea, [tabindex]";
10
+ export function Tooltip({ content, placement = "top", delay = 300, color, textColor, maxWidth, disabled = false, children, }) {
11
+ useStyle("tooltip", tooltipCss);
12
+ const [open, setOpen] = useState(false);
13
+ const [engine] = useState(() => new TooltipEngine(setOpen));
14
+ const anchorRef = useRef(null);
15
+ const child = Children.only(children);
16
+ const childProps = child.props;
17
+ const childRef = getElementRef(child);
18
+ const setAnchor = useCallback((node) => {
19
+ anchorRef.current = node;
20
+ assignRef(childRef, node);
21
+ }, [childRef]);
22
+ useLayoutEffect(() => {
23
+ engine.setLook({ placement, color, textColor, maxWidth });
24
+ }, [engine, placement, color, textColor, maxWidth]);
25
+ useEffect(() => {
26
+ if (disabled)
27
+ engine.hide();
28
+ }, [engine, disabled]);
29
+ useEffect(() => () => engine.hide(), [engine]);
30
+ useEffect(() => {
31
+ if (isDev() && !anchorRef.current) {
32
+ console.warn("Tooltip: the child did not receive the ref (use a DOM element or a forwardRef component).");
33
+ }
34
+ }, []);
35
+ useEffect(() => {
36
+ if (open && isDev() && engine.content.querySelector(INTERACTIVE)) {
37
+ console.warn("Tooltip: the content holds an interactive element, which can never be reached.");
38
+ }
39
+ }, [engine, open, content]);
40
+ const onPointerEnter = (event) => {
41
+ childProps.onPointerEnter?.(event);
42
+ if (!disabled && event.pointerType !== "touch")
43
+ engine.scheduleShow(event.currentTarget, delay);
44
+ };
45
+ const onPointerLeave = (event) => {
46
+ childProps.onPointerLeave?.(event);
47
+ if (event.pointerType !== "touch")
48
+ engine.hide();
49
+ };
50
+ const onPointerDown = (event) => {
51
+ childProps.onPointerDown?.(event);
52
+ if (!disabled && event.pointerType === "touch")
53
+ engine.showForTouch(event.currentTarget);
54
+ };
55
+ const onFocus = (event) => {
56
+ childProps.onFocus?.(event);
57
+ if (!disabled)
58
+ engine.show(event.currentTarget);
59
+ };
60
+ const onBlur = (event) => {
61
+ childProps.onBlur?.(event);
62
+ engine.hide();
63
+ };
64
+ const describedBy = open
65
+ ? [childProps["aria-describedby"], engine.id].filter(Boolean).join(" ")
66
+ : childProps["aria-describedby"];
67
+ return (_jsxs(_Fragment, { children: [cloneElement(child, {
68
+ ref: setAnchor,
69
+ "aria-describedby": describedBy,
70
+ onPointerEnter,
71
+ onPointerLeave,
72
+ onPointerDown,
73
+ onFocus,
74
+ onBlur,
75
+ }), createPortal(content, engine.content)] }));
76
+ }
@@ -0,0 +1,11 @@
1
+ export declare function openDrawer(id: string): void;
2
+ export declare function closeDrawer(id: string): void;
3
+ export declare function toggleDrawer(id: string): void;
4
+ /** Reads whether drawer `id` is open by call, with bound `open` / `close` / `toggle`. */
5
+ export declare function useDrawer(id: string): {
6
+ isOpen: boolean;
7
+ open: () => void;
8
+ close: () => void;
9
+ toggle: () => void;
10
+ };
11
+ //# sourceMappingURL=drawerStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drawerStore.d.ts","sourceRoot":"","sources":["../../src/components/drawerStore.ts"],"names":[],"mappings":"AAqBA,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAE3C;AAED,wBAAgB,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAE5C;AAED,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAE7C;AAED,yFAAyF;AACzF,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,IAAI,CAAA;CAAE,CAYlH"}
@@ -0,0 +1,40 @@
1
+ import { useCallback, useMemo, useSyncExternalStore } from "react";
2
+ // Drawers opened by call, by id. Kept apart from Drawer.tsx so the component file
3
+ // only exports components (react-refresh rule).
4
+ const openIds = new Set();
5
+ const listeners = new Set();
6
+ function subscribe(listener) {
7
+ listeners.add(listener);
8
+ return () => {
9
+ listeners.delete(listener);
10
+ };
11
+ }
12
+ function setDrawerOpen(id, open) {
13
+ if (openIds.has(id) === open)
14
+ return;
15
+ if (open)
16
+ openIds.add(id);
17
+ else
18
+ openIds.delete(id);
19
+ listeners.forEach(listener => listener());
20
+ }
21
+ export function openDrawer(id) {
22
+ setDrawerOpen(id, true);
23
+ }
24
+ export function closeDrawer(id) {
25
+ setDrawerOpen(id, false);
26
+ }
27
+ export function toggleDrawer(id) {
28
+ setDrawerOpen(id, !openIds.has(id));
29
+ }
30
+ /** Reads whether drawer `id` is open by call, with bound `open` / `close` / `toggle`. */
31
+ export function useDrawer(id) {
32
+ const getSnapshot = useCallback(() => openIds.has(id), [id]);
33
+ const isOpen = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
34
+ return useMemo(() => ({
35
+ isOpen,
36
+ open: () => openDrawer(id),
37
+ close: () => closeDrawer(id),
38
+ toggle: () => toggleDrawer(id),
39
+ }), [id, isOpen]);
40
+ }
@@ -0,0 +1,18 @@
1
+ export type PaginationItem = {
2
+ type: "page";
3
+ page: number;
4
+ current: boolean;
5
+ } | {
6
+ type: "first" | "previous" | "next" | "last";
7
+ page: number;
8
+ disabled: boolean;
9
+ } | {
10
+ type: "ellipsis";
11
+ };
12
+ /**
13
+ * Items to render for `page` of `total` (1-based): edges, previous / next, the first and
14
+ * last pages, `siblings` pages around the current one, and ellipses for the gaps.
15
+ * A gap of a single page shows that page instead of an ellipsis.
16
+ */
17
+ export declare function getPaginationItems(page: number, total: number, siblings: number, showEdges: boolean): PaginationItem[];
18
+ //# sourceMappingURL=paginationItems.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paginationItems.d.ts","sourceRoot":"","sources":["../../src/components/paginationItems.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,cAAc,GACtB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,GAChD;IAAE,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,GACjF;IAAE,IAAI,EAAE,UAAU,CAAA;CAAE,CAAA;AAExB;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,cAAc,EAAE,CAyBtH"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Items to render for `page` of `total` (1-based): edges, previous / next, the first and
3
+ * last pages, `siblings` pages around the current one, and ellipses for the gaps.
4
+ * A gap of a single page shows that page instead of an ellipsis.
5
+ */
6
+ export function getPaginationItems(page, total, siblings, showEdges) {
7
+ const last = Math.max(1, total);
8
+ const current = Math.min(Math.max(1, page), last);
9
+ const shown = new Set([1, last]);
10
+ for (let p = current - siblings; p <= current + siblings; p++) {
11
+ if (p >= 1 && p <= last)
12
+ shown.add(p);
13
+ }
14
+ const pages = [...shown].sort((a, b) => a - b);
15
+ const items = [];
16
+ if (showEdges)
17
+ items.push({ type: "first", page: 1, disabled: current === 1 });
18
+ items.push({ type: "previous", page: current - 1, disabled: current === 1 });
19
+ let previous = 0;
20
+ for (const p of pages) {
21
+ if (p - previous === 2)
22
+ items.push({ type: "page", page: p - 1, current: false });
23
+ else if (p - previous > 2)
24
+ items.push({ type: "ellipsis" });
25
+ items.push({ type: "page", page: p, current: p === current });
26
+ previous = p;
27
+ }
28
+ items.push({ type: "next", page: current + 1, disabled: current === last });
29
+ if (showEdges)
30
+ items.push({ type: "last", page: last, disabled: current === last });
31
+ return items;
32
+ }
package/dist/index.d.ts CHANGED
@@ -2,21 +2,37 @@ export { default as Modal, openModal, closeModal } from "./components/Modal.js";
2
2
  export { default as ColorPicker } from "./components/ColorPicker.js";
3
3
  export { default as ConfirmDeleteButton } from "./components/ConfirmDeleteButton.js";
4
4
  export { ColorField } from "./components/ColorField.js";
5
- export { Button, type ButtonVariant } from "./components/Button.js";
5
+ export { Button, type ButtonVariant, type ThemeColorVar } from "./components/Button.js";
6
6
  export { NavBar } from "./components/NavBar.js";
7
- export { PageMessage, LoadingMessage, ErrorMessage } from "./components/PageStatus.js";
7
+ export { Spinner } from "./components/Spinner.js";
8
+ export { Skeleton } from "./components/Skeleton.js";
9
+ export { Card } from "./components/Card.js";
10
+ export { Stack } from "./components/Stack.js";
11
+ export { Badge, type BadgeVariant } from "./components/Badge.js";
12
+ export { Tag } from "./components/Tag.js";
13
+ export { Tabs } from "./components/Tabs.js";
14
+ export { Tooltip } from "./components/Tooltip.js";
15
+ export { DropdownMenu } from "./components/DropdownMenu.js";
16
+ export { Table, type TableColumn, type SortState } from "./components/Table.js";
17
+ export type { TooltipPlacement } from "./tooltip/tooltipEngine.js";
18
+ export { Drawer, type DrawerSide, type DrawerMode } from "./components/Drawer.js";
19
+ export { openDrawer, closeDrawer, toggleDrawer, useDrawer } from "./components/drawerStore.js";
20
+ export { PageStatus } from "./components/PageStatus.js";
21
+ export { Pagination } from "./components/Pagination.js";
22
+ export { Accordion } from "./components/Accordion.js";
23
+ export type { PaginationItem } from "./components/paginationItems.js";
8
24
  export { ThemeProvider, useTheme } from "./theme/ThemeProvider.js";
9
25
  export { ThemeSelect } from "./theme/ThemeSelect.js";
10
26
  export { defaultPalette } from "./theme/palette.js";
11
27
  export { createLocalThemeStore, createHttpThemeStore, type ThemeStore } from "./theme/store.js";
12
28
  export { serializeColors, parseColors, isHexColor, randomHex } from "./theme/clipboard.js";
13
29
  export type { ThemeColors, ThemeDefinition, ThemeCatalog, ThemeDraft } from "./theme/types.js";
14
- export { PopupProvider, type InterceptFetchMode } from "./popup/PopupProvider.js";
15
- export { usePopup, type PopupApi } from "./popup/usePopup.js";
16
- export { PopupHost } from "./popup/PopupHost.js";
30
+ export { PopupProvider, type InterceptFetchMode, type ToastGroupConfig } from "./popup/PopupProvider.js";
31
+ export { usePopup, type PopupApi, type PushOptions } from "./popup/usePopup.js";
32
+ export { PopupHost, type ToastBox } from "./popup/PopupHost.js";
17
33
  export { createInterceptedFetch, type InterceptorHandlers } from "./popup/fetchInterceptor.js";
18
34
  export { createErrorRules, createGraphqlErrorRules, defaultMessages, type ErrorRules, type ErrorRulesOptions, type GraphqlError, type GraphqlErrorRulesOptions, type PopupMessages, type FetchErrorReport, } from "./popup/errorRules.js";
19
35
  export { fetchJson } from "./net/fetchJson.js";
20
- export { useDocumentTitle, useAsync, useElementSize, useClickOutside, useEscapeKey } from "@-reddacted-/react-hooks";
36
+ export { useDocumentTitle, useAsync, useElementSize, useClickOutside, useEscapeKey, useFocusTrap, useScrollLock, useOverlayStack, } from "@-reddacted-/react-hooks";
21
37
  export type { AsyncStatus, AsyncStatus as LoadStatus, AsyncResult, ElementSize } from "@-reddacted-/react-hooks";
22
38
  //# 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,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,MAAM,wBAAwB,CAAA;AAC/C,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAEtF,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAA;AAClE,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAA;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,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,KAAK,kBAAkB,EAAE,MAAM,0BAA0B,CAAA;AACjF,OAAO,EAAE,QAAQ,EAAE,KAAK,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAC7D,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,KAAK,mBAAmB,EAAE,MAAM,6BAA6B,CAAA;AAC9F,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,eAAe,EACf,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,YAAY,EACjB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,uBAAuB,CAAA;AAE9B,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAA;AAG9C,OAAO,EAAE,gBAAgB,EAAE,QAAQ,EAAE,cAAc,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AACpH,YAAY,EAAE,WAAW,EAAE,WAAW,IAAI,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -4,7 +4,21 @@ export { default as ConfirmDeleteButton } from "./components/ConfirmDeleteButton
4
4
  export { ColorField } from "./components/ColorField.js";
5
5
  export { Button } from "./components/Button.js";
6
6
  export { NavBar } from "./components/NavBar.js";
7
- export { PageMessage, LoadingMessage, ErrorMessage } from "./components/PageStatus.js";
7
+ export { Spinner } from "./components/Spinner.js";
8
+ export { Skeleton } from "./components/Skeleton.js";
9
+ export { Card } from "./components/Card.js";
10
+ export { Stack } from "./components/Stack.js";
11
+ export { Badge } from "./components/Badge.js";
12
+ export { Tag } from "./components/Tag.js";
13
+ export { Tabs } from "./components/Tabs.js";
14
+ export { Tooltip } from "./components/Tooltip.js";
15
+ export { DropdownMenu } from "./components/DropdownMenu.js";
16
+ export { Table } from "./components/Table.js";
17
+ export { Drawer } from "./components/Drawer.js";
18
+ export { openDrawer, closeDrawer, toggleDrawer, useDrawer } from "./components/drawerStore.js";
19
+ export { PageStatus } from "./components/PageStatus.js";
20
+ export { Pagination } from "./components/Pagination.js";
21
+ export { Accordion } from "./components/Accordion.js";
8
22
  export { ThemeProvider, useTheme } from "./theme/ThemeProvider.js";
9
23
  export { ThemeSelect } from "./theme/ThemeSelect.js";
10
24
  export { defaultPalette } from "./theme/palette.js";
@@ -17,4 +31,4 @@ export { createInterceptedFetch } from "./popup/fetchInterceptor.js";
17
31
  export { createErrorRules, createGraphqlErrorRules, defaultMessages, } from "./popup/errorRules.js";
18
32
  export { fetchJson } from "./net/fetchJson.js";
19
33
  // Réexportés depuis react-hooks : les appelants n'ont pas à connaître le découpage.
20
- export { useDocumentTitle, useAsync, useElementSize, useClickOutside, useEscapeKey } from "@-reddacted-/react-hooks";
34
+ export { useDocumentTitle, useAsync, useElementSize, useClickOutside, useEscapeKey, useFocusTrap, useScrollLock, useOverlayStack, } from "@-reddacted-/react-hooks";
@@ -1,8 +1,24 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ /** One group's box: every message of the group, stacked. */
3
+ export type ToastBox = {
4
+ group: string;
5
+ /** `"--color-x"` reads the theme variable; anything else is a raw CSS color. */
6
+ color: string;
7
+ className?: string;
8
+ items: {
9
+ id: number | string;
10
+ content: ReactNode;
11
+ }[];
12
+ };
1
13
  type PopupHostProps = {
2
- messages: string[];
3
- onDismiss: () => void;
14
+ boxes: ToastBox[];
15
+ /** Called with the group of the box whose × was clicked. */
16
+ onDismiss: (group: string) => void;
4
17
  closeLabel?: string;
18
+ /** Set on the `.rui-toasts` container. */
19
+ className?: string;
20
+ style?: CSSProperties;
5
21
  };
6
- export declare function PopupHost({ messages, onDismiss, closeLabel }: PopupHostProps): import("react").JSX.Element | null;
22
+ export declare function PopupHost({ boxes, onDismiss, closeLabel, className, style }: PopupHostProps): import("react").JSX.Element | null;
7
23
  export {};
8
24
  //# sourceMappingURL=PopupHost.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PopupHost.d.ts","sourceRoot":"","sources":["../../src/popup/PopupHost.tsx"],"names":[],"mappings":"AAEA,KAAK,cAAc,GAAG;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,SAAS,EAAE,MAAM,IAAI,CAAA;IACrB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,wBAAgB,SAAS,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAoB,EAAE,EAAE,cAAc,sCAiBtF"}
1
+ {"version":3,"file":"PopupHost.d.ts","sourceRoot":"","sources":["../../src/popup/PopupHost.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAIrD,4DAA4D;AAC5D,MAAM,MAAM,QAAQ,GAAG;IACrB,KAAK,EAAE,MAAM,CAAA;IACb,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,OAAO,EAAE,SAAS,CAAA;KAAE,EAAE,CAAA;CACrD,CAAA;AAED,KAAK,cAAc,GAAG;IACpB,KAAK,EAAE,QAAQ,EAAE,CAAA;IACjB,4DAA4D;IAC5D,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAClC,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,0CAA0C;IAC1C,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB,CAAA;AAED,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,UAAoB,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,cAAc,sCA6BrG"}
@@ -1,46 +1,67 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- export function PopupHost({ messages, onDismiss, closeLabel = "Close" }) {
3
- if (messages.length === 0)
2
+ import { resolveColor } from "../style/resolveColor.js";
3
+ import { useStyle } from "../style/useStyle.js";
4
+ export function PopupHost({ boxes, onDismiss, closeLabel = "Close", className, style }) {
5
+ useStyle("toast", toastCss);
6
+ if (boxes.length === 0)
4
7
  return null;
5
- return (_jsxs("div", { role: "alert", "aria-live": "assertive", style: hostStyle, children: [_jsx("div", { style: bodyStyle, children: messages.map(message => (_jsx("p", { style: messageStyle, children: message }, message))) }), _jsx("button", { type: "button", "aria-label": closeLabel, onClick: onDismiss, style: closeStyle, children: "\u00D7" })] }));
6
- }
7
- const hostStyle = {
8
- position: "fixed",
9
- top: "1rem",
10
- left: "50%",
11
- transform: "translateX(-50%)",
12
- zIndex: 2000,
13
- display: "flex",
14
- alignItems: "flex-start",
15
- gap: "0.75rem",
16
- maxWidth: "min(32rem, calc(100vw - 2rem))",
17
- padding: "0.75rem 0.75rem 0.75rem 1rem",
18
- borderRadius: "8px",
19
- border: "1px solid var(--color-danger)",
20
- backgroundColor: "var(--color-danger-soft)",
21
- boxShadow: "0 4px 16px color-mix(in srgb, var(--color-text) 15%, transparent)",
22
- };
23
- const bodyStyle = {
24
- display: "flex",
25
- flexDirection: "column",
26
- gap: "0.25rem",
27
- };
28
- const messageStyle = {
29
- margin: 0,
30
- fontSize: "0.875rem",
31
- lineHeight: 1.4,
32
- color: "var(--color-text)",
33
- };
34
- const closeStyle = {
35
- flexShrink: 0,
36
- padding: 0,
37
- width: "1.5rem",
38
- height: "1.5rem",
39
- lineHeight: 1,
40
- fontSize: "1.125rem",
41
- cursor: "pointer",
42
- border: "none",
43
- borderRadius: "4px",
44
- backgroundColor: "transparent",
45
- color: "var(--color-danger)",
46
- };
8
+ return (_jsx("div", { className: className ? `rui-toasts ${className}` : "rui-toasts", style: style, children: boxes.map(box => (_jsxs("div", { role: "alert", "aria-live": "assertive", className: box.className ? `rui-toast ${box.className}` : "rui-toast", "data-group": box.group || undefined, style: { "--rui-toast-color": resolveColor(box.color) }, children: [_jsx("div", { className: "rui-toast-body", children: box.items.map(item => (_jsx("div", { className: "rui-toast-message", children: item.content }, item.id))) }), _jsx("button", { type: "button", className: "rui-toast-close", "aria-label": closeLabel, onClick: () => onDismiss(box.group), children: "\u00D7" })] }, box.group))) }));
9
+ }
10
+ // Every selector is wrapped in :where() so it has zero specificity:
11
+ // any class or style set by the consuming project wins without !important.
12
+ // Above the overlay stack (modals start at 1000) so errors stay visible over a modal.
13
+ const toastCss = `
14
+ :where(.rui-toasts) {
15
+ position: fixed;
16
+ top: 1rem;
17
+ left: 50%;
18
+ transform: translateX(-50%);
19
+ z-index: 2000;
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: 0.5rem;
23
+ width: max-content;
24
+ max-width: min(32rem, calc(100vw - 2rem));
25
+ }
26
+ :where(.rui-toast) {
27
+ display: flex;
28
+ align-items: flex-start;
29
+ gap: 0.75rem;
30
+ padding: 0.75rem 0.75rem 0.75rem 1rem;
31
+ border: 1px solid var(--rui-toast-color);
32
+ border-radius: 8px;
33
+ background-color: color-mix(in srgb, var(--rui-toast-color) 12%, var(--color-surface));
34
+ box-shadow: 0 4px 16px color-mix(in srgb, var(--color-text) 15%, transparent);
35
+ }
36
+ :where(.rui-toast-body) {
37
+ flex: 1;
38
+ display: flex;
39
+ flex-direction: column;
40
+ gap: 0.25rem;
41
+ }
42
+ :where(.rui-toast-message) {
43
+ font-size: 0.875rem;
44
+ line-height: 1.4;
45
+ color: var(--color-text);
46
+ }
47
+ :where(.rui-toast-close) {
48
+ flex-shrink: 0;
49
+ width: 1.5rem;
50
+ height: 1.5rem;
51
+ padding: 0;
52
+ border: none;
53
+ border-radius: 4px;
54
+ background-color: transparent;
55
+ color: var(--rui-toast-color);
56
+ font-size: 1.125rem;
57
+ line-height: 1;
58
+ cursor: pointer;
59
+ }
60
+ :where(.rui-toast-close:hover) {
61
+ background-color: color-mix(in srgb, var(--rui-toast-color) 12%, transparent);
62
+ }
63
+ :where(.rui-toast-close:focus-visible) {
64
+ outline: 2px solid var(--rui-toast-color);
65
+ outline-offset: 1px;
66
+ }
67
+ `;
@@ -1,19 +1,36 @@
1
- import type { ReactNode } from "react";
1
+ import type { CSSProperties, ReactNode } from "react";
2
2
  import type { FetchErrorReport, GraphqlErrorRulesOptions } from "./errorRules.js";
3
3
  /** Which `window.fetch` failures raise a popup on their own; `false` leaves fetch untouched. */
4
4
  export type InterceptFetchMode = false | "rest" | "graphql";
5
+ export type ToastGroupConfig = {
6
+ /** `"--color-x"` reads the theme variable; anything else is a raw CSS color. Defaults to the provider's `color`. */
7
+ color?: string;
8
+ /** Closes the group's box this many ms after its last message. Absent or 0: stays until closed. */
9
+ duration?: number;
10
+ /** Added to the group's `.rui-toast` box. */
11
+ className?: string;
12
+ };
5
13
  type PopupProviderProps = GraphqlErrorRulesOptions & {
6
14
  children: ReactNode;
7
15
  closeLabel?: string;
16
+ /** Groups the project uses, by name. Undeclared groups are accepted with the defaults. */
17
+ groups?: Record<string, ToastGroupConfig>;
18
+ /** Default color of every box. `"--color-x"` reads the theme variable. Defaults to `"--color-primary"`. */
19
+ color?: string;
8
20
  /**
9
21
  * Installs a `window.fetch` interceptor that raises popups by itself: `"rest"` reads HTTP
10
22
  * statuses, `"graphql"` also reads the `errors` array on `graphqlPaths`. Off by default.
11
23
  * Intercepted messages clear as soon as the next request starts; pushed ones stay.
12
24
  */
13
25
  interceptFetch?: InterceptFetchMode;
26
+ /** Group of intercepted fetch failures. Omitted: the anonymous default group. */
27
+ fetchGroup?: string;
14
28
  /** Called for every intercepted failure, with the method, URL and status behind it. */
15
29
  onReport?: (report: FetchErrorReport) => void;
30
+ /** Set on the `.rui-toasts` container, e.g. to move it. */
31
+ className?: string;
32
+ style?: CSSProperties;
16
33
  };
17
- export declare function PopupProvider({ children, closeLabel, interceptFetch, onReport, ...ruleOptions }: PopupProviderProps): import("react").JSX.Element;
34
+ export declare function PopupProvider({ children, closeLabel, groups: groupConfigs, color, interceptFetch, fetchGroup, onReport, className, style, ...ruleOptions }: PopupProviderProps): import("react").JSX.Element;
18
35
  export {};
19
36
  //# sourceMappingURL=PopupProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"PopupProvider.d.ts","sourceRoot":"","sources":["../../src/popup/PopupProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,OAAO,KAAK,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAA;AAOjF,gGAAgG;AAChG,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAAA;AAE3D,KAAK,kBAAkB,GAAG,wBAAwB,GAAG;IACnD,QAAQ,EAAE,SAAS,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;;OAIG;IACH,cAAc,CAAC,EAAE,kBAAkB,CAAA;IACnC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAA;CAC9C,CAAA;AAKD,wBAAgB,aAAa,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,cAAsB,EAAE,QAAQ,EAAE,GAAG,WAAW,EAAE,EAAE,kBAAkB,+BAyD3H"}
1
+ {"version":3,"file":"PopupProvider.d.ts","sourceRoot":"","sources":["../../src/popup/PopupProvider.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAErD,OAAO,KAAK,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAA;AASjF,gGAAgG;AAChG,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,MAAM,GAAG,SAAS,CAAA;AAE3D,MAAM,MAAM,gBAAgB,GAAG;IAC7B,oHAAoH;IACpH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mGAAmG;IACnG,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED,KAAK,kBAAkB,GAAG,wBAAwB,GAAG;IACnD,QAAQ,EAAE,SAAS,CAAA;IACnB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,0FAA0F;IAC1F,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAA;IACzC,2GAA2G;IAC3G,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;OAIG;IACH,cAAc,CAAC,EAAE,kBAAkB,CAAA;IACnC,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,uFAAuF;IACvF,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAA;IAC7C,2DAA2D;IAC3D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB,CAAA;AAwBD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,UAAU,EACV,MAAM,EAAE,YAAY,EACpB,KAAyB,EACzB,cAAsB,EACtB,UAA0B,EAC1B,QAAQ,EACR,SAAS,EACT,KAAK,EACL,GAAG,WAAW,EACf,EAAE,kBAAkB,+BA0GpB"}
@@ -4,18 +4,63 @@ import { createErrorRules, createGraphqlErrorRules } from "./errorRules.js";
4
4
  import { createInterceptedFetch } from "./fetchInterceptor.js";
5
5
  import { PopupHost } from "./PopupHost.js";
6
6
  import { PopupContext } from "./usePopup.js";
7
- const addMessage = (message) => (current) => current.includes(message) ? current : [...current, message];
8
- export function PopupProvider({ children, closeLabel, interceptFetch = false, onReport, ...ruleOptions }) {
9
- const [fetchMessages, setFetchMessages] = useState([]);
10
- const [pushedMessages, setPushedMessages] = useState([]);
7
+ // Anonymous group used by push() without `group` and by fetch without `fetchGroup`.
8
+ const DEFAULT_GROUP = "";
9
+ let nextItemId = 0;
10
+ function addItem(groups, name, item) {
11
+ const group = groups.find(g => g.name === name);
12
+ if (!group)
13
+ return [...groups, { name, items: [item] }];
14
+ if (item.dedupKey !== null && group.items.some(i => i.dedupKey === item.dedupKey))
15
+ return groups;
16
+ return groups.map(g => (g === group ? { name, items: [...g.items, item] } : g));
17
+ }
18
+ function removeFetchItems(groups) {
19
+ if (!groups.some(g => g.items.some(i => i.fromFetch)))
20
+ return groups;
21
+ return groups
22
+ .map(g => ({ name: g.name, items: g.items.filter(i => !i.fromFetch) }))
23
+ .filter(g => g.items.length > 0);
24
+ }
25
+ export function PopupProvider({ children, closeLabel, groups: groupConfigs, color = "--color-primary", interceptFetch = false, fetchGroup = DEFAULT_GROUP, onReport, className, style, ...ruleOptions }) {
26
+ const [groups, setGroups] = useState([]);
11
27
  // L'interception ne s'installe qu'une fois : le callback, lui, peut changer d'un rendu à
12
28
  // l'autre sans avoir à la réinstaller.
13
29
  const reportRef = useRef(onReport);
14
30
  reportRef.current = onReport;
15
- // Options arrive as literals: reading them through a ref keeps the interceptor from being
16
- // reinstalled on every parent render.
31
+ // Options arrive as literals: reading them through refs keeps the interceptor from being
32
+ // reinstalled, and push() stable, on every parent render.
17
33
  const ruleOptionsRef = useRef(ruleOptions);
18
34
  ruleOptionsRef.current = ruleOptions;
35
+ const groupConfigsRef = useRef(groupConfigs);
36
+ groupConfigsRef.current = groupConfigs;
37
+ const fetchGroupRef = useRef(fetchGroup);
38
+ fetchGroupRef.current = fetchGroup;
39
+ const timers = useRef(new Map());
40
+ const dismiss = useCallback((group) => {
41
+ if (group === undefined) {
42
+ timers.current.forEach(clearTimeout);
43
+ timers.current.clear();
44
+ setGroups([]);
45
+ return;
46
+ }
47
+ clearTimeout(timers.current.get(group));
48
+ timers.current.delete(group);
49
+ setGroups(current => (current.some(g => g.name === group) ? current.filter(g => g.name !== group) : current));
50
+ }, []);
51
+ const add = useCallback((group, content, dedupKey, fromFetch) => {
52
+ setGroups(current => addItem(current, group, { id: nextItemId++, dedupKey, content, fromFetch }));
53
+ // The group's timer restarts with every message.
54
+ const duration = groupConfigsRef.current?.[group]?.duration;
55
+ if (duration) {
56
+ clearTimeout(timers.current.get(group));
57
+ timers.current.set(group, setTimeout(() => dismiss(group), duration));
58
+ }
59
+ }, [dismiss]);
60
+ useEffect(() => {
61
+ const pending = timers.current;
62
+ return () => pending.forEach(clearTimeout);
63
+ }, []);
19
64
  useEffect(() => {
20
65
  if (!interceptFetch)
21
66
  return;
@@ -26,8 +71,8 @@ export function PopupProvider({ children, closeLabel, interceptFetch = false, on
26
71
  ? createGraphqlErrorRules(ruleOptionsRef.current)
27
72
  : createErrorRules(ruleOptionsRef.current);
28
73
  const intercepted = createInterceptedFetch(original, {
29
- onRequestStart: () => setFetchMessages(current => (current.length === 0 ? current : [])),
30
- onError: message => setFetchMessages(addMessage(message)),
74
+ onRequestStart: () => setGroups(removeFetchItems),
75
+ onError: message => add(fetchGroupRef.current, message, message, true),
31
76
  onReport: report => reportRef.current?.(report),
32
77
  }, rules);
33
78
  intercepted.__popupIntercepted = true;
@@ -35,13 +80,20 @@ export function PopupProvider({ children, closeLabel, interceptFetch = false, on
35
80
  return () => {
36
81
  window.fetch = original;
37
82
  };
38
- }, [interceptFetch]);
39
- const pushError = useCallback((message) => setPushedMessages(addMessage(message)), []);
40
- const dismiss = useCallback(() => {
41
- setFetchMessages([]);
42
- setPushedMessages([]);
43
- }, []);
44
- const api = useMemo(() => ({ pushError, dismiss }), [pushError, dismiss]);
45
- const messages = useMemo(() => [...new Set([...pushedMessages, ...fetchMessages])], [pushedMessages, fetchMessages]);
46
- return (_jsxs(PopupContext.Provider, { value: api, children: [children, _jsx(PopupHost, { messages: messages, onDismiss: dismiss, closeLabel: closeLabel })] }));
83
+ }, [interceptFetch, add]);
84
+ const push = useCallback((content, options) => {
85
+ const dedupKey = options?.id ?? (typeof content === "string" ? content : null);
86
+ add(options?.group ?? DEFAULT_GROUP, content, dedupKey, false);
87
+ }, [add]);
88
+ const api = useMemo(() => ({ push, dismiss }), [push, dismiss]);
89
+ const boxes = groups.map(group => {
90
+ const config = groupConfigs?.[group.name];
91
+ return {
92
+ group: group.name,
93
+ color: config?.color ?? color,
94
+ className: config?.className,
95
+ items: group.items.map(item => ({ id: item.id, content: item.content })),
96
+ };
97
+ });
98
+ return (_jsxs(PopupContext.Provider, { value: api, children: [children, _jsx(PopupHost, { boxes: boxes, onDismiss: dismiss, closeLabel: closeLabel, className: className, style: style })] }));
47
99
  }
@@ -1,8 +1,15 @@
1
+ import type { ReactNode } from "react";
2
+ export type PushOptions = {
3
+ /** Group whose box shows the message. Omitted: the anonymous default group. */
4
+ group?: string;
5
+ /** Dedup key within the group. Strings dedup by value; other content only through `id`. */
6
+ id?: string;
7
+ };
1
8
  export type PopupApi = {
2
- /** Shows a message until the user closes the popup. Identical messages are shown once. */
3
- pushError: (message: string) => void;
4
- /** Closes the popup and forgets every message. */
5
- dismiss: () => void;
9
+ /** Shows `content` in its group's box until closed (× or the group's `duration`). */
10
+ push: (content: ReactNode, options?: PushOptions) => void;
11
+ /** Closes one group's box, or every box when called without a group. */
12
+ dismiss: (group?: string) => void;
6
13
  };
7
14
  export declare const PopupContext: import("react").Context<PopupApi | null>;
8
15
  export declare function usePopup(): PopupApi;
@@ -1 +1 @@
1
- {"version":3,"file":"usePopup.d.ts","sourceRoot":"","sources":["../../src/popup/usePopup.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG;IACrB,0FAA0F;IAC1F,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAA;IACpC,kDAAkD;IAClD,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAED,eAAO,MAAM,YAAY,0CAAuC,CAAA;AAEhE,wBAAgB,QAAQ,IAAI,QAAQ,CAInC"}
1
+ {"version":3,"file":"usePopup.d.ts","sourceRoot":"","sources":["../../src/popup/usePopup.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC,MAAM,MAAM,WAAW,GAAG;IACxB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,2FAA2F;IAC3F,EAAE,CAAC,EAAE,MAAM,CAAA;CACZ,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,qFAAqF;IACrF,IAAI,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,WAAW,KAAK,IAAI,CAAA;IACzD,wEAAwE;IACxE,OAAO,EAAE,CAAC,KAAK,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;CAClC,CAAA;AAED,eAAO,MAAM,YAAY,0CAAuC,CAAA;AAEhE,wBAAgB,QAAQ,IAAI,QAAQ,CAInC"}
@@ -0,0 +1,2 @@
1
+ export declare const accordionCss = "\n:where(.rui-accordion:not([data-variant=\"separated\"])) {\n border: 1px solid var(--rui-accordion-border, var(--color-border));\n border-radius: 12px;\n background-color: var(--rui-accordion-bg, var(--color-surface));\n overflow: hidden;\n}\n:where(.rui-accordion:not([data-variant=\"separated\"])) > :where(details + details) {\n border-top: 1px solid var(--rui-accordion-border, var(--color-border));\n}\n:where(.rui-accordion[data-variant=\"separated\"]) {\n display: flex;\n flex-direction: column;\n gap: var(--rui-accordion-gap, 0.5rem);\n}\n:where(.rui-accordion[data-variant=\"separated\"]) > :where(details) {\n border: 1px solid var(--rui-accordion-border, var(--color-border));\n border-radius: 12px;\n background-color: var(--rui-accordion-bg, var(--color-surface));\n overflow: hidden;\n}\n\n:where(.rui-accordion) :where(summary) {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n padding: var(--rui-accordion-padding, 0.875rem 1rem);\n color: var(--color-text);\n font-weight: 500;\n list-style: none;\n cursor: pointer;\n}\n:where(.rui-accordion) :where(summary)::-webkit-details-marker {\n display: none;\n}\n:where(.rui-accordion) :where(summary:hover) {\n background-color: color-mix(in srgb, var(--color-text) 4%, transparent);\n}\n:where(.rui-accordion) :where(summary:focus-visible) {\n outline: 2px solid var(--color-primary);\n outline-offset: -2px;\n}\n:where(.rui-accordion) :where(details) > :where(:not(summary)) {\n padding: var(--rui-accordion-padding, 0.875rem 1rem);\n color: var(--color-text);\n}\n\n:where(.rui-accordion:not([data-chevron=\"start\"], [data-chevron=\"none\"])) :where(summary)::after {\n \n content: \"\";\n flex-shrink: 0;\n width: 0.45em;\n height: 0.45em;\n border-right: 2px solid var(--color-text-muted);\n border-bottom: 2px solid var(--color-text-muted);\n transform: translateY(-25%) rotate(45deg);\n transition: transform 150ms ease-out;\n\n margin-left: auto;\n}\n:where(.rui-accordion:not([data-chevron=\"start\"], [data-chevron=\"none\"])) :where(details[open] > summary)::after {\n transform: translateY(25%) rotate(-135deg);\n}\n:where(.rui-accordion[data-chevron=\"start\"]) :where(summary)::before {\n \n content: \"\";\n flex-shrink: 0;\n width: 0.45em;\n height: 0.45em;\n border-right: 2px solid var(--color-text-muted);\n border-bottom: 2px solid var(--color-text-muted);\n transform: translateY(-25%) rotate(45deg);\n transition: transform 150ms ease-out;\n\n}\n:where(.rui-accordion[data-chevron=\"start\"]) :where(details[open] > summary)::before {\n transform: translateY(25%) rotate(-135deg);\n}\n\n/* Progressive enhancement: animated where the browser can transition to height: auto. */\n@supports (interpolate-size: allow-keywords) {\n :where(.rui-accordion) {\n interpolate-size: allow-keywords;\n }\n :where(.rui-accordion details)::details-content {\n height: 0;\n overflow: hidden;\n transition: height 200ms ease-out, content-visibility 200ms allow-discrete;\n }\n :where(.rui-accordion details[open])::details-content {\n height: auto;\n }\n}\n@media (prefers-reduced-motion: reduce) {\n :where(.rui-accordion) :where(summary)::before,\n :where(.rui-accordion) :where(summary)::after,\n :where(.rui-accordion details)::details-content {\n transition: none;\n }\n}\n";
2
+ //# sourceMappingURL=accordionCss.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accordionCss.d.ts","sourceRoot":"","sources":["../../src/style/accordionCss.ts"],"names":[],"mappings":"AAmBA,eAAO,MAAM,YAAY,6wGAkFxB,CAAA"}