mui-atelier 0.1.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 (77) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +32 -0
  4. package/README.md +282 -0
  5. package/dist/components/AddressFields.js +83 -0
  6. package/dist/components/AuthCard.js +94 -0
  7. package/dist/components/AuthSplash.js +88 -0
  8. package/dist/components/BrandHeader.js +81 -0
  9. package/dist/components/BrandLink.js +22 -0
  10. package/dist/components/BrandMark.js +69 -0
  11. package/dist/components/BrandProvider.js +14 -0
  12. package/dist/components/Breadcrumbs.js +62 -0
  13. package/dist/components/CardHead.js +46 -0
  14. package/dist/components/ConfirmProvider.js +77 -0
  15. package/dist/components/DashboardLayout.js +645 -0
  16. package/dist/components/DataTable.js +546 -0
  17. package/dist/components/DetailRows.js +42 -0
  18. package/dist/components/EmptyNote.js +28 -0
  19. package/dist/components/Eyebrow.js +21 -0
  20. package/dist/components/HeadingHelp.js +38 -0
  21. package/dist/components/HeadingLevel.js +18 -0
  22. package/dist/components/IconTile.js +42 -0
  23. package/dist/components/ImagePreviewDialog.js +86 -0
  24. package/dist/components/LabeledField.js +156 -0
  25. package/dist/components/LanguageSwitcher.js +43 -0
  26. package/dist/components/ListRow.js +59 -0
  27. package/dist/components/ModeToggle.js +35 -0
  28. package/dist/components/PageHeader.js +65 -0
  29. package/dist/components/PasswordField.js +43 -0
  30. package/dist/components/PublicLayout.js +27 -0
  31. package/dist/components/PublicNav.js +17 -0
  32. package/dist/components/RouteMessage.js +31 -0
  33. package/dist/components/RowList.js +10 -0
  34. package/dist/components/SafeHtml.js +57 -0
  35. package/dist/components/SaveState.js +13 -0
  36. package/dist/components/SectionPanel.js +48 -0
  37. package/dist/components/SectionTabs.js +139 -0
  38. package/dist/components/SiteNav.js +130 -0
  39. package/dist/components/Skeletons.js +63 -0
  40. package/dist/components/Sortable.js +81 -0
  41. package/dist/components/StatTile.js +66 -0
  42. package/dist/components/StatusChip.js +17 -0
  43. package/dist/components/StatusDot.js +42 -0
  44. package/dist/components/SubCard.js +30 -0
  45. package/dist/components/TabbedCard.js +20 -0
  46. package/dist/components/TableFilters.js +247 -0
  47. package/dist/components/ToastProvider.js +65 -0
  48. package/dist/eslint/chipRules.mjs +65 -0
  49. package/dist/hooks/useDocumentTitle.js +19 -0
  50. package/dist/hooks/useOverflowTabs.js +53 -0
  51. package/dist/hooks/useUnsavedChanges.js +46 -0
  52. package/dist/i18n/loadMessages.js +15 -0
  53. package/dist/i18n/mergeMessages.js +13 -0
  54. package/dist/i18n/messages/en.json +103 -0
  55. package/dist/i18n/messages/es.json +103 -0
  56. package/dist/i18n/messages/zh.json +103 -0
  57. package/dist/lib/authCard.js +4 -0
  58. package/dist/lib/chipTone.js +46 -0
  59. package/dist/lib/countryRegionData.js +29 -0
  60. package/dist/lib/countryRegionData.json +1 -0
  61. package/dist/lib/navLock.js +22 -0
  62. package/dist/lib/navTabSx.js +78 -0
  63. package/dist/lib/orgTime.js +243 -0
  64. package/dist/lib/pageHtml.js +139 -0
  65. package/dist/lib/safeNext.js +13 -0
  66. package/dist/lib/sanitizeHtml.js +36 -0
  67. package/dist/lib/slugify.js +7 -0
  68. package/dist/lib/tableFilter.js +147 -0
  69. package/dist/lib/tableStyle.js +87 -0
  70. package/dist/test/jsdomShims.js +53 -0
  71. package/dist/test/themeContract.js +178 -0
  72. package/dist/theme/OrgThemeRegistry.js +71 -0
  73. package/dist/theme/ThemeRegistry.js +17 -0
  74. package/dist/theme/berryNavRow.js +21 -0
  75. package/dist/theme/createSiteTheme.js +974 -0
  76. package/dist/theme/themes.js +438 -0
  77. package/package.json +118 -0
@@ -0,0 +1,57 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { useMemo } from "react";
4
+ import Box from "@mui/material/Box";
5
+ import { sanitizeHtml } from "../lib/sanitizeHtml.js";
6
+ const RICH_SX = {
7
+ "& > :first-of-type": { mt: 0 },
8
+ "& > :last-child": { mb: 0 },
9
+ "& h1, & h2, & h3, & h4, & h5, & h6": { mt: 2, mb: 1, lineHeight: 1.3 },
10
+ "& p, & ul, & ol, & blockquote, & pre": { mt: 0, mb: 1.5 },
11
+ "& p, & ul, & ol, & blockquote": { maxWidth: "68ch" },
12
+ "& ul, & ol": { pl: 3 },
13
+ "& li": { mb: 0.5 },
14
+ "& blockquote": {
15
+ m: 0,
16
+ pl: 2,
17
+ borderLeft: 3,
18
+ borderColor: "divider",
19
+ color: "text.secondary"
20
+ },
21
+ "& pre": { p: 1.5, borderRadius: (t) => `${t.rt?.radii?.base ?? 8}px`, bgcolor: "action.hover", overflowX: "auto" },
22
+ "& code": { fontFamily: "monospace", fontSize: "0.875em" },
23
+ "& hr": { border: 0, borderTop: 1, borderColor: "divider", my: 2 },
24
+ "& table": {
25
+ display: "block",
26
+ width: "100%",
27
+ overflowX: "auto",
28
+ borderCollapse: "collapse",
29
+ mb: 1.5
30
+ },
31
+ "& th, & td": { border: 1, borderColor: "divider", px: 1, py: 0.5, textAlign: "left" },
32
+ "& th": { bgcolor: "action.hover", fontWeight: 600 },
33
+ "& caption": { captionSide: "bottom", pt: 1, color: "text.secondary", fontSize: "0.8125rem" }
34
+ };
35
+ function SafeHtml({ html, block = false, rich = false, sx, ...rest }) {
36
+ const clean = useMemo(() => sanitizeHtml(html, { block, rich }), [html, block, rich]);
37
+ if (!clean) return null;
38
+ const asBlock = block || rich;
39
+ return /* @__PURE__ */ jsx(
40
+ Box,
41
+ {
42
+ component: asBlock ? "div" : "span",
43
+ suppressHydrationWarning: true,
44
+ sx: {
45
+ "& a": { color: "primary.main", textDecoration: "underline" },
46
+ ...asBlock ? { "& p": { m: 0 }, "& p:not(:last-child)": { mb: 0.5 } } : {},
47
+ ...rich ? RICH_SX : {},
48
+ ...sx
49
+ },
50
+ ...rest,
51
+ dangerouslySetInnerHTML: { __html: clean }
52
+ }
53
+ );
54
+ }
55
+ export {
56
+ SafeHtml as default
57
+ };
@@ -0,0 +1,13 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import Stack from "@mui/material/Stack";
4
+ import Typography from "@mui/material/Typography";
5
+ import { useTranslations } from "next-intl";
6
+ import StatusChip from "./StatusChip.js";
7
+ function SaveState({ dirty, saving }) {
8
+ const t = useTranslations("uikit");
9
+ return /* @__PURE__ */ jsx(Stack, { direction: "row", spacing: 1, sx: { alignItems: "center" }, role: "status", "aria-live": "polite", children: saving ? /* @__PURE__ */ jsx(Typography, { variant: "caption", sx: { color: "text.secondary" }, children: t("save.saving") }) : dirty ? /* @__PURE__ */ jsx(StatusChip, { size: "small", role: "caution", variant: "outlined", label: t("save.unsaved") }) : null });
10
+ }
11
+ export {
12
+ SaveState as default
13
+ };
@@ -0,0 +1,48 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import Card from "@mui/material/Card";
3
+ import Box from "@mui/material/Box";
4
+ import Stack from "@mui/material/Stack";
5
+ import Typography from "@mui/material/Typography";
6
+ import { useTheme } from "@mui/material/styles";
7
+ import HeadingHelp from "./HeadingHelp.js";
8
+ import HeadingLevel, { useHeadingLevel, useNestedHeadingLevel } from "./HeadingLevel.js";
9
+ function SectionPanel({ title, subtitle, help, action, component, children, sx }) {
10
+ const hasHeader = title || action;
11
+ const level = useHeadingLevel();
12
+ const headingComponent = component ?? level;
13
+ const nested = useNestedHeadingLevel();
14
+ const berry = useTheme().rt?.ui?.surface === "berry";
15
+ return /* @__PURE__ */ jsxs(
16
+ Card,
17
+ {
18
+ sx: [
19
+ berry ? { p: 3 } : { p: { xs: 2, sm: 2.5 } },
20
+ ...Array.isArray(sx) ? sx : [sx]
21
+ ],
22
+ children: [
23
+ hasHeader ? /* @__PURE__ */ jsxs(
24
+ Stack,
25
+ {
26
+ direction: "row",
27
+ spacing: 1.5,
28
+ sx: berry && children ? { alignItems: "center", mx: -3, px: 3, pb: 3, mb: 3, borderBottom: "1px solid", borderColor: "divider", "& .MuiTypography-h6": { fontSize: "1.125rem", fontWeight: 500 } } : { alignItems: "center", mb: children ? 2 : 0 },
29
+ children: [
30
+ /* @__PURE__ */ jsxs(Box, { sx: { flex: 1, minWidth: 0 }, children: [
31
+ title ? /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 0.25, sx: { alignItems: "center" }, children: [
32
+ /* @__PURE__ */ jsx(Typography, { variant: "h6", component: headingComponent, children: title }),
33
+ /* @__PURE__ */ jsx(HeadingHelp, { text: help, label: typeof title === "string" ? title : void 0 })
34
+ ] }) : null,
35
+ subtitle ? /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "text.secondary" }, children: subtitle }) : null
36
+ ] }),
37
+ action
38
+ ]
39
+ }
40
+ ) : null,
41
+ title ? /* @__PURE__ */ jsx(HeadingLevel, { level: nested, children }) : children
42
+ ]
43
+ }
44
+ );
45
+ }
46
+ export {
47
+ SectionPanel as default
48
+ };
@@ -0,0 +1,139 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { Children, cloneElement, isValidElement } from "react";
4
+ import Box from "@mui/material/Box";
5
+ import Tabs from "@mui/material/Tabs";
6
+ import { useTheme } from "@mui/material/styles";
7
+ const SIZES = {
8
+ medium: { pad: 4, minHeight: 38, px: 2, fontSize: "0.9375rem" },
9
+ small: { pad: 3, minHeight: 30, px: 1.25, fontSize: "0.8125rem" }
10
+ };
11
+ function SectionTabs({ size = "medium", children, sx, ...rest }) {
12
+ const { rt } = useTheme();
13
+ const radius = rt?.radii?.button ?? 8;
14
+ const { pad, minHeight, px, fontSize } = SIZES[size] ?? SIZES.medium;
15
+ if (rt?.ui?.tabs === "underline") {
16
+ const tabs = Children.map(children, (child) => isValidElement(child) && child.props?.value !== void 0 ? cloneElement(child, {
17
+ id: tabId(child.props.value),
18
+ "aria-controls": tabPanelId(child.props.value)
19
+ }) : child);
20
+ return /* @__PURE__ */ jsx(
21
+ Tabs,
22
+ {
23
+ variant: "scrollable",
24
+ scrollButtons: "auto",
25
+ sx: [{ minHeight: 0, "& .MuiTab-root": { minHeight: size === "small" ? 36 : 48, fontSize, px: 2 } }, ...Array.isArray(sx) ? sx : [sx]],
26
+ ...rest,
27
+ children: tabs
28
+ }
29
+ );
30
+ }
31
+ return /* @__PURE__ */ jsx(
32
+ Tabs,
33
+ {
34
+ variant: "scrollable",
35
+ scrollButtons: "auto",
36
+ sx: [
37
+ (theme) => ({
38
+ minHeight: 0,
39
+ // inline-flex so the strip hugs its tabs. Stretched full-width it
40
+ // would read as a toolbar, and the ground would run out past the
41
+ // last tab to the edge of the page.
42
+ display: "inline-flex",
43
+ maxWidth: "100%",
44
+ p: `${pad}px`,
45
+ borderRadius: `${radius + pad}px`,
46
+ /*
47
+ * The WELL: a BLACK wash, in both modes, closed with a hairline.
48
+ *
49
+ * `action.hover` was the obvious choice and it is wrong here, because
50
+ * MUI flips it to a WHITE overlay in dark mode. That made the track
51
+ * lighter than the page while the chip stayed at `background.paper`,
52
+ * so on every dark theme the current tab was DARKER than the track it
53
+ * sat in — the depth cue exactly inverted, a chip that reads as a
54
+ * hole. Measured on `editorial` dark: chip #1B1D24 inside a #292722
55
+ * track.
56
+ *
57
+ * Darkening in both modes keeps one rule true everywhere: the track
58
+ * is below the page, the chip is at page level, so the chip is always
59
+ * the thing standing up.
60
+ */
61
+ bgcolor: "rgba(0, 0, 0, 0.05)",
62
+ ...theme.applyStyles?.("dark", { backgroundColor: "rgba(0, 0, 0, 0.32)" }),
63
+ border: 1,
64
+ borderColor: "divider",
65
+ "& .MuiTabs-list": { gap: `${pad}px` },
66
+ /*
67
+ * The indicator IS the chip: full height, behind the labels. It is
68
+ * painted after the tabs in the DOM, so the tabs need the z-index —
69
+ * without it the chip covers its own label.
70
+ *
71
+ * `background.paper` over `action.hover` is the whole illusion: the
72
+ * chip is the one thing here at page level, so it looks lifted OUT of
73
+ * the well rather than painted onto it.
74
+ *
75
+ * PAPER + A `divider` HAIRLINE + `rt.lift.rest` — the same three
76
+ * things a raised input and a raised page button are made of, so the
77
+ * current tab, a search field and an outlined action all read as one
78
+ * kind of surface. It used to carry a flat grey shadow and a fake 1px
79
+ * ring painted as a second box-shadow layer, which is a hardcoded
80
+ * approximation of both: the ring did not follow `divider` when a
81
+ * theme changed its hairline, and the shadow was neutral grey under
82
+ * every palette while everything beside it was tinted.
83
+ *
84
+ * `lift.rest` and not `raised`: the chip is at page level, not
85
+ * floating above it, and a tab is not an action — the pages this sits
86
+ * on already argue that at length above.
87
+ */
88
+ "& .MuiTabs-indicator": {
89
+ height: "100%",
90
+ borderRadius: `${radius}px`,
91
+ bgcolor: "background.paper",
92
+ border: "1px solid",
93
+ borderColor: "divider",
94
+ // Fallbacks for a bare MUI theme, which carries no `rt`.
95
+ boxShadow: rt?.lift?.rest ?? theme.shadows[1],
96
+ ...theme.applyStyles?.("dark", { boxShadow: rt?.lift?.restDark ?? theme.shadows[1] })
97
+ },
98
+ "& .MuiTab-root": {
99
+ zIndex: 1,
100
+ minWidth: 0,
101
+ // MUI's 90px floor makes "Body" a pill wider than its word
102
+ minHeight,
103
+ px,
104
+ borderRadius: `${radius}px`,
105
+ textTransform: "none",
106
+ fontSize,
107
+ fontWeight: 600,
108
+ color: "text.secondary",
109
+ transition: "color 150ms",
110
+ "&:hover": { color: "text.primary" },
111
+ /*
112
+ * The accent lands HERE — on the label of the raised chip, over
113
+ * paper. `primary.main` is guaranteed readable on `background.paper`
114
+ * (it is what every link and text Button in the app already does);
115
+ * `contrastText` would be the pairing for an accent FILL, which is
116
+ * exactly the button look this control is avoiding.
117
+ */
118
+ "&.Mui-selected": { color: "primary.main" }
119
+ }
120
+ }),
121
+ ...Array.isArray(sx) ? sx : [sx]
122
+ ],
123
+ ...rest,
124
+ children
125
+ }
126
+ );
127
+ }
128
+ const tabId = (value) => `rt-tab-${value}`;
129
+ const tabPanelId = (value) => `rt-tabpanel-${value}`;
130
+ function SectionTabPanel({ value, active, children }) {
131
+ if (value !== active) return null;
132
+ return /* @__PURE__ */ jsx(Box, { role: "tabpanel", id: tabPanelId(value), "aria-labelledby": tabId(value), tabIndex: 0, children });
133
+ }
134
+ export {
135
+ SectionTabPanel,
136
+ SectionTabs as default,
137
+ tabId,
138
+ tabPanelId
139
+ };
@@ -0,0 +1,130 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId, useState } from "react";
4
+ import NextLink from "next/link";
5
+ import Box from "@mui/material/Box";
6
+ import ButtonBase from "@mui/material/ButtonBase";
7
+ import Container from "@mui/material/Container";
8
+ import Menu from "@mui/material/Menu";
9
+ import MenuItem from "@mui/material/MenuItem";
10
+ import ExpandMoreRoundedIcon from "@mui/icons-material/ExpandMoreRounded";
11
+ import { useTheme } from "@mui/material/styles";
12
+ import { useTranslations } from "next-intl";
13
+ import useOverflowTabs from "../hooks/useOverflowTabs.js";
14
+ import { WELL_PAD, underlineRowSx, underlineTabSx, wellSx, wellTabSx } from "../lib/navTabSx.js";
15
+ function SiteNav({ items, label, moreLabel, hideBelow = "md", sx }) {
16
+ const theme = useTheme();
17
+ const t = useTranslations("uikit");
18
+ const barH = theme.rt?.barHeight ?? null;
19
+ const navH = theme.rt?.navHeight ?? 48;
20
+ const underline = theme.rt?.ui?.tabs === "underline";
21
+ const navLabel = label ?? t("nav.sections");
22
+ const more = moreLabel ?? t("nav.more");
23
+ const activeIndex = items.findIndex((i) => i.active);
24
+ const { containerRef, measureRef, visibleCount } = useOverflowTabs(items.length, activeIndex);
25
+ let shown = items.slice(0, visibleCount);
26
+ let hidden = items.slice(visibleCount);
27
+ if (activeIndex >= visibleCount) {
28
+ const active = items[activeIndex];
29
+ shown = [...items.slice(0, Math.max(0, visibleCount - 1)), active];
30
+ hidden = items.filter((i) => !shown.includes(i));
31
+ }
32
+ const hiddenActive = hidden.some((i) => i.active);
33
+ const [anchor, setAnchor] = useState(null);
34
+ const menuId = useId();
35
+ const tabSx = (active) => underline ? underlineTabSx(active) : wellTabSx(theme, active);
36
+ const rowSx = underline ? underlineRowSx() : wellSx(theme);
37
+ return /* @__PURE__ */ jsxs(
38
+ Box,
39
+ {
40
+ component: "nav",
41
+ "aria-label": navLabel,
42
+ "data-tabs": underline ? "underline" : "well",
43
+ sx: [
44
+ {
45
+ display: hideBelow ? { xs: "none", [hideBelow]: "block" } : "block",
46
+ position: "sticky",
47
+ top: barH && barH !== 64 ? barH : { xs: 56, sm: 64 },
48
+ zIndex: (th) => th.zIndex.appBar - 1,
49
+ bgcolor: "background.paper",
50
+ borderBottom: "1px solid",
51
+ borderColor: "divider"
52
+ },
53
+ ...Array.isArray(sx) ? sx : [sx]
54
+ ],
55
+ children: [
56
+ /* @__PURE__ */ jsxs(Container, { maxWidth: false, sx: { position: "relative" }, children: [
57
+ /* @__PURE__ */ jsx(Box, { ref: containerRef, sx: { display: "flex", justifyContent: "center", alignItems: "center", minHeight: navH }, children: /* @__PURE__ */ jsxs(Box, { sx: rowSx, children: [
58
+ shown.map((i) => /* @__PURE__ */ jsx(Box, { component: NextLink, href: i.href, "aria-current": i.active ? "page" : void 0, sx: tabSx(i.active), children: i.label }, i.key ?? i.href)),
59
+ hidden.length > 0 ? /* @__PURE__ */ jsxs(
60
+ ButtonBase,
61
+ {
62
+ onClick: (e) => setAnchor(e.currentTarget),
63
+ "aria-haspopup": "menu",
64
+ "aria-expanded": anchor ? "true" : "false",
65
+ "aria-controls": anchor ? menuId : void 0,
66
+ "aria-current": hiddenActive ? "page" : void 0,
67
+ sx: { ...tabSx(hiddenActive), pr: 1.25 },
68
+ children: [
69
+ more,
70
+ /* @__PURE__ */ jsx(ExpandMoreRoundedIcon, { sx: { fontSize: 18, ml: -0.25 } })
71
+ ]
72
+ }
73
+ ) : null
74
+ ] }) }),
75
+ /* @__PURE__ */ jsxs(
76
+ Box,
77
+ {
78
+ ref: measureRef,
79
+ "aria-hidden": "true",
80
+ sx: {
81
+ ...rowSx,
82
+ position: "absolute",
83
+ top: 0,
84
+ left: 0,
85
+ height: 0,
86
+ overflow: "hidden",
87
+ visibility: "hidden",
88
+ pointerEvents: "none",
89
+ maxWidth: "none",
90
+ ...underline ? null : { gap: `${WELL_PAD}px` }
91
+ },
92
+ children: [
93
+ items.map((i) => /* @__PURE__ */ jsx(Box, { component: "span", sx: tabSx(false), children: i.label }, i.key ?? i.href)),
94
+ /* @__PURE__ */ jsxs(Box, { component: "span", sx: { ...tabSx(false), pr: 1.25 }, children: [
95
+ more,
96
+ /* @__PURE__ */ jsx(ExpandMoreRoundedIcon, { sx: { fontSize: 18, ml: -0.25 } })
97
+ ] })
98
+ ]
99
+ }
100
+ )
101
+ ] }),
102
+ hidden.length > 0 ? /* @__PURE__ */ jsx(
103
+ Menu,
104
+ {
105
+ id: menuId,
106
+ anchorEl: anchor,
107
+ open: Boolean(anchor),
108
+ onClose: () => setAnchor(null),
109
+ slotProps: { list: { "aria-label": more } },
110
+ children: hidden.map((i) => /* @__PURE__ */ jsx(
111
+ MenuItem,
112
+ {
113
+ component: NextLink,
114
+ href: i.href,
115
+ selected: i.active,
116
+ "aria-current": i.active ? "page" : void 0,
117
+ onClick: () => setAnchor(null),
118
+ children: i.label
119
+ },
120
+ i.key ?? i.href
121
+ ))
122
+ }
123
+ ) : null
124
+ ]
125
+ }
126
+ );
127
+ }
128
+ export {
129
+ SiteNav as default
130
+ };
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import Box from "@mui/material/Box";
4
+ import Paper from "@mui/material/Paper";
5
+ import Skeleton from "@mui/material/Skeleton";
6
+ import Stack from "@mui/material/Stack";
7
+ function TableSkeleton({ rows = 8, columns = 5 }) {
8
+ return /* @__PURE__ */ jsxs(Paper, { variant: "outlined", sx: { borderRadius: (t) => `${t.rt?.radii?.card ?? 10}px`, overflow: "hidden" }, children: [
9
+ /* @__PURE__ */ jsxs(Box, { sx: { px: 2, py: 1.5, borderBottom: 1, borderColor: "divider", display: "flex", gap: 1.5, alignItems: "center" }, children: [
10
+ /* @__PURE__ */ jsx(Skeleton, { animation: "wave", variant: "rounded", width: 320, height: 36 }),
11
+ /* @__PURE__ */ jsx(Skeleton, { animation: "wave", variant: "rounded", width: 90, height: 30 })
12
+ ] }),
13
+ /* @__PURE__ */ jsx(Box, { sx: { p: 2 }, children: /* @__PURE__ */ jsx(Stack, { spacing: 1.5, children: Array.from({ length: rows }).map((_, row) => /* @__PURE__ */ jsx(Stack, { direction: "row", spacing: 2, sx: { alignItems: "center" }, children: Array.from({ length: columns }).map((_2, col) => /* @__PURE__ */ jsx(
14
+ Skeleton,
15
+ {
16
+ animation: "wave",
17
+ height: 20,
18
+ sx: { flex: col === 0 ? 2 : 1 }
19
+ },
20
+ col
21
+ )) }, row)) }) })
22
+ ] });
23
+ }
24
+ function CardListSkeleton({ count = 4, height = 84 }) {
25
+ return /* @__PURE__ */ jsx(Stack, { spacing: 1.5, children: Array.from({ length: count }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, { animation: "wave", variant: "rounded", height, sx: { borderRadius: (t) => `${t.rt?.radii?.card ?? 10}px` } }, i)) });
26
+ }
27
+ function FormSkeleton({ fields = 4 }) {
28
+ return /* @__PURE__ */ jsxs(Stack, { spacing: 3, children: [
29
+ /* @__PURE__ */ jsx(Skeleton, { animation: "wave", variant: "text", width: 200, height: 36 }),
30
+ Array.from({ length: fields }).map((_, i) => /* @__PURE__ */ jsxs(Box, { children: [
31
+ /* @__PURE__ */ jsx(Skeleton, { animation: "wave", variant: "text", width: 120, height: 18, sx: { mb: 0.75 } }),
32
+ /* @__PURE__ */ jsx(Skeleton, { animation: "wave", variant: "rounded", height: 40 })
33
+ ] }, i))
34
+ ] });
35
+ }
36
+ function ShellSkeleton() {
37
+ return /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", minHeight: "100dvh" }, "aria-hidden": true, children: [
38
+ /* @__PURE__ */ jsx(Box, { sx: { width: { xs: 0, md: 260 }, flexShrink: 0, borderRight: { md: 1 }, borderColor: "divider", p: 2, display: { xs: "none", md: "block" } }, children: /* @__PURE__ */ jsx(Stack, { spacing: 1.5, sx: { mt: 9 }, children: Array.from({ length: 7 }).map((_, i) => /* @__PURE__ */ jsx(Skeleton, { animation: "wave", variant: "rounded", height: 34 }, i)) }) }),
39
+ /* @__PURE__ */ jsxs(Box, { sx: { flexGrow: 1, minWidth: 0 }, children: [
40
+ /* @__PURE__ */ jsxs(Box, { sx: { height: 64, borderBottom: 1, borderColor: "divider", display: "flex", alignItems: "center", px: 2, gap: 2 }, children: [
41
+ /* @__PURE__ */ jsx(Skeleton, { animation: "wave", variant: "circular", width: 28, height: 28 }),
42
+ /* @__PURE__ */ jsx(Skeleton, { animation: "wave", variant: "text", width: 180, height: 24 })
43
+ ] }),
44
+ /* @__PURE__ */ jsx(Box, { sx: { p: 3 }, children: /* @__PURE__ */ jsx(CardListSkeleton, { count: 3, height: 120 }) })
45
+ ] })
46
+ ] });
47
+ }
48
+ const PAGE_SHAPES = {
49
+ cards: (props) => /* @__PURE__ */ jsx(CardListSkeleton, { count: 3, height: 120, ...props }),
50
+ table: (props) => /* @__PURE__ */ jsx(TableSkeleton, { rows: 8, columns: 5, ...props }),
51
+ form: (props) => /* @__PURE__ */ jsx(FormSkeleton, { fields: 5, ...props })
52
+ };
53
+ function PageSkeleton({ shape = "cards", ...props }) {
54
+ const Render = PAGE_SHAPES[shape] ?? PAGE_SHAPES.cards;
55
+ return /* @__PURE__ */ jsx(Box, { sx: { py: 1 }, children: Render(props) });
56
+ }
57
+ export {
58
+ CardListSkeleton,
59
+ FormSkeleton,
60
+ PageSkeleton,
61
+ ShellSkeleton,
62
+ TableSkeleton
63
+ };
@@ -0,0 +1,81 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useTranslations } from "next-intl";
4
+ import { useState } from "react";
5
+ import { DndContext, DragOverlay, closestCenter, PointerSensor, KeyboardSensor, useSensor, useSensors } from "@dnd-kit/core";
6
+ import { SortableContext, verticalListSortingStrategy, useSortable, sortableKeyboardCoordinates } from "@dnd-kit/sortable";
7
+ import { CSS } from "@dnd-kit/utilities";
8
+ import Box from "@mui/material/Box";
9
+ import IconButton from "@mui/material/IconButton";
10
+ import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
11
+ import { useOrgThemeScope } from "../theme/OrgThemeRegistry.js";
12
+ const overlayHandle = /* @__PURE__ */ jsx(
13
+ IconButton,
14
+ {
15
+ size: "small",
16
+ "aria-hidden": true,
17
+ tabIndex: -1,
18
+ sx: { cursor: "grabbing", color: "text.disabled", flexShrink: 0 },
19
+ children: /* @__PURE__ */ jsx(DragIndicatorIcon, { fontSize: "small" })
20
+ }
21
+ );
22
+ function SortableList({ ids, onMove, children, renderOverlay }) {
23
+ const [activeId, setActiveId] = useState(null);
24
+ const themeScope = useOrgThemeScope();
25
+ const sensors = useSensors(
26
+ useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
27
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
28
+ );
29
+ function onDragEnd(e) {
30
+ setActiveId(null);
31
+ const { active, over } = e;
32
+ if (!over || active.id === over.id) return;
33
+ const from = ids.indexOf(active.id);
34
+ const to = ids.indexOf(over.id);
35
+ if (from !== -1 && to !== -1) onMove(from, to);
36
+ }
37
+ return /* @__PURE__ */ jsxs(
38
+ DndContext,
39
+ {
40
+ sensors,
41
+ collisionDetection: closestCenter,
42
+ onDragStart: (e) => setActiveId(e.active.id),
43
+ onDragEnd,
44
+ onDragCancel: () => setActiveId(null),
45
+ children: [
46
+ /* @__PURE__ */ jsx(SortableContext, { items: ids, strategy: verticalListSortingStrategy, children }),
47
+ /* @__PURE__ */ jsx(DragOverlay, { children: activeId != null && renderOverlay ? /* @__PURE__ */ jsx("div", { className: themeScope, children: renderOverlay(activeId) }) : null })
48
+ ]
49
+ }
50
+ );
51
+ }
52
+ function SortableItem({ id, disabled, children }) {
53
+ const t = useTranslations("uikit");
54
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ id, disabled });
55
+ const style = {
56
+ // Translate only — never scale. Shifting siblings should slide, not stretch.
57
+ transform: CSS.Translate.toString(transform),
58
+ transition,
59
+ // The row the user picked up is drawn by the DragOverlay; leave a dimmed
60
+ // placeholder in the flow so the drop target stays visible.
61
+ opacity: isDragging ? 0.4 : 1
62
+ };
63
+ const handle = /* @__PURE__ */ jsx(
64
+ IconButton,
65
+ {
66
+ size: "small",
67
+ "aria-label": t("sortable.drag"),
68
+ disabled,
69
+ sx: { cursor: disabled ? "default" : "grab", touchAction: "none", color: "text.disabled", flexShrink: 0 },
70
+ ...attributes,
71
+ ...listeners,
72
+ children: /* @__PURE__ */ jsx(DragIndicatorIcon, { fontSize: "small" })
73
+ }
74
+ );
75
+ return /* @__PURE__ */ jsx(Box, { ref: setNodeRef, style, children: children(handle) });
76
+ }
77
+ export {
78
+ SortableItem,
79
+ SortableList,
80
+ overlayHandle
81
+ };
@@ -0,0 +1,66 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import Box from "@mui/material/Box";
4
+ import Paper from "@mui/material/Paper";
5
+ import Stack from "@mui/material/Stack";
6
+ import Typography from "@mui/material/Typography";
7
+ import { useTheme } from "@mui/material/styles";
8
+ const GRAD = "linear-gradient(135deg, var(--rt-grad-from), var(--rt-grad-to))";
9
+ const SIZES = {
10
+ medium: { p: 2.5, minHeight: 132, gap: 1.5, value: 30, label: "subtitle2" },
11
+ compact: { p: 2, minHeight: 88, gap: 0.75, value: 24, label: "body2" }
12
+ };
13
+ function StatTile({ label, value, caption, icon, chart, size = "medium", sx, ...rest }) {
14
+ const theme = useTheme();
15
+ const m = SIZES[size] ?? SIZES.medium;
16
+ const gradient = theme.rt?.ui?.stat === "gradient";
17
+ const pc = theme.vars?.palette?.primary?.mainChannel ?? theme.palette.primary.mainChannel;
18
+ const gradientSx = gradient ? {
19
+ backgroundImage: GRAD,
20
+ color: theme.vars?.palette?.primary?.contrastText ?? theme.palette.primary.contrastText,
21
+ border: "none",
22
+ boxShadow: `0 6px 18px -8px rgba(${pc} / 0.45)`
23
+ } : {};
24
+ return /* @__PURE__ */ jsxs(
25
+ Paper,
26
+ {
27
+ "data-treatment": gradient ? "gradient" : "plain",
28
+ "data-size": size,
29
+ sx: { p: m.p, display: "flex", flexDirection: "column", gap: m.gap, minHeight: m.minHeight, ...gradientSx, ...sx },
30
+ ...rest,
31
+ children: [
32
+ /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1.5, sx: { alignItems: "flex-start" }, children: [
33
+ icon ? /* @__PURE__ */ jsx(
34
+ Box,
35
+ {
36
+ "aria-hidden": true,
37
+ sx: {
38
+ width: 40,
39
+ height: 40,
40
+ borderRadius: 2,
41
+ flexShrink: 0,
42
+ display: "grid",
43
+ placeItems: "center",
44
+ // On the gradient, a translucent chip in the tile's own text colour;
45
+ // on the plain surface, a soft accent-tinted chip with the accent icon.
46
+ ...gradient ? { backgroundColor: "rgba(255,255,255,0.20)", color: "inherit" } : { backgroundColor: `rgba(${pc} / 0.12)`, color: "primary.main" }
47
+ },
48
+ children: icon
49
+ }
50
+ ) : null,
51
+ /* @__PURE__ */ jsxs(Box, { sx: { minWidth: 0 }, children: [
52
+ /* @__PURE__ */ jsx(Typography, { variant: m.label, sx: { fontWeight: 600, lineHeight: 1.25 }, children: label }),
53
+ caption ? /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: gradient ? { opacity: 0.82 } : { color: "text.secondary" }, children: caption }) : null
54
+ ] })
55
+ ] }),
56
+ /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "flex-end", justifyContent: "space-between", gap: 1, mt: "auto" }, children: [
57
+ /* @__PURE__ */ jsx(Typography, { component: "div", sx: { fontSize: m.value, fontWeight: 700, lineHeight: 1, letterSpacing: "-0.01em", fontVariantNumeric: "tabular-nums" }, children: value }),
58
+ chart ? /* @__PURE__ */ jsx(Box, { sx: { flexShrink: 0, opacity: gradient ? 0.9 : 1 }, children: chart }) : null
59
+ ] })
60
+ ]
61
+ }
62
+ );
63
+ }
64
+ export {
65
+ StatTile as default
66
+ };
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import Chip from "@mui/material/Chip";
4
+ import { useTheme } from "@mui/material/styles";
5
+ import { chipColor, tagStyle } from "../lib/chipTone.js";
6
+ function StatusChip({ role = "idle", tagKey, tagIndex, ...rest }) {
7
+ const theme = useTheme();
8
+ const secondaryIsOwnColour = theme.palette.secondary?.main !== theme.palette.info?.main && theme.palette.secondary?.main !== theme.palette.text?.secondary;
9
+ if (role === "tag") {
10
+ const { color, variant } = tagStyle(tagKey ?? rest.label, secondaryIsOwnColour, tagIndex);
11
+ return /* @__PURE__ */ jsx(Chip, { color, variant, ...rest });
12
+ }
13
+ return /* @__PURE__ */ jsx(Chip, { color: chipColor(role, tagKey ?? rest.label, secondaryIsOwnColour), ...rest });
14
+ }
15
+ export {
16
+ StatusChip as default
17
+ };
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import Box from "@mui/material/Box";
4
+ import { useTheme } from "@mui/material/styles";
5
+ import { chipColor } from "../lib/chipTone.js";
6
+ function StatusDot({ role = "idle", label, size = 8, sx, ...rest }) {
7
+ const theme = useTheme();
8
+ const palette = chipColor(role, label);
9
+ const hidden = rest["aria-hidden"] === true || rest["aria-hidden"] === "true";
10
+ return /* @__PURE__ */ jsx(
11
+ Box,
12
+ {
13
+ component: "span",
14
+ role: hidden ? void 0 : "img",
15
+ "aria-label": hidden ? void 0 : label,
16
+ title: hidden ? void 0 : label,
17
+ ...rest,
18
+ sx: [
19
+ {
20
+ display: "inline-block",
21
+ flexShrink: 0,
22
+ width: size,
23
+ height: size,
24
+ borderRadius: "50%",
25
+ /*
26
+ * `default` is the one role with no palette entry of its own — it is
27
+ * the absence of a colour owed, which reads as the muted mark a
28
+ * disabled control uses rather than as a hue.
29
+ */
30
+ bgcolor: palette === "default" ? "text.disabled" : `${palette}.main`,
31
+ // A pale theme's success on a pale ground is a dot you have to look
32
+ // for; the ring is what makes it land at 8px.
33
+ boxShadow: palette === "default" ? "none" : `0 0 0 2px rgba(${theme.vars.palette[palette].mainChannel} / 0.18)`
34
+ },
35
+ ...Array.isArray(sx) ? sx : [sx]
36
+ ]
37
+ }
38
+ );
39
+ }
40
+ export {
41
+ StatusDot as default
42
+ };