ppij-design-system 0.3.0 → 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 (58) hide show
  1. package/README.md +37 -0
  2. package/dist/components/account-sheet.d.ts +37 -0
  3. package/dist/components/account-sheet.js +25 -0
  4. package/dist/components/admin-drawer.js +1 -1
  5. package/dist/components/admin-top-bar.d.ts +47 -0
  6. package/dist/components/admin-top-bar.js +20 -0
  7. package/dist/components/bar-chart.d.ts +32 -0
  8. package/dist/components/bar-chart.js +28 -0
  9. package/dist/components/callout.d.ts +21 -0
  10. package/dist/components/callout.js +28 -0
  11. package/dist/components/distribution-bar.d.ts +25 -0
  12. package/dist/components/distribution-bar.js +21 -0
  13. package/dist/components/document-viewer.d.ts +38 -0
  14. package/dist/components/document-viewer.js +25 -0
  15. package/dist/components/expandable-text.d.ts +21 -0
  16. package/dist/components/expandable-text.js +33 -0
  17. package/dist/components/fact-list.d.ts +36 -0
  18. package/dist/components/fact-list.js +29 -0
  19. package/dist/components/figures-band.d.ts +32 -0
  20. package/dist/components/figures-band.js +28 -0
  21. package/dist/components/filter-chips.d.ts +23 -0
  22. package/dist/components/filter-chips.js +15 -0
  23. package/dist/components/filter-menu.d.ts +24 -0
  24. package/dist/components/filter-menu.js +15 -0
  25. package/dist/components/form-grid.d.ts +32 -0
  26. package/dist/components/form-grid.js +29 -0
  27. package/dist/components/identity-chip.d.ts +18 -0
  28. package/dist/components/identity-chip.js +12 -0
  29. package/dist/components/input.d.ts +12 -1
  30. package/dist/components/input.js +2 -2
  31. package/dist/components/list-row.d.ts +29 -0
  32. package/dist/components/list-row.js +27 -0
  33. package/dist/components/map-card.d.ts +45 -0
  34. package/dist/components/map-card.js +45 -0
  35. package/dist/components/member-card-block.d.ts +5 -0
  36. package/dist/components/member-card-block.js +1 -1
  37. package/dist/components/member-card.css +2 -0
  38. package/dist/components/panel-card.d.ts +28 -0
  39. package/dist/components/panel-card.js +25 -0
  40. package/dist/components/pie-chart.d.ts +34 -0
  41. package/dist/components/pie-chart.js +75 -0
  42. package/dist/components/reveal-value.d.ts +26 -0
  43. package/dist/components/reveal-value.js +12 -0
  44. package/dist/components/searchable-multi-select.d.ts +34 -0
  45. package/dist/components/searchable-multi-select.js +36 -0
  46. package/dist/components/stat-strip.d.ts +27 -0
  47. package/dist/components/stat-strip.js +25 -0
  48. package/dist/components/status-card.d.ts +7 -2
  49. package/dist/components/status-card.js +6 -5
  50. package/dist/components/tabs.d.ts +8 -2
  51. package/dist/components/tabs.js +18 -5
  52. package/dist/components/timeline.d.ts +21 -5
  53. package/dist/components/timeline.js +20 -6
  54. package/dist/components/workspace-selector.d.ts +36 -0
  55. package/dist/components/workspace-selector.js +74 -0
  56. package/dist/index.d.ts +23 -2
  57. package/dist/index.js +21 -0
  58. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # ppij-design-system
2
+
3
+ PPIJ presentation components on React 19, Tailwind CSS v4 and shadcn/ui on
4
+ Radix, with the framework-free token layer beside them. Public, member portal,
5
+ admin, Pemira, Keycloak and email surfaces consume this package; it holds no
6
+ application data logic, no API clients and no secrets.
7
+
8
+ ```sh
9
+ npm install ppij-design-system
10
+ npm install github:ppij-dev/ppij-design-system # the released tarball route
11
+ ```
12
+
13
+ ## Exports
14
+
15
+ - `ppij-design-system` — the React components from the package root
16
+ - `ppij-design-system/tokens.css` — the token layer, plus Tailwind's own import
17
+ - `ppij-design-system/tokens.json` — the same values, framework free
18
+
19
+ ## Consumer setup
20
+
21
+ Two things are the consuming app's:
22
+
23
+ - **Tailwind CSS v4.** `tokens.css` opens with `@import "tailwindcss"`, so
24
+ install `tailwindcss` and `@tailwindcss/vite` (or the equivalent PostCSS
25
+ plugin) and import the token file once. The component classes are Tailwind
26
+ utilities; without the plugin the token import cannot resolve.
27
+ - **The automatic JSX runtime**, as in any React 19 app
28
+ (`@vitejs/plugin-react`, or `esbuild: { jsx: "automatic" }`).
29
+
30
+ ```tsx
31
+ import { AdminTopBar, MapCard, PanelCard, PieChart } from "ppij-design-system";
32
+ import "ppij-design-system/tokens.css";
33
+ import tokens from "ppij-design-system/tokens.json";
34
+ ```
35
+
36
+ The component guidelines and every component story live in the Storybook Docs
37
+ pages at https://ppij-dev.github.io/ppij-design-system/.
@@ -0,0 +1,37 @@
1
+ import * as React from "react";
2
+ export type AccountSheetLocale = "id" | "en";
3
+ export interface AccountSheetItem {
4
+ label: string;
5
+ /** Second line under the label, for example the destination route. */
6
+ sublabel?: string;
7
+ icon: React.ReactNode;
8
+ onSelect: () => void;
9
+ /** `danger` marks the sign-out row. */
10
+ tone?: "default" | "danger";
11
+ }
12
+ export interface AccountSheetProps {
13
+ open: boolean;
14
+ onOpenChange: (open: boolean) => void;
15
+ name: string;
16
+ detail: string;
17
+ initials: string;
18
+ items: AccountSheetItem[];
19
+ /** Slot above the rows, for example the Portal | Admin switch. */
20
+ switchSlot?: React.ReactNode;
21
+ signOutLabel?: string;
22
+ onSignOut?: () => void;
23
+ locale: AccountSheetLocale;
24
+ /** Overrides the built-in close label for this locale. */
25
+ closeLabel?: string;
26
+ /** Accessible name of the dialog. Defaults to the locale's profile label. */
27
+ label?: string;
28
+ className?: string;
29
+ }
30
+ /**
31
+ * The account sheet: who is signed in, the context switch, the account rows and
32
+ * sign out, as a bottom sheet on every screen width. Escape, the backdrop and
33
+ * the close button dismiss it; focus starts on the close button and returns to
34
+ * the trigger that opened it; every row is an ordinary button of at least 44px.
35
+ * The rows and their destinations are the caller's.
36
+ */
37
+ export declare function AccountSheet({ open, onOpenChange, name, detail, initials, items, switchSlot, signOutLabel, onSignOut, locale, closeLabel, label, className, }: AccountSheetProps): React.JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { IconLogout, IconX } from "@tabler/icons-react";
4
+ import { cn } from "../lib/utils";
5
+ import { Dialog, DialogContent } from "./dialog";
6
+ const labels = {
7
+ id: { close: "Tutup profil", dialog: "Profil pengguna" },
8
+ en: { close: "Close profile", dialog: "User profile" },
9
+ };
10
+ /**
11
+ * The account sheet: who is signed in, the context switch, the account rows and
12
+ * sign out, as a bottom sheet on every screen width. Escape, the backdrop and
13
+ * the close button dismiss it; focus starts on the close button and returns to
14
+ * the trigger that opened it; every row is an ordinary button of at least 44px.
15
+ * The rows and their destinations are the caller's.
16
+ */
17
+ export function AccountSheet({ open, onOpenChange, name, detail, initials, items, switchSlot, signOutLabel, onSignOut, locale, closeLabel, label, className, }) {
18
+ const l = labels[locale];
19
+ const closeRef = React.useRef(null);
20
+ const dialogLabel = label ?? l.dialog;
21
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { "data-slot": "account-sheet", showCloseButton: false, "aria-label": dialogLabel, onOpenAutoFocus: (event) => {
22
+ event.preventDefault();
23
+ closeRef.current?.focus();
24
+ }, className: cn("bottom-0 top-auto left-1/2 w-[min(420px,calc(100vw-24px))] translate-x-[-50%] translate-y-0 gap-0 overflow-hidden rounded-b-none rounded-t-[14px] border border-b-0 bg-card p-0 pb-[max(10px,env(safe-area-inset-bottom))]", className), children: [_jsx("span", { "aria-hidden": true, className: "mx-auto mt-2.5 h-[5px] w-[38px] rounded-full bg-brand-border" }), _jsxs("div", { className: "relative flex items-center gap-2.5 border-b border-brand-border px-4 pt-2 pb-3 pr-14", children: [_jsx("span", { "aria-hidden": true, className: "grid size-9 shrink-0 place-items-center rounded-full bg-brand-red/8 text-[13px] font-extrabold text-brand-red", children: initials }), _jsxs("span", { className: "min-w-0", children: [_jsx("b", { className: "block truncate text-sm text-brand-ink", children: name }), _jsx("small", { className: "block truncate text-[11.5px] text-brand-muted", children: detail })] }), _jsx("button", { ref: closeRef, type: "button", "aria-label": closeLabel ?? l.close, onClick: () => onOpenChange(false), className: "absolute top-1 right-1 grid size-11 place-items-center rounded-[10px] text-brand-muted transition-colors hover:bg-secondary hover:text-brand-ink focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none", children: _jsx(IconX, { "aria-hidden": true, className: "size-[18px]" }) })] }), switchSlot ? _jsx("div", { className: "grid gap-1.5 px-4 pt-2.5", children: switchSlot }) : null, _jsxs("div", { className: "px-2 pt-1 pb-2.5", children: [items.map((item) => (_jsxs("button", { type: "button", onClick: item.onSelect, className: cn("flex min-h-11 w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-[13.5px] font-semibold transition-colors hover:bg-secondary focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none", item.tone === "danger" ? "text-destructive" : "text-brand-ink"), children: [_jsx("span", { "aria-hidden": true, className: cn("shrink-0 [&_svg]:size-4", item.tone === "danger" ? "text-destructive" : "text-brand-muted"), children: item.icon }), _jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "block truncate", children: item.label }), item.sublabel ? _jsx("small", { className: "block text-xs font-normal text-brand-muted", children: item.sublabel }) : null] })] }, item.label))), onSignOut && signOutLabel ? (_jsxs("button", { type: "button", onClick: onSignOut, className: "mt-1 flex min-h-11 w-full items-center gap-2.5 rounded-lg border-t border-brand-border px-2.5 py-2 text-left text-[13.5px] font-semibold text-destructive transition-colors hover:bg-secondary focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none", children: [_jsx(IconLogout, { "aria-hidden": true, className: "size-4 shrink-0" }), _jsx("span", { className: "min-w-0 truncate", children: signOutLabel })] })) : null] })] }) }));
25
+ }
@@ -31,5 +31,5 @@ const sizeClass = {
31
31
  export function AdminDrawer({ open, onOpenChange, title, description, locale, closeLabel, backLabel, density = "default", step = 1, onBack, children, footer, onCloseAutoFocus, size = "md", className, }) {
32
32
  const isDesktop = useIsDesktop();
33
33
  const showBack = step === 2 && onBack !== undefined;
34
- return (_jsx(Drawer, { open: open, onOpenChange: onOpenChange, direction: isDesktop ? "right" : "bottom", children: _jsxs(DrawerContent, { "data-admin-drawer": "", "data-size": size, onCloseAutoFocus: onCloseAutoFocus, "data-step": step, "data-density": density, className: cn("min-h-0 gap-0 p-0 text-foreground", "data-[vaul-drawer-direction=bottom]:max-h-[85vh]", sizeClass[size], density === "compact" && "text-[13px] leading-5", className), children: [_jsxs(DrawerHeader, { "data-vaul-no-drag": "", className: cn("shrink-0 flex-row items-start justify-between gap-3 border-b border-border p-4 text-left", density === "compact" && "p-3"), children: [_jsxs("div", { className: "min-w-0", children: [_jsx(DrawerTitle, { className: density === "compact" ? "text-sm leading-5" : "text-base leading-6", children: title }), description ? _jsx(DrawerDescription, { className: cn("break-words", density === "compact" && "text-xs leading-5"), children: description }) : null] }), _jsx(DrawerClose, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: density === "compact" && isDesktop ? "icon-sm" : "icon", "aria-label": (closeLabel ?? labels[locale].close), className: "shrink-0", children: _jsx(IconX, { className: "size-4" }) }) })] }), _jsx("div", { "data-admin-drawer-body": "", className: cn("min-h-0 flex-1 overflow-y-auto p-4", density === "compact" && "p-3"), children: children }), showBack || footer ? (_jsxs(DrawerFooter, { "data-vaul-no-drag": "", "data-admin-drawer-footer": "", className: cn("shrink-0 flex-row flex-wrap items-center justify-end gap-2 border-t border-border bg-card p-4", density === "compact" && "p-3"), children: [showBack ? (_jsx(Button, { type: "button", variant: "outline", size: density === "compact" ? "sm" : "default", onClick: onBack, className: "mr-auto", children: (backLabel ?? labels[locale].back) })) : null, footer] })) : null] }) }));
34
+ return (_jsx(Drawer, { open: open, onOpenChange: onOpenChange, direction: isDesktop ? "right" : "bottom", children: _jsxs(DrawerContent, { "data-admin-drawer": "", "data-size": size, onCloseAutoFocus: onCloseAutoFocus, "data-step": step, "data-density": density, className: cn("min-h-0 gap-0 p-0 text-foreground", "data-[vaul-drawer-direction=bottom]:max-h-[85vh]", sizeClass[size], density === "compact" && "text-[13px] leading-5", className), children: [_jsxs(DrawerHeader, { "data-vaul-no-drag": "", className: cn("shrink-0 flex-row items-start justify-between gap-3 border-b border-border p-4 text-left", density === "compact" && "p-3"), children: [_jsxs("div", { className: "min-w-0", children: [_jsx(DrawerTitle, { className: density === "compact" ? "text-sm leading-5" : "text-base leading-6", children: title }), description ? _jsx(DrawerDescription, { className: cn("break-words", density === "compact" && "text-xs leading-5"), children: description }) : null] }), _jsx(DrawerClose, { asChild: true, children: _jsx(Button, { type: "button", variant: "ghost", size: density === "compact" && isDesktop ? "icon-sm" : "icon", "aria-label": (closeLabel ?? labels[locale].close), className: "shrink-0 max-[600px]:size-11", children: _jsx(IconX, { className: "size-4" }) }) })] }), _jsx("div", { "data-admin-drawer-body": "", className: cn("min-h-0 flex-1 overflow-y-auto p-4", density === "compact" && "p-3"), children: children }), showBack || footer ? (_jsxs(DrawerFooter, { "data-vaul-no-drag": "", "data-admin-drawer-footer": "", className: cn("shrink-0 flex-row flex-wrap items-center justify-end gap-2 border-t border-border bg-card p-4", density === "compact" && "p-3"), children: [showBack ? (_jsx(Button, { type: "button", variant: "outline", size: density === "compact" ? "sm" : "default", onClick: onBack, className: "mr-auto", children: (backLabel ?? labels[locale].back) })) : null, footer] })) : null] }) }));
35
35
  }
@@ -0,0 +1,47 @@
1
+ import * as React from "react";
2
+ export type TopBarLocale = "id" | "en";
3
+ export interface TopBarPathItem {
4
+ label: string;
5
+ /** Only the last path segment is a link; the title carries the current page. */
6
+ href?: string;
7
+ }
8
+ export interface AdminTopBarIdentity {
9
+ name: string;
10
+ initials: string;
11
+ }
12
+ export interface AdminTopBarProps {
13
+ /** Trail above the current page, for example Admin › Organisasi. */
14
+ path?: TopBarPathItem[];
15
+ /** The current page title; it ends the trail and is reported as the current page. */
16
+ title: string;
17
+ /** Quiet ambient facts (a date, a timezone), hidden below 1100px to keep the bar calm. */
18
+ meta?: React.ReactNode[];
19
+ /** Right-hand controls; the bar raises them to the 44px touch floor below 600px. */
20
+ actions?: React.ReactNode;
21
+ /** Renders the bell. It carries the locale accessible name. */
22
+ onNotificationsClick?: () => void;
23
+ /** Unread count beside the bell; announced with the button's name. */
24
+ notificationCount?: number;
25
+ identity?: AdminTopBarIdentity;
26
+ /** Makes the identity a button that opens the account sheet. */
27
+ onIdentityClick?: () => void;
28
+ /** Phone brand lock-up: the logo, its alt text and the label beside it. */
29
+ brand?: {
30
+ src: string;
31
+ alt: string;
32
+ label: string;
33
+ };
34
+ /** Slot at the very start of the bar, for example a sidebar toggle. */
35
+ leading?: React.ReactNode;
36
+ /** The shell mounts exactly one variant, so only one `<header>` landmark exists. */
37
+ variant: "desktop" | "phone";
38
+ locale: TopBarLocale;
39
+ className?: string;
40
+ }
41
+ /**
42
+ * The admin shell's top bar. Desktop: the breadcrumb trail, quiet facts, the
43
+ * bar's actions and the identity. Phone: the brand, one workspace slot in
44
+ * `leading` and the 44px identity target. The bar owns no route and no session;
45
+ * labels, links and callbacks arrive as props.
46
+ */
47
+ export declare function AdminTopBar({ path, title, meta, actions, onNotificationsClick, notificationCount, identity, onIdentityClick, brand, leading, variant, locale, className, }: AdminTopBarProps): React.JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { IconBell, IconChevronRight } from "@tabler/icons-react";
3
+ import { cn } from "../lib/utils";
4
+ const labels = {
5
+ id: { breadcrumb: "Jejak halaman", notifications: "Notifikasi", identity: "Profil pengguna", brand: "PPI Jepang" },
6
+ en: { breadcrumb: "Breadcrumb", notifications: "Notifications", identity: "User profile", brand: "PPI Japan" },
7
+ };
8
+ /**
9
+ * The admin shell's top bar. Desktop: the breadcrumb trail, quiet facts, the
10
+ * bar's actions and the identity. Phone: the brand, one workspace slot in
11
+ * `leading` and the 44px identity target. The bar owns no route and no session;
12
+ * labels, links and callbacks arrive as props.
13
+ */
14
+ export function AdminTopBar({ path = [], title, meta = [], actions, onNotificationsClick, notificationCount, identity, onIdentityClick, brand, leading, variant, locale, className, }) {
15
+ const l = labels[locale];
16
+ if (variant === "phone") {
17
+ return (_jsxs("header", { "data-slot": "admin-top-bar", "data-variant": "phone", className: cn("sticky top-0 z-25 flex h-15 shrink-0 items-center justify-between border-b border-brand-border bg-card px-4", className), children: [brand ? (_jsxs("span", { className: "flex min-w-0 items-center gap-2 text-[15px] font-bold text-brand-ink", children: [_jsx("img", { src: brand.src, alt: brand.alt, className: "size-[34px] shrink-0 rounded-md border border-brand-border object-contain" }), _jsx("span", { className: "truncate", children: brand.label })] })) : (_jsx("span", { className: "truncate text-[15px] font-bold text-brand-ink", children: title })), _jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: [leading, identity ? (_jsx("button", { type: "button", onClick: onIdentityClick, "aria-label": identity.name, "aria-haspopup": onIdentityClick ? "dialog" : undefined, className: "grid size-11 shrink-0 place-items-center rounded-[10px] bg-brand-red/8 text-[13px] font-extrabold text-brand-red focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none", children: identity.initials })) : null] })] }));
18
+ }
19
+ return (_jsxs("header", { "data-slot": "admin-top-bar", "data-variant": "desktop", className: cn("flex h-17 shrink-0 items-center justify-between gap-4 border-b border-brand-border bg-card px-[clamp(24px,3vw,48px)]", className), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2.5", children: [leading, _jsx("nav", { "aria-label": l.breadcrumb, className: "min-w-0", children: _jsxs("ol", { className: "flex min-w-0 items-center gap-2.5 text-sm text-brand-muted", children: [path.map((item, index) => (_jsxs("li", { className: "flex min-w-0 items-center gap-2.5", children: [item.href ? (_jsx("a", { href: item.href, className: "truncate hover:text-brand-ink", children: item.label })) : (_jsx("span", { className: "truncate", children: item.label })), _jsx(IconChevronRight, { "aria-hidden": true, className: "size-4 shrink-0 text-brand-border" })] }, `${item.label}-${index}`))), _jsx("li", { className: "min-w-0", children: _jsx("strong", { "aria-current": "page", className: "block truncate font-bold text-brand-ink", children: title }) })] }) })] }), _jsxs("div", { className: "flex shrink-0 items-center gap-2.5 text-[13px] text-brand-muted", children: [meta.length ? (_jsx("span", { className: "hidden items-center gap-2.5 min-[1100px]:flex", children: meta.map((item, index) => (_jsx("span", { children: item }, index))) })) : null, actions || onNotificationsClick ? (_jsxs("span", { className: "flex items-center gap-1.5 max-[600px]:[&_[data-slot=button]]:min-h-11 min-[600px]:[&_[data-slot=button]]:min-h-8", children: [actions, onNotificationsClick ? (_jsxs("button", { type: "button", onClick: onNotificationsClick, "aria-label": notificationCount ? `${l.notifications}, ${notificationCount}` : l.notifications, className: "relative grid size-8 place-items-center rounded-md text-brand-muted transition-colors hover:bg-secondary hover:text-brand-ink focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none max-[600px]:size-11", children: [_jsx(IconBell, { "aria-hidden": true, className: "size-5" }), notificationCount ? (_jsx("span", { "aria-hidden": true, className: "absolute -top-0.5 -right-0.5 grid min-w-4.5 place-items-center rounded-full bg-brand-red px-1 text-[11px] leading-4.5 font-bold text-white", children: notificationCount })) : null] })) : null] })) : null, identity ? (onIdentityClick ? (_jsxs("button", { type: "button", onClick: onIdentityClick, "aria-label": identity.name, "aria-haspopup": "dialog", className: "flex items-center gap-2 rounded-full pr-1 pl-0.5 transition-colors hover:bg-secondary focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none max-[600px]:min-h-11 min-[600px]:h-8", children: [_jsx("span", { "aria-hidden": true, className: "grid size-8 place-items-center rounded-full bg-brand-red/8 text-xs font-extrabold text-brand-red", children: identity.initials }), _jsx("span", { className: "hidden max-w-40 truncate font-bold text-brand-ink min-[900px]:block", children: identity.name })] })) : (_jsxs("span", { className: "flex items-center gap-2 max-[600px]:min-h-11 min-[600px]:h-8", children: [_jsx("span", { "aria-hidden": true, className: "grid size-8 place-items-center rounded-full bg-brand-red/8 text-xs font-extrabold text-brand-red", children: identity.initials }), _jsx("span", { className: "hidden max-w-40 truncate font-bold text-brand-ink min-[900px]:block", children: identity.name })] }))) : null] })] }));
20
+ }
@@ -0,0 +1,32 @@
1
+ export interface BarChartDatum {
2
+ label: string;
3
+ value: number;
4
+ /** Short context for the bar, for example "12 pendaftaran". */
5
+ hint?: string;
6
+ }
7
+ export interface BarChartProps {
8
+ data: BarChartDatum[];
9
+ /** `vertical` is the monthly trend; `horizontal` the facet distribution. */
10
+ orientation?: "vertical" | "horizontal";
11
+ /** Paints the newest bar in brand red, the earlier ones in the tint. */
12
+ highlightLast?: boolean;
13
+ /** Scale ceiling; defaults to the largest value, never below 1. */
14
+ max?: number;
15
+ /** Chart height in pixels for the vertical orientation. Default 150. */
16
+ height?: number;
17
+ formatValue?: (value: number) => string;
18
+ /** Hairlines behind the vertical bars. */
19
+ gridLines?: boolean;
20
+ /** `table` adds a screen-reader `<table>` carrying the same numbers. */
21
+ fallback?: "table" | "none";
22
+ ariaLabel?: string;
23
+ className?: string;
24
+ }
25
+ /**
26
+ * The bar chart: a monthly trend read top to bottom, or a facet distribution
27
+ * read left to right. Both orientations are divs on the token palette, so no
28
+ * chart library and no SVG maths is needed. Every bar carries a `title` and its
29
+ * label, a small value keeps a 4px visible bar, and `fallback="table"` gives a
30
+ * screen reader the same numbers.
31
+ */
32
+ export declare function BarChart({ data, orientation, highlightLast, max, height, formatValue, gridLines, fallback, ariaLabel, className, }: BarChartProps): import("react").JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils";
3
+ const defaultFormat = (value) => value.toLocaleString("id-ID");
4
+ /**
5
+ * The bar chart: a monthly trend read top to bottom, or a facet distribution
6
+ * read left to right. Both orientations are divs on the token palette, so no
7
+ * chart library and no SVG maths is needed. Every bar carries a `title` and its
8
+ * label, a small value keeps a 4px visible bar, and `fallback="table"` gives a
9
+ * screen reader the same numbers.
10
+ */
11
+ export function BarChart({ data, orientation = "vertical", highlightLast = false, max, height = 150, formatValue = defaultFormat, gridLines = false, fallback = "none", ariaLabel, className, }) {
12
+ const ceiling = Math.max(max ?? 0, 1, ...data.map((item) => item.value));
13
+ const summary = data.length
14
+ ? data.map((item) => `${item.label} ${formatValue(item.value)}${item.hint ? ` (${item.hint})` : ""}`).join(", ")
15
+ : "Tidak ada data";
16
+ const table = fallback === "table" ? (_jsxs("table", { className: "sr-only", children: [_jsx("caption", { children: ariaLabel ?? summary }), _jsx("tbody", { children: data.map((item) => (_jsxs("tr", { children: [_jsx("th", { scope: "row", children: item.label }), _jsx("td", { children: formatValue(item.value) }), _jsx("td", { children: item.hint ?? "" })] }, item.label))) })] })) : null;
17
+ if (orientation === "horizontal") {
18
+ return (_jsxs("div", { "data-slot": "bar-chart", "data-orientation": "horizontal", className: cn("grid min-w-0 gap-3", className), children: [_jsx("ol", { role: "img", "aria-label": ariaLabel ?? summary, className: "grid list-none gap-2.5", children: data.map((item) => {
19
+ const pct = ceiling > 0 ? (item.value / ceiling) * 100 : 0;
20
+ return (_jsxs("li", { className: "grid grid-cols-[minmax(96px,150px)_minmax(0,1fr)_52px] items-center gap-2.5 text-[13px]", children: [_jsx("span", { className: "min-w-0 truncate text-brand-ink", children: item.label }), _jsx("span", { className: "h-2.5 min-w-0 overflow-hidden rounded-full bg-secondary", children: _jsx("span", { className: "block h-full rounded-full bg-brand-red", style: { width: `${pct}%` }, title: item.hint ?? `${item.label}: ${formatValue(item.value)}` }) }), _jsx("strong", { className: "text-right font-extrabold tabular-nums text-brand-ink", children: formatValue(item.value) })] }, item.label));
21
+ }) }), table] }));
22
+ }
23
+ return (_jsxs("div", { "data-slot": "bar-chart", "data-orientation": "vertical", className: cn("grid min-w-0 gap-2", className), children: [_jsxs("div", { role: "img", "aria-label": ariaLabel ?? summary, className: "relative flex items-stretch gap-2", style: { height }, children: [gridLines ? (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 grid grid-rows-4", children: [0, 1, 2, 3].map((line) => (_jsx("span", { className: "border-t border-dashed border-brand-border" }, line))) })) : null, data.map((item, index) => {
24
+ const pct = ceiling > 0 ? (item.value / ceiling) * 100 : 0;
25
+ const last = index === data.length - 1;
26
+ return (_jsxs("span", { className: "flex min-w-0 flex-1 flex-col justify-end gap-1.5", children: [_jsx("span", { className: "text-center text-[10px] font-semibold text-brand-muted tabular-nums", children: formatValue(item.value) }), _jsx("span", { className: cn("block min-h-1 w-full rounded-t-[3px]", highlightLast && last ? "bg-brand-red" : "bg-[#efbbc4]"), style: { height: `${pct}%` }, title: item.hint ?? `${item.label}: ${formatValue(item.value)}` })] }, item.label));
27
+ })] }), _jsx("div", { "aria-hidden": true, className: "flex gap-2", children: data.map((item) => (_jsx("span", { className: "min-w-0 flex-1 truncate text-center text-[10px] text-brand-muted", children: item.label }, item.label))) }), table] }));
28
+ }
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ export type CalloutTone = "info" | "success" | "warning" | "danger" | "neutral";
3
+ export interface CalloutProps {
4
+ tone?: CalloutTone;
5
+ /** Leading Tabler icon. The component hides it from assistive technology. */
6
+ icon?: React.ReactNode;
7
+ title?: string;
8
+ children: React.ReactNode;
9
+ /** Inline control slot, for example the "Lengkapi data" Button. */
10
+ action?: React.ReactNode;
11
+ /** `compact` is the admin drawer note: a 12px body with 10px padding. */
12
+ density?: "default" | "compact";
13
+ className?: string;
14
+ }
15
+ /**
16
+ * One inline note: a colour step, an optional icon, a title and a body, plus
17
+ * one action. Replaces the flag, claim, read-only, sample-note, audit-note,
18
+ * study-prompt, section-note, drawer-stamp and drawer-note rules. Text contrast
19
+ * is checked per tone: the lightest is 5.7:1 against its own surface.
20
+ */
21
+ export declare function Callout({ tone, icon, title, children, action, density, className }: CalloutProps): React.JSX.Element;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils";
3
+ /** The prototype's nine inline notes: the same shape, four urgency steps and a quiet neutral. */
4
+ const TONES = {
5
+ info: "bg-[#f0f4fa] text-[#4c627b]",
6
+ success: "bg-[#f1f5f3] text-[#35604a]",
7
+ warning: "bg-[#fff5e6] text-[#7c5014]",
8
+ danger: "border border-[#f0ccd4] bg-[#fdf2f4] text-[#8f1730]",
9
+ neutral: "border border-[#e6eaed] bg-[#f8fafb] text-[#56636d]",
10
+ };
11
+ /** `role="alert"` interrupts, so only the tone that reports a problem claims it. */
12
+ const ROLES = {
13
+ info: "status",
14
+ success: "status",
15
+ warning: undefined,
16
+ danger: "alert",
17
+ neutral: undefined,
18
+ };
19
+ /**
20
+ * One inline note: a colour step, an optional icon, a title and a body, plus
21
+ * one action. Replaces the flag, claim, read-only, sample-note, audit-note,
22
+ * study-prompt, section-note, drawer-stamp and drawer-note rules. Text contrast
23
+ * is checked per tone: the lightest is 5.7:1 against its own surface.
24
+ */
25
+ export function Callout({ tone = "info", icon, title, children, action, density = "default", className }) {
26
+ const compact = density === "compact";
27
+ return (_jsxs("div", { "data-slot": "callout", "data-tone": tone, role: ROLES[tone], className: cn("rounded-lg", compact ? "px-3 py-2.5 text-xs leading-5" : "px-3.5 py-3 text-[13px] leading-5", TONES[tone], className), children: [_jsxs("div", { className: "flex items-start gap-2.5", children: [icon ? (_jsx("span", { "aria-hidden": true, className: "mt-0.5 shrink-0 [&_svg]:size-[18px]", children: icon })) : null, _jsxs("div", { className: "min-w-0", children: [title ? _jsx("strong", { className: cn("block font-bold", compact ? "text-xs" : "text-[13px]"), children: title }) : null, _jsx("div", { className: cn("min-w-0", title && "mt-0.5 text-xs"), children: children })] })] }), action ? (_jsx("div", { className: cn("mt-2.5 flex flex-wrap items-center gap-2", icon && "min-[600px]:pl-7", "max-[600px]:[&_[data-slot=button]]:min-h-11"), children: action })) : null] }));
28
+ }
@@ -0,0 +1,25 @@
1
+ export interface DistributionSegment {
2
+ label: string;
3
+ value: number;
4
+ /** Defaults to the token ramp: brand red first, then its tints, then the neutral greys. */
5
+ color?: string;
6
+ }
7
+ export interface DistributionBarProps {
8
+ segments: DistributionSegment[];
9
+ /** `sm` is the 8px figures-band bar, `md` the 12px stacked bar. A taller card bar takes `className="h-[18px]"`. */
10
+ size?: "sm" | "md";
11
+ /** `inline` is the figures-band legend, `list` the Statistik legend with counts. */
12
+ legend?: "list" | "inline" | "none";
13
+ /** Adds a labelled total row to the `list` legend. */
14
+ totalLabel?: string;
15
+ formatValue?: (value: number) => string;
16
+ ariaLabel?: string;
17
+ className?: string;
18
+ }
19
+ /**
20
+ * One stacked bar: the completeness split on the dashboard figures band, the
21
+ * degree composition beside it and the Statistik completeness card. Every
22
+ * segment carries its own `title`, the legend is text rather than colour alone,
23
+ * and the accessible name names every segment with its count.
24
+ */
25
+ export declare function DistributionBar({ segments, size, legend, totalLabel, formatValue, ariaLabel, className }: DistributionBarProps): import("react").JSX.Element;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils";
3
+ const PALETTE = ["var(--brand-red)", "#efbbc4", "#c6ccd2", "#fae6e9", "#1c1f23"];
4
+ const defaultFormat = (value) => value.toLocaleString("id-ID");
5
+ /**
6
+ * One stacked bar: the completeness split on the dashboard figures band, the
7
+ * degree composition beside it and the Statistik completeness card. Every
8
+ * segment carries its own `title`, the legend is text rather than colour alone,
9
+ * and the accessible name names every segment with its count.
10
+ */
11
+ export function DistributionBar({ segments, size = "sm", legend = "list", totalLabel, formatValue = defaultFormat, ariaLabel, className }) {
12
+ const total = segments.reduce((sum, segment) => sum + segment.value, 0);
13
+ const summary = segments.length
14
+ ? segments.map((segment) => `${segment.label} ${formatValue(segment.value)}`).join(", ")
15
+ : "Tidak ada data";
16
+ return (_jsxs("div", { "data-slot": "distribution-bar", "data-size": size, className: cn("grid min-w-0 gap-2", className), children: [_jsx("div", { role: "img", "aria-label": ariaLabel ?? summary, className: cn("flex min-w-0 gap-0.5 overflow-hidden", size === "sm" ? "h-2 rounded-full" : "h-3 rounded-md"), children: segments.map((segment, index) => {
17
+ const pct = total > 0 ? (segment.value / total) * 100 : 0;
18
+ const color = segment.color ?? PALETTE[index % PALETTE.length];
19
+ return pct > 0 ? (_jsx("span", { className: "block h-full min-w-0", style: { width: `${pct}%`, background: color }, title: `${segment.label}: ${formatValue(segment.value)}` }, segment.label)) : null;
20
+ }) }), legend === "inline" ? (_jsx("p", { className: "flex min-w-0 flex-wrap gap-x-3 gap-y-1 text-[11px] text-brand-muted", children: segments.map((segment, index) => (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { "aria-hidden": true, className: "size-2.5 shrink-0 rounded-[3px]", style: { background: segment.color ?? PALETTE[index % PALETTE.length] } }), _jsxs("span", { className: "truncate", children: [segment.label, " ", _jsx("strong", { className: "font-bold text-brand-ink tabular-nums", children: formatValue(segment.value) })] })] }, segment.label))) })) : null, legend === "list" ? (_jsxs("ul", { className: "flex min-w-0 flex-wrap gap-x-[18px] gap-y-2 text-xs text-brand-muted", children: [segments.map((segment, index) => (_jsxs("li", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { "aria-hidden": true, className: "size-2.5 shrink-0 rounded-[3px]", style: { background: segment.color ?? PALETTE[index % PALETTE.length] } }), _jsx("span", { className: "truncate", children: segment.label }), _jsx("strong", { className: "font-bold text-brand-ink tabular-nums", children: formatValue(segment.value) })] }, segment.label))), totalLabel ? (_jsxs("li", { className: "flex min-w-0 items-center gap-1.5 border-t border-brand-border pt-1.5", children: [_jsx("span", { className: "truncate font-semibold text-brand-ink", children: totalLabel }), _jsx("strong", { className: "font-extrabold text-brand-ink tabular-nums", children: formatValue(total) })] })) : null] })) : null] }));
21
+ }
@@ -0,0 +1,38 @@
1
+ import * as React from "react";
2
+ import { type FactListItem } from "./fact-list";
3
+ export type DocumentViewerLocale = "id" | "en";
4
+ export interface DocumentSide {
5
+ id: string;
6
+ /** Tab label, for example "Sisi depan". */
7
+ label: string;
8
+ kind: "image" | "pdf";
9
+ /** File name in the file row. */
10
+ name: string;
11
+ /** Quiet line under the name, for example "Berkas contoh · JPG". */
12
+ meta?: string;
13
+ }
14
+ export interface DocumentViewerProps {
15
+ /** The sides the document has; one or two in practice. */
16
+ sides: DocumentSide[];
17
+ /** The selected side id. */
18
+ value: string;
19
+ onChange: (id: string) => void;
20
+ /** The app's preview stage: the real image, the PDF viewer or a placeholder. */
21
+ children: React.ReactNode;
22
+ /** Facts under the stage, for example the document's own fields. */
23
+ facts?: FactListItem[];
24
+ loading?: boolean;
25
+ emptyLabel?: string;
26
+ locale: DocumentViewerLocale;
27
+ /** Accessible name of the stage region. */
28
+ label?: string;
29
+ className?: string;
30
+ }
31
+ /**
32
+ * The student-card frame: the side switcher, the file row, the app's preview
33
+ * stage and the document's own facts. The file itself and the preview stay
34
+ * app-side; this component owns the frame, the accessible names and the
35
+ * loading and empty states. Side tabs are 44px below 600px and 32px from
36
+ * 600px up, and the facts list is one column on a phone.
37
+ */
38
+ export declare function DocumentViewer({ sides, value, onChange, children, facts, loading, emptyLabel, locale, label, className }: DocumentViewerProps): React.JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { IconFileTypePdf, IconPhoto } from "@tabler/icons-react";
3
+ import { cn } from "../lib/utils";
4
+ import { FactList } from "./fact-list";
5
+ import { Skeleton } from "./skeleton";
6
+ const labels = {
7
+ id: { preview: "Pratinjau berkas", image: "Berkas gambar", pdf: "Berkas PDF", empty: "Tidak ada dokumen untuk ditampilkan.", loading: "Memuat pratinjau dokumen" },
8
+ en: { preview: "File preview", image: "Image file", pdf: "PDF file", empty: "No document to show.", loading: "Loading the document preview" },
9
+ };
10
+ /**
11
+ * The student-card frame: the side switcher, the file row, the app's preview
12
+ * stage and the document's own facts. The file itself and the preview stay
13
+ * app-side; this component owns the frame, the accessible names and the
14
+ * loading and empty states. Side tabs are 44px below 600px and 32px from
15
+ * 600px up, and the facts list is one column on a phone.
16
+ */
17
+ export function DocumentViewer({ sides, value, onChange, children, facts, loading = false, emptyLabel, locale, label, className }) {
18
+ const l = labels[locale];
19
+ const selected = sides.find((side) => side.id === value) ?? sides[0];
20
+ const stageName = label ?? l.preview;
21
+ if (!sides.length) {
22
+ return (_jsx("div", { "data-slot": "document-viewer", className: cn("grid rounded-[11px] border border-[#dce3e7] bg-card p-5 text-[13px] text-brand-muted", className), children: emptyLabel ?? l.empty }));
23
+ }
24
+ return (_jsxs("div", { "data-slot": "document-viewer", "data-loading": loading, className: cn("grid min-w-0 gap-3", className), children: [sides.length > 1 ? (_jsx("div", { role: "group", "aria-label": stageName, className: "inline-flex w-fit max-w-full gap-1 overflow-x-auto rounded-[9px] border border-[#e4e8eb] bg-card p-[3px]", children: sides.map((side) => (_jsx("button", { type: "button", "aria-pressed": side.id === selected?.id, onClick: () => onChange(side.id), className: cn("min-h-11 shrink-0 rounded-[7px] px-3 text-[13px] font-semibold transition-colors focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none min-[600px]:min-h-8", side.id === selected?.id ? "bg-secondary text-brand-ink" : "text-brand-muted hover:text-brand-ink"), children: side.label }, side.id))) })) : null, _jsxs("div", { className: "overflow-hidden rounded-[11px] border border-[#dce3e7] bg-card", children: [selected ? (_jsxs("div", { className: "flex items-center gap-3 border-b border-[#e4e9ec] px-4 py-3 text-brand-red", children: [_jsx("span", { role: "img", "aria-label": selected.kind === "pdf" ? l.pdf : l.image, className: "shrink-0", children: selected.kind === "pdf" ? _jsx(IconFileTypePdf, { "aria-hidden": true, className: "size-[22px]" }) : _jsx(IconPhoto, { "aria-hidden": true, className: "size-[22px]" }) }), _jsxs("span", { className: "grid min-w-0 gap-0.5", children: [_jsx("strong", { className: "truncate text-sm text-brand-ink", children: selected.name }), selected.meta ? _jsx("span", { className: "truncate text-xs text-brand-muted", children: selected.meta }) : null] })] })) : null, _jsx("div", { role: "region", "aria-label": stageName, "aria-live": "polite", className: "grid min-h-[220px] place-items-center bg-card p-5", children: loading ? (_jsxs("div", { role: "status", "aria-busy": "true", "aria-label": l.loading, className: "grid w-full max-w-[300px] gap-3", children: [_jsx(Skeleton, { "aria-hidden": "true", className: "h-[178px] w-full rounded-lg" }), _jsx(Skeleton, { "aria-hidden": "true", className: "h-4 w-2/3" }), _jsx(Skeleton, { "aria-hidden": "true", className: "h-4 w-1/3" })] })) : (children) }), facts?.length ? _jsx(FactList, { variant: "twoColumn", items: facts, className: "border-t border-[#e4e9ec] bg-[#fbfcfc] px-4 py-3.5" }) : null] })] }));
25
+ }
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ export interface ExpandableTextProps {
3
+ /** The text to clamp. Only a string: the clamp needs the text itself. */
4
+ children: string;
5
+ /** Lines shown before the toggle appears. Default 3. */
6
+ lines?: number;
7
+ /** Starts expanded; the component is uncontrolled. Default false. */
8
+ defaultExpanded?: boolean;
9
+ /** Toggle label while clamped. Default "Lihat selengkapnya". */
10
+ expandLabel?: string;
11
+ /** Toggle label while expanded. Default "Tampilkan lebih sedikit". */
12
+ collapseLabel?: string;
13
+ className?: string;
14
+ }
15
+ /**
16
+ * Long text clamped to a few lines with a Lihat selengkapnya toggle. The clamp
17
+ * is CSS, so the whole text stays in the accessibility tree and stays findable;
18
+ * the toggle only appears when the text actually overflows, and it reports
19
+ * `aria-expanded`.
20
+ */
21
+ export declare function ExpandableText({ children, lines, defaultExpanded, expandLabel, collapseLabel, className }: ExpandableTextProps): React.JSX.Element;
@@ -0,0 +1,33 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { cn } from "../lib/utils";
4
+ /**
5
+ * Long text clamped to a few lines with a Lihat selengkapnya toggle. The clamp
6
+ * is CSS, so the whole text stays in the accessibility tree and stays findable;
7
+ * the toggle only appears when the text actually overflows, and it reports
8
+ * `aria-expanded`.
9
+ */
10
+ export function ExpandableText({ children, lines = 3, defaultExpanded = false, expandLabel = "Lihat selengkapnya", collapseLabel = "Tampilkan lebih sedikit", className }) {
11
+ const textRef = React.useRef(null);
12
+ const [expanded, setExpanded] = React.useState(defaultExpanded);
13
+ const [canExpand, setCanExpand] = React.useState(false);
14
+ // The clamp is applied while collapsed, so overflow is measurable. Expanded
15
+ // text is not re-measured: `canExpand` keeps its last value so the collapse
16
+ // direction stays available.
17
+ React.useEffect(() => {
18
+ const element = textRef.current;
19
+ if (!element)
20
+ return;
21
+ // Only measure while clamped: expanded text cannot overflow its clamp box.
22
+ if (expanded) {
23
+ setCanExpand(true);
24
+ return;
25
+ }
26
+ const measure = () => setCanExpand(element.scrollHeight > element.clientHeight + 1);
27
+ measure();
28
+ const observer = new ResizeObserver(measure);
29
+ observer.observe(element);
30
+ return () => observer.disconnect();
31
+ }, [children, lines, expanded]);
32
+ return (_jsxs("span", { "data-slot": "expandable-text", className: cn("grid justify-items-start gap-1", className), children: [_jsx("span", { ref: textRef, className: cn("block min-w-0 break-words", !expanded && "overflow-hidden"), style: expanded ? undefined : { display: "-webkit-box", WebkitBoxOrient: "vertical", WebkitLineClamp: lines }, children: children }), canExpand ? (_jsx("button", { type: "button", "aria-expanded": expanded, onClick: () => setExpanded((value) => !value), className: cn("-ml-px text-left text-[13px] font-bold text-brand-red underline-offset-2 hover:underline", "min-h-8 max-[600px]:min-h-11", "focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none"), children: expanded ? collapseLabel : expandLabel })) : null] }));
33
+ }
@@ -0,0 +1,36 @@
1
+ import * as React from "react";
2
+ export interface FactListItem {
3
+ label: string;
4
+ value: React.ReactNode;
5
+ /** Spans the full width in the column variants, for example a long address. */
6
+ span?: boolean;
7
+ /** Quiets the value, for text that is absent or not applicable. */
8
+ muted?: boolean;
9
+ }
10
+ export type FactListVariant = "rows" | "inline" | "twoColumn";
11
+ export interface FactListProps {
12
+ items: FactListItem[];
13
+ /**
14
+ * `rows` is the drawer detail: a fixed 96px term column, one fact per row,
15
+ * with a hairline between rows. `twoColumn` stacks term over value in two
16
+ * columns, for review field groups. `inline` puts the value beside its term,
17
+ * two facts per row, for short document facts. Both column variants collapse
18
+ * to one column below 600px; `rows` keeps its term column on a phone.
19
+ */
20
+ variant?: FactListVariant;
21
+ /** A final emphasised row, for example a coverage total. */
22
+ total?: {
23
+ label: string;
24
+ value: React.ReactNode;
25
+ };
26
+ /** Shown instead of the list when there is nothing to show. */
27
+ emptyLabel?: string;
28
+ className?: string;
29
+ }
30
+ /**
31
+ * Term and value rows: the drawer detail, the review field groups, the document
32
+ * facts, the coverage list, the role detail and the invite summary. It renders
33
+ * a real `<dl>`, so every term stays beside its value and a masked value keeps
34
+ * its own label. Presentation only; the page decides what each value says.
35
+ */
36
+ export declare function FactList({ items, variant, total, emptyLabel, className }: FactListProps): React.JSX.Element | null;
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils";
3
+ const listClass = {
4
+ rows: "grid",
5
+ twoColumn: "grid grid-cols-2 gap-x-5 gap-y-3 max-[600px]:grid-cols-1",
6
+ inline: "grid grid-cols-2 gap-x-5 gap-y-2.5 max-[600px]:grid-cols-1",
7
+ };
8
+ const itemClass = {
9
+ rows: "grid min-w-0 grid-cols-[96px_minmax(0,1fr)] items-baseline gap-x-3 border-b border-brand-border py-2",
10
+ twoColumn: "grid min-w-0 gap-0.5",
11
+ inline: "grid min-w-0 grid-cols-[auto_minmax(0,1fr)] items-baseline gap-x-2",
12
+ };
13
+ const valueClass = {
14
+ rows: "text-[13px] font-semibold",
15
+ twoColumn: "text-sm font-semibold",
16
+ inline: "text-[13px] font-semibold",
17
+ };
18
+ /**
19
+ * Term and value rows: the drawer detail, the review field groups, the document
20
+ * facts, the coverage list, the role detail and the invite summary. It renders
21
+ * a real `<dl>`, so every term stays beside its value and a masked value keeps
22
+ * its own label. Presentation only; the page decides what each value says.
23
+ */
24
+ export function FactList({ items, variant = "rows", total, emptyLabel, className }) {
25
+ if (!items.length && !total) {
26
+ return emptyLabel ? _jsx("p", { "data-slot": "fact-list-empty", className: cn("m-0 text-[13px] text-brand-muted", className), children: emptyLabel }) : null;
27
+ }
28
+ return (_jsxs("dl", { "data-slot": "fact-list", "data-variant": variant, className: cn("m-0", listClass[variant], className), children: [items.map((item, index) => (_jsxs("div", { className: cn(itemClass[variant], item.span && "col-span-full max-[600px]:col-span-1"), children: [_jsx("dt", { className: "text-xs text-brand-muted", children: item.label }), _jsx("dd", { className: cn("m-0 break-words", valueClass[variant], item.muted ? "text-brand-muted" : "text-brand-ink"), children: item.value })] }, `${item.label}-${index}`))), total ? (_jsxs("div", { className: "col-span-full flex items-baseline justify-between gap-3 border-t border-brand-border pt-2.5", children: [_jsx("dt", { className: "text-xs text-brand-muted", children: total.label }), _jsx("dd", { className: "m-0 text-sm font-bold text-brand-ink", children: total.value })] })) : null] }));
29
+ }
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ export interface FiguresBandGroup {
3
+ label: string;
4
+ /** The headline figure, already formatted by the page. */
5
+ value: string;
6
+ /** `warn` marks a figure that needs attention without changing its size. */
7
+ tone?: "default" | "warn";
8
+ /** The quiet lines under the value; the page states the numbers here too. */
9
+ sub?: React.ReactNode;
10
+ /** Decoration under the sub-line, for example a DistributionBar. Always hidden from assistive technology. */
11
+ visual?: React.ReactNode;
12
+ linkLabel: string;
13
+ onSelect: () => void;
14
+ }
15
+ export interface FiguresBandProps {
16
+ title: string;
17
+ description?: string;
18
+ groups: FiguresBandGroup[];
19
+ /** Columns from 1101px up. Defaults to the group count, capped at four. */
20
+ columns?: number;
21
+ className?: string;
22
+ }
23
+ /**
24
+ * The grouped figures band: one headline per question, its quiet context and a
25
+ * deep link, with an optional visual under each group. The dashboard's national
26
+ * and komsat scopes are the two instances. Presentation only: the metric choice
27
+ * and the numbers are the page's, and the visual slot is decoration because the
28
+ * sub-line already states the same figures.
29
+ */
30
+ export declare function FiguresBand({ title, description, groups, columns, className }: FiguresBandProps): React.JSX.Element;
31
+ /** The 3px separator the prototype drew between two sub-line facts. */
32
+ export declare function FiguresBandSeparator(): React.JSX.Element;