ppij-design-system 0.2.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 (60) 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.d.ts +7 -1
  5. package/dist/components/admin-drawer.js +7 -2
  6. package/dist/components/admin-top-bar.d.ts +47 -0
  7. package/dist/components/admin-top-bar.js +20 -0
  8. package/dist/components/bar-chart.d.ts +32 -0
  9. package/dist/components/bar-chart.js +28 -0
  10. package/dist/components/callout.d.ts +21 -0
  11. package/dist/components/callout.js +28 -0
  12. package/dist/components/distribution-bar.d.ts +25 -0
  13. package/dist/components/distribution-bar.js +21 -0
  14. package/dist/components/document-viewer.d.ts +38 -0
  15. package/dist/components/document-viewer.js +25 -0
  16. package/dist/components/drawer.js +1 -1
  17. package/dist/components/expandable-text.d.ts +21 -0
  18. package/dist/components/expandable-text.js +33 -0
  19. package/dist/components/fact-list.d.ts +36 -0
  20. package/dist/components/fact-list.js +29 -0
  21. package/dist/components/figures-band.d.ts +32 -0
  22. package/dist/components/figures-band.js +28 -0
  23. package/dist/components/filter-chips.d.ts +23 -0
  24. package/dist/components/filter-chips.js +15 -0
  25. package/dist/components/filter-menu.d.ts +24 -0
  26. package/dist/components/filter-menu.js +15 -0
  27. package/dist/components/form-grid.d.ts +32 -0
  28. package/dist/components/form-grid.js +29 -0
  29. package/dist/components/identity-chip.d.ts +18 -0
  30. package/dist/components/identity-chip.js +12 -0
  31. package/dist/components/input.d.ts +12 -1
  32. package/dist/components/input.js +2 -2
  33. package/dist/components/list-row.d.ts +29 -0
  34. package/dist/components/list-row.js +27 -0
  35. package/dist/components/map-card.d.ts +45 -0
  36. package/dist/components/map-card.js +45 -0
  37. package/dist/components/member-card-block.d.ts +5 -0
  38. package/dist/components/member-card-block.js +1 -1
  39. package/dist/components/member-card.css +2 -0
  40. package/dist/components/panel-card.d.ts +28 -0
  41. package/dist/components/panel-card.js +25 -0
  42. package/dist/components/pie-chart.d.ts +34 -0
  43. package/dist/components/pie-chart.js +75 -0
  44. package/dist/components/reveal-value.d.ts +26 -0
  45. package/dist/components/reveal-value.js +12 -0
  46. package/dist/components/searchable-multi-select.d.ts +34 -0
  47. package/dist/components/searchable-multi-select.js +36 -0
  48. package/dist/components/stat-strip.d.ts +27 -0
  49. package/dist/components/stat-strip.js +25 -0
  50. package/dist/components/status-card.d.ts +7 -2
  51. package/dist/components/status-card.js +6 -5
  52. package/dist/components/tabs.d.ts +8 -2
  53. package/dist/components/tabs.js +18 -5
  54. package/dist/components/timeline.d.ts +21 -5
  55. package/dist/components/timeline.js +20 -6
  56. package/dist/components/workspace-selector.d.ts +36 -0
  57. package/dist/components/workspace-selector.js +74 -0
  58. package/dist/index.d.ts +24 -3
  59. package/dist/index.js +21 -0
  60. package/package.json +1 -1
@@ -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;
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { IconArrowRight } from "@tabler/icons-react";
3
+ import { cn } from "../lib/utils";
4
+ /** One column below 600px, two at 601-1100px, then the band's own count from 1101px up. */
5
+ const columnClass = {
6
+ 1: "grid-cols-1",
7
+ 2: "min-[601px]:grid-cols-2",
8
+ 3: "min-[601px]:grid-cols-2 min-[1101px]:grid-cols-3",
9
+ 4: "min-[601px]:grid-cols-2 min-[1101px]:grid-cols-4",
10
+ };
11
+ /**
12
+ * The grouped figures band: one headline per question, its quiet context and a
13
+ * deep link, with an optional visual under each group. The dashboard's national
14
+ * and komsat scopes are the two instances. Presentation only: the metric choice
15
+ * and the numbers are the page's, and the visual slot is decoration because the
16
+ * sub-line already states the same figures.
17
+ */
18
+ export function FiguresBand({ title, description, groups, columns, className }) {
19
+ const columnCount = Math.min(Math.max(columns ?? groups.length, 1), 4);
20
+ return (_jsxs("section", { "data-slot": "figures-band", "aria-label": title, className: cn("rounded-xl border border-brand-border bg-card px-[22px] pt-5 pb-2 max-[600px]:px-4", className), children: [_jsxs("div", { className: "mb-1 flex flex-wrap items-baseline justify-between gap-3.5", children: [_jsx("h2", { className: "m-0 text-lg font-bold tracking-tight text-brand-ink", children: title }), description ? _jsx("p", { className: "m-0 text-xs text-brand-muted", children: description }) : null] }), _jsx("div", { className: cn("grid grid-cols-1", columnClass[columnCount],
21
+ // Separators are scoped by width so only one of them ever applies:
22
+ // a left hairline in the four-up row, a top hairline once the groups wrap.
23
+ "[&>button:not(:first-child)]:border-brand-border", "min-[1101px]:[&>button:not(:first-child)]:border-l", "max-[1100px]:[&>button:not(:first-child)]:border-t", "min-[601px]:max-[1100px]:[&>button:nth-child(2)]:border-t-0"), children: groups.map((group) => (_jsxs("button", { type: "button", onClick: group.onSelect, className: "grid min-h-11 content-start gap-[7px] px-[18px] pt-4 pb-[18px] text-left transition-colors hover:bg-brand-red/3 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none max-[600px]:min-h-11 max-[600px]:px-0.5 max-[600px]:py-3.5 min-[600px]:min-h-8", children: [_jsx("span", { className: "text-xs font-bold text-brand-muted", children: group.label }), _jsx("span", { className: cn("text-[30px] leading-[1.1] font-extrabold tracking-tight", group.tone === "warn" ? "text-[#a66d20]" : "text-brand-ink"), children: group.value }), group.sub ? _jsx("span", { className: "flex flex-wrap items-center gap-x-1.5 gap-y-1 text-xs text-brand-muted", children: group.sub }) : null, group.visual ? (_jsx("span", { "aria-hidden": true, className: "min-w-0 pt-0.5", children: group.visual })) : null, _jsxs("span", { className: "mt-0.5 inline-flex items-center gap-1.5 text-xs font-bold text-brand-red", children: [group.linkLabel, _jsx(IconArrowRight, { "aria-hidden": true, className: "size-3.5" })] })] }, group.label))) })] }));
24
+ }
25
+ /** The 3px separator the prototype drew between two sub-line facts. */
26
+ export function FiguresBandSeparator() {
27
+ return _jsx("span", { "aria-hidden": true, className: "inline-block size-[3px] shrink-0 rounded-full bg-brand-border" });
28
+ }
@@ -0,0 +1,23 @@
1
+ export interface FilterChip {
2
+ label: string;
3
+ value: string;
4
+ /** Per-value count, for example how many rows the value contains. */
5
+ count?: number;
6
+ onRemove: () => void;
7
+ }
8
+ export interface FilterChipsProps {
9
+ chips: FilterChip[];
10
+ onClearAll?: () => void;
11
+ clearAllLabel?: string;
12
+ /** Names the row for assistive technology; each chip's accessible name starts with it. */
13
+ label?: string;
14
+ className?: string;
15
+ }
16
+ /**
17
+ * The removable active-filter row: the Statistik warehouse chips, the member
18
+ * directory row, the invitation list and the management lists. Every chip
19
+ * removes its own filter and states what it removes, the counts arrive as
20
+ * props, and clear-all is a separate control so a single mis-tap cannot wipe
21
+ * the row.
22
+ */
23
+ export declare function FilterChips({ chips, onClearAll, clearAllLabel, label, className }: FilterChipsProps): import("react").JSX.Element | null;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { IconX } from "@tabler/icons-react";
3
+ import { cn } from "../lib/utils";
4
+ /**
5
+ * The removable active-filter row: the Statistik warehouse chips, the member
6
+ * directory row, the invitation list and the management lists. Every chip
7
+ * removes its own filter and states what it removes, the counts arrive as
8
+ * props, and clear-all is a separate control so a single mis-tap cannot wipe
9
+ * the row.
10
+ */
11
+ export function FilterChips({ chips, onClearAll, clearAllLabel = "Hapus semua filter", label = "Filter aktif", className }) {
12
+ if (!chips.length && !onClearAll)
13
+ return null;
14
+ return (_jsxs("div", { "data-slot": "filter-chips", className: cn("flex min-w-0 flex-wrap items-center gap-2", className), children: [_jsx("ul", { "aria-label": label, className: "flex min-w-0 flex-wrap items-center gap-2", children: chips.map((chip) => (_jsx("li", { className: "min-w-0", children: _jsxs("button", { type: "button", onClick: chip.onRemove, "aria-label": chip.count === undefined ? `${label}: ${chip.label}` : `${label}: ${chip.label} (${chip.count})`, className: "flex max-w-full items-center gap-1.5 rounded-full border border-[#ecd7db] bg-[#fdf5f6] px-3 py-1.5 text-xs font-semibold text-brand-ink transition-colors hover:bg-brand-red/5 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none max-[600px]:min-h-11 min-[600px]:min-h-8", children: [_jsxs("span", { className: "min-w-0 truncate", children: [chip.label, chip.count === undefined ? null : _jsx("strong", { className: "ml-1 font-extrabold text-brand-red", children: chip.count })] }), _jsx(IconX, { "aria-hidden": true, className: "size-3.5 shrink-0 text-brand-muted" })] }) }, `${chip.value}-${chip.label}`))) }), onClearAll ? (_jsxs("button", { type: "button", onClick: onClearAll, "aria-label": clearAllLabel, className: "flex items-center gap-1.5 rounded-md px-2 py-1.5 text-xs font-bold text-brand-muted transition-colors hover:text-brand-ink focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none max-[600px]:min-h-11 min-[600px]:min-h-8", children: [_jsx(IconX, { "aria-hidden": true, className: "size-3.5" }), clearAllLabel] })) : null] }));
15
+ }
@@ -0,0 +1,24 @@
1
+ export interface FilterMenuOption {
2
+ value: string;
3
+ label: string;
4
+ }
5
+ export interface FilterMenuProps {
6
+ label: string;
7
+ /** The selected value; the empty string is "all". */
8
+ value: string;
9
+ options: FilterMenuOption[];
10
+ /** The row that clears the filter, for example "Semua jenis". */
11
+ allLabel: string;
12
+ onChange: (value: string) => void;
13
+ /** Keeps the caption for assistive technology but drops it from the layout. */
14
+ hideLabel?: boolean;
15
+ disabled?: boolean;
16
+ className?: string;
17
+ }
18
+ /**
19
+ * The non-searchable single-select with checkmarks: the verifikasi kind,
20
+ * ownership, feedback target and reason filters. The trigger names the control
21
+ * and the current value, the menu is a radio group so the selected row reports
22
+ * `aria-checked`, and the checkmark is decoration on top of that.
23
+ */
24
+ export declare function FilterMenu({ label, value, options, allLabel, onChange, hideLabel, disabled, className }: FilterMenuProps): import("react").JSX.Element;
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { IconCheck, IconChevronDown } from "@tabler/icons-react";
3
+ import { cn } from "../lib/utils";
4
+ import { Button } from "./button";
5
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger, } from "./dropdown-menu";
6
+ /**
7
+ * The non-searchable single-select with checkmarks: the verifikasi kind,
8
+ * ownership, feedback target and reason filters. The trigger names the control
9
+ * and the current value, the menu is a radio group so the selected row reports
10
+ * `aria-checked`, and the checkmark is decoration on top of that.
11
+ */
12
+ export function FilterMenu({ label, value, options, allLabel, onChange, hideLabel = false, disabled = false, className }) {
13
+ const selectedLabel = options.find((option) => option.value === value)?.label ?? allLabel;
14
+ return (_jsxs("div", { "data-slot": "filter-menu", "data-hide-label": hideLabel, className: cn("grid min-w-0 gap-1.5", className), children: [_jsx("span", { className: cn("text-[13px] font-semibold text-brand-muted", hideLabel && "sr-only"), children: label }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "outline", disabled: disabled, "aria-label": `${label}: ${selectedLabel}`, className: "flex w-max max-w-full min-h-11 justify-start gap-2.5 px-3 text-[13px] min-[600px]:h-8 min-[600px]:min-h-8", children: [_jsx("span", { className: "truncate", children: selectedLabel }), _jsx(IconChevronDown, { "aria-hidden": true, className: "size-4 shrink-0 text-brand-muted" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "min-w-[190px] bg-card", children: _jsxs(DropdownMenuRadioGroup, { value: value, onValueChange: onChange, children: [_jsxs(DropdownMenuRadioItem, { value: "", className: "min-h-11 gap-1.5 min-[600px]:min-h-8", children: [_jsx(IconCheck, { "aria-hidden": true, className: cn("size-4 text-brand-red", value === "" ? undefined : "invisible") }), allLabel] }), options.map((option) => (_jsxs(DropdownMenuRadioItem, { value: option.value, className: "min-h-11 gap-1.5 min-[600px]:min-h-8", children: [_jsx(IconCheck, { "aria-hidden": true, className: cn("size-4 text-brand-red", value === option.value ? undefined : "invisible") }), option.label] }, option.value)))] }) })] })] }));
15
+ }
@@ -0,0 +1,32 @@
1
+ import * as React from "react";
2
+ export interface FormFieldProps {
3
+ label: string;
4
+ /** Quiet help line under the control. */
5
+ hint?: string;
6
+ /** Validation message; renders `role="alert"` and marks the control invalid. */
7
+ error?: string;
8
+ /** Spans every column of the surrounding `FormGrid`. */
9
+ wide?: boolean;
10
+ children: React.ReactNode;
11
+ className?: string;
12
+ }
13
+ /**
14
+ * One labelled field of a `FormGrid`. The label wraps the control, so the
15
+ * association needs no id. When `error` is set, the message carries
16
+ * `role="alert"` and the component marks the control with `aria-invalid` and
17
+ * `aria-describedby` pointing at that message; props the caller sets on the
18
+ * control win.
19
+ */
20
+ export declare function FormField({ label, hint, error, wide, children, className }: FormFieldProps): React.JSX.Element;
21
+ export interface FormGridProps {
22
+ /** Two columns from 600px up; always one column below. Default 2. */
23
+ columns?: 1 | 2;
24
+ children: React.ReactNode;
25
+ className?: string;
26
+ }
27
+ /**
28
+ * The two-column admin form grid: wide rows span both columns and every field
29
+ * is one column on a phone. Controls inside pick up the sizing band — 32px from
30
+ * 600px up, never under 44px below — so a page does not restate it.
31
+ */
32
+ export declare function FormGrid({ columns, children, className }: FormGridProps): React.JSX.Element;
@@ -0,0 +1,29 @@
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
+ * One labelled field of a `FormGrid`. The label wraps the control, so the
6
+ * association needs no id. When `error` is set, the message carries
7
+ * `role="alert"` and the component marks the control with `aria-invalid` and
8
+ * `aria-describedby` pointing at that message; props the caller sets on the
9
+ * control win.
10
+ */
11
+ export function FormField({ label, hint, error, wide = false, children, className }) {
12
+ const errorId = React.useId();
13
+ const childProps = (React.isValidElement(children) ? children.props : {});
14
+ const control = error && React.isValidElement(children)
15
+ ? React.cloneElement(children, {
16
+ ...(childProps["aria-invalid"] === undefined ? { "aria-invalid": true } : {}),
17
+ ...(childProps["aria-describedby"] === undefined ? { "aria-describedby": errorId } : {}),
18
+ })
19
+ : children;
20
+ return (_jsxs("label", { className: cn("grid min-w-0 gap-1.5 text-xs font-semibold text-brand-muted", wide && "col-span-full max-[600px]:col-span-1", className), children: [_jsx("span", { children: label }), control, hint ? _jsx("span", { className: "text-xs font-normal text-brand-muted", children: hint }) : null, error ? (_jsx("span", { id: errorId, role: "alert", className: "text-xs font-semibold text-destructive", children: error })) : null] }));
21
+ }
22
+ /**
23
+ * The two-column admin form grid: wide rows span both columns and every field
24
+ * is one column on a phone. Controls inside pick up the sizing band — 32px from
25
+ * 600px up, never under 44px below — so a page does not restate it.
26
+ */
27
+ export function FormGrid({ columns = 2, children, className }) {
28
+ return (_jsx("div", { "data-slot": "form-grid", "data-columns": columns, className: cn("grid items-start gap-x-3.5 gap-y-4", columns === 2 ? "grid-cols-2 max-[600px]:grid-cols-1" : "grid-cols-1", "min-[600px]:[&_input]:h-8 min-[600px]:[&_select]:h-8 min-[600px]:[&_[data-slot=select-trigger]]:h-8", "max-[600px]:[&_input]:min-h-11 max-[600px]:[&_select]:min-h-11 max-[600px]:[&_textarea]:min-h-11 max-[600px]:[&_[data-slot=select-trigger]]:min-h-11", "max-[600px]:[&_input]:text-base max-[600px]:[&_select]:text-base", className), children: children }));
29
+ }
@@ -0,0 +1,18 @@
1
+ export interface IdentityChipProps {
2
+ name: string;
3
+ /** Second line, for example the role or the unit. */
4
+ detail: string;
5
+ /** Two letters, shown in the avatar square. */
6
+ initials: string;
7
+ onClick: () => void;
8
+ /** Sidebar rail state: a 44px square that keeps the person's name as its accessible name. */
9
+ collapsed?: boolean;
10
+ className?: string;
11
+ }
12
+ /**
13
+ * The sidebar footer identity: who is signed in and which role they hold, as
14
+ * one button that opens the account sheet. The collapsed rail keeps a 44px
15
+ * square target and the person's name as the accessible name, never the
16
+ * initials alone.
17
+ */
18
+ export declare function IdentityChip({ name, detail, initials, onClick, collapsed, className }: IdentityChipProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { IconChevronRight } from "@tabler/icons-react";
3
+ import { cn } from "../lib/utils";
4
+ /**
5
+ * The sidebar footer identity: who is signed in and which role they hold, as
6
+ * one button that opens the account sheet. The collapsed rail keeps a 44px
7
+ * square target and the person's name as the accessible name, never the
8
+ * initials alone.
9
+ */
10
+ export function IdentityChip({ name, detail, initials, onClick, collapsed = false, className }) {
11
+ return (_jsxs("button", { "data-slot": "identity-chip", "data-collapsed": collapsed, type: "button", onClick: onClick, "aria-label": name, "aria-haspopup": "dialog", className: cn("flex items-center rounded-[10px] border border-brand-border bg-card text-left transition-colors hover:bg-secondary/50 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none", collapsed ? "mx-auto size-11 shrink-0 justify-center p-1.5" : "mx-2 mb-2.5 min-h-11 w-[calc(100%-16px)] gap-2.5 p-2.5 min-[600px]:h-8 min-[600px]:min-h-8 min-[600px]:py-0", className), children: [_jsx("span", { "aria-hidden": true, className: "grid size-7 shrink-0 place-items-center rounded-lg bg-brand-red/8 text-xs leading-none font-extrabold text-brand-red", children: initials }), collapsed ? null : (_jsxs(_Fragment, { children: [_jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-[13px] leading-4 font-bold text-brand-ink", children: name }), _jsx("span", { className: "block truncate text-xs leading-3.5 text-brand-muted", children: detail })] }), _jsx(IconChevronRight, { "aria-hidden": true, className: "size-3.5 shrink-0 text-brand-muted" })] }))] }));
12
+ }
@@ -1,3 +1,14 @@
1
1
  import * as React from "react";
2
- declare function Input({ className, type, ...props }: React.ComponentProps<"input">): React.JSX.Element;
2
+ /** The admin compact field text: 13px from 600px up, 16px below so iOS does not zoom on focus. */
3
+ export type InputTextSize = "compact";
4
+ export type InputProps = React.ComponentProps<"input"> & {
5
+ /**
6
+ * Compact admin text: 13px from 600px up and 16px below, which is the size an
7
+ * admin page would otherwise reach with its own `font-size` rule. Pair it with
8
+ * `data-size="sm"` for the 32px toolbar field; the 44px phone floor below
9
+ * 600px is part of the compact band.
10
+ */
11
+ textSize?: InputTextSize;
12
+ };
13
+ declare function Input({ className, type, textSize, ...props }: InputProps): React.JSX.Element;
3
14
  export { Input };
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { cn } from "../lib/utils";
3
- function Input({ className, type, ...props }) {
4
- return (_jsx("input", { type: type, "data-slot": "input", className: cn("h-9 w-full min-w-0 data-[size=sm]:h-8 data-[size=lg]:h-11 rounded-md border border-input bg-card px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30", "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", className), ...props }));
3
+ function Input({ className, type, textSize, ...props }) {
4
+ return (_jsx("input", { type: type, "data-slot": "input", "data-text-size": textSize, className: cn("h-9 w-full min-w-0 data-[size=sm]:h-8 data-[size=lg]:h-11 rounded-md border border-input bg-card px-3 py-1 shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 dark:bg-input/30", textSize === "compact" ? "text-base min-[600px]:text-[13px] max-[600px]:min-h-11" : "text-base md:text-sm", "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50", "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40", className), ...props }));
5
5
  }
6
6
  export { Input };
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+ export type ListRowTone = "default" | "danger" | "muted";
3
+ export interface ListRowProps {
4
+ /** Leading slot: an Avatar, an icon chip, a checkbox. */
5
+ leading?: React.ReactNode;
6
+ title: string;
7
+ subtitle?: string;
8
+ /** Quiet middle column, for example a date or a unit. */
9
+ meta?: React.ReactNode;
10
+ /** Right-hand slot: a Badge, a Button, a chevron. */
11
+ trailing?: React.ReactNode;
12
+ /** Badge rendered after the title. */
13
+ badge?: React.ReactNode;
14
+ tone?: ListRowTone;
15
+ /** Makes the whole row one button. */
16
+ onSelect?: () => void;
17
+ /** Makes the whole row one link; wins over `onSelect`. */
18
+ href?: string;
19
+ /** Highlights the row and reports it as current or pressed. */
20
+ selected?: boolean;
21
+ className?: string;
22
+ }
23
+ /**
24
+ * The one selectable row shape behind the priority list, the recent list, the
25
+ * management and invitation lists, the coverage list and the phone member card.
26
+ * It renders a real `<a>` or `<button>` so the whole row is a single tab stop;
27
+ * every child control inside `trailing` stays its own control.
28
+ */
29
+ export declare function ListRow({ leading, title, subtitle, meta, trailing, badge, tone, onSelect, href, selected, className, }: ListRowProps): React.JSX.Element;
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils";
3
+ const toneClass = {
4
+ default: "text-brand-ink",
5
+ danger: "text-destructive",
6
+ muted: "text-brand-muted",
7
+ };
8
+ /**
9
+ * The one selectable row shape behind the priority list, the recent list, the
10
+ * management and invitation lists, the coverage list and the phone member card.
11
+ * It renders a real `<a>` or `<button>` so the whole row is a single tab stop;
12
+ * every child control inside `trailing` stays its own control.
13
+ */
14
+ export function ListRow({ leading, title, subtitle, meta, trailing, badge, tone = "default", onSelect, href, selected = false, className, }) {
15
+ const body = (_jsxs(_Fragment, { children: [leading ? _jsx("span", { className: "flex shrink-0 items-center", children: leading }) : null, _jsxs("span", { className: "grid min-w-0 gap-1", children: [_jsxs("span", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [_jsx("strong", { className: cn("block text-sm leading-snug font-semibold break-words", toneClass[tone]), children: title }), badge] }), subtitle ? _jsx("small", { className: "block text-xs leading-snug break-words text-brand-muted", children: subtitle }) : null] }), meta ? _jsx("span", { className: "hidden text-xs text-brand-muted min-[600px]:block", children: meta }) : null, trailing ? _jsx("span", { className: "flex shrink-0 items-center justify-end gap-2", children: trailing }) : null] }));
16
+ const classes = cn("grid w-full items-center gap-x-3 gap-y-1 border-t border-brand-border px-1 py-3 text-left", "max-[600px]:min-h-11", (onSelect || href) && "transition-colors hover:bg-secondary/50 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none", selected && "bg-secondary/60", className);
17
+ // Only the slots that exist get a track, so the title keeps the flexible
18
+ // column whether or not the row has a leading slot or a meta column.
19
+ const columns = [leading ? "auto" : null, "minmax(0,1fr)", meta ? "auto" : null, "auto"].filter(Boolean).join(" ");
20
+ if (href) {
21
+ return (_jsx("a", { "data-slot": "list-row", href: href, "aria-current": selected ? "true" : undefined, className: classes, style: { gridTemplateColumns: columns }, children: body }));
22
+ }
23
+ if (onSelect) {
24
+ return (_jsx("button", { "data-slot": "list-row", type: "button", onClick: onSelect, "aria-pressed": selected, className: classes, style: { gridTemplateColumns: columns }, children: body }));
25
+ }
26
+ return (_jsx("div", { "data-slot": "list-row", className: classes, style: { gridTemplateColumns: columns }, children: body }));
27
+ }
@@ -0,0 +1,45 @@
1
+ import * as React from "react";
2
+ import { type StatStripItem } from "./stat-strip";
3
+ export interface MapCardChip {
4
+ label: string;
5
+ onClear: () => void;
6
+ }
7
+ export interface MapCardProps {
8
+ title: string;
9
+ description?: string;
10
+ /** The app's map canvas. The package imports no map library and no geography. */
11
+ children: React.ReactNode;
12
+ /** The selection summary body, rendered as an overlay, an inline bar or a sheet. */
13
+ panel: React.ReactNode;
14
+ /** Accessible name of the panel. Defaults to "Ringkasan peta". */
15
+ panelLabel?: string;
16
+ /** The stat strip under the map: three items in practice. */
17
+ footer?: StatStripItem[];
18
+ /** The clear-selection chip, shown when a unit is selected. */
19
+ chip?: MapCardChip;
20
+ onExpandChange?: (expanded: boolean) => void;
21
+ /** Controlled fullscreen state; uncontrolled when omitted. */
22
+ expanded?: boolean;
23
+ expandLabel?: string;
24
+ collapseLabel?: string;
25
+ /** The one-line summary the snap-sheet handle reads while collapsed. */
26
+ sheetSummary?: string;
27
+ /** Quiet note about units with no coordinates yet. */
28
+ unmappedNote?: React.ReactNode;
29
+ loading?: boolean;
30
+ /** The app's own loading state for the map canvas. */
31
+ loadingFallback?: React.ReactNode;
32
+ className?: string;
33
+ }
34
+ /**
35
+ * The map card frame: the approved height steps (420px at 900px and wider,
36
+ * 320px at 601-899px, 240px at 600px and below), the selection panel as an
37
+ * overlay on desktop, an inline bar on tablet and a snap sheet in fullscreen on
38
+ * a phone, plus the stat strip footer. The map itself, the geography and the
39
+ * metrics are the app's; `children` is the canvas.
40
+ *
41
+ * Escape closes fullscreen and returns focus to the expand toggle, the toggle
42
+ * and the panel controls are 44px below 600px and 32px from 600px up, and the
43
+ * panel comes before the footer in DOM order.
44
+ */
45
+ export declare function MapCard({ title, description, children, panel, panelLabel, footer, chip, onExpandChange, expanded, expandLabel, collapseLabel, sheetSummary, unmappedNote, loading, loadingFallback, className, }: MapCardProps): React.JSX.Element;
@@ -0,0 +1,45 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as React from "react";
3
+ import { IconChevronDown, IconChevronUp, IconMaximize, IconMinimize, IconX } from "@tabler/icons-react";
4
+ import { cn } from "../lib/utils";
5
+ import { Skeleton } from "./skeleton";
6
+ import { StatStrip } from "./stat-strip";
7
+ /**
8
+ * The map card frame: the approved height steps (420px at 900px and wider,
9
+ * 320px at 601-899px, 240px at 600px and below), the selection panel as an
10
+ * overlay on desktop, an inline bar on tablet and a snap sheet in fullscreen on
11
+ * a phone, plus the stat strip footer. The map itself, the geography and the
12
+ * metrics are the app's; `children` is the canvas.
13
+ *
14
+ * Escape closes fullscreen and returns focus to the expand toggle, the toggle
15
+ * and the panel controls are 44px below 600px and 32px from 600px up, and the
16
+ * panel comes before the footer in DOM order.
17
+ */
18
+ export function MapCard({ title, description, children, panel, panelLabel = "Ringkasan peta", footer, chip, onExpandChange, expanded, expandLabel = "Perbesar peta", collapseLabel = "Tutup layar penuh", sheetSummary, unmappedNote, loading = false, loadingFallback, className, }) {
19
+ const [internalExpanded, setInternalExpanded] = React.useState(false);
20
+ const [sheetOpen, setSheetOpen] = React.useState(false);
21
+ const toggleRef = React.useRef(null);
22
+ const controlled = expanded !== undefined;
23
+ const isExpanded = controlled ? expanded : internalExpanded;
24
+ const setExpanded = React.useCallback((next) => {
25
+ if (!controlled)
26
+ setInternalExpanded(next);
27
+ if (!next)
28
+ setSheetOpen(false);
29
+ onExpandChange?.(next);
30
+ }, [controlled, onExpandChange]);
31
+ // Escape closes fullscreen and puts focus back on the toggle that opened it.
32
+ React.useEffect(() => {
33
+ if (!isExpanded)
34
+ return;
35
+ const onKeyDown = (event) => {
36
+ if (event.key !== "Escape")
37
+ return;
38
+ setExpanded(false);
39
+ toggleRef.current?.focus();
40
+ };
41
+ document.addEventListener("keydown", onKeyDown);
42
+ return () => document.removeEventListener("keydown", onKeyDown);
43
+ }, [isExpanded, setExpanded]);
44
+ return (_jsxs("section", { "data-slot": "map-card", "data-expanded": isExpanded, "aria-label": title, className: cn("relative rounded-xl border border-brand-border bg-card p-5 max-[600px]:p-4", isExpanded && "fixed inset-0 z-50 flex flex-col overflow-y-auto rounded-none border-0", className), children: [_jsxs("header", { className: "mb-3.5 flex flex-wrap items-start justify-between gap-3.5", children: [_jsxs("div", { className: "min-w-0", children: [_jsx("h2", { className: "mb-1 text-lg font-bold tracking-tight text-brand-ink", children: title }), description ? _jsx("p", { className: "m-0 text-[13px] leading-snug text-brand-muted", children: description }) : null] }), _jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: [chip ? (_jsxs("button", { type: "button", onClick: chip.onClear, "aria-label": `Hapus pilihan: ${chip.label}`, className: "flex max-w-[16rem] items-center gap-1.5 rounded-full border border-brand-red bg-card px-3 py-1.5 text-xs font-bold text-brand-ink transition-colors hover:bg-brand-red/5 focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none max-[600px]:min-h-11 min-[600px]:min-h-8", children: [_jsx("span", { className: "truncate", children: chip.label }), _jsx(IconX, { "aria-hidden": true, className: "size-3.5 shrink-0 text-brand-muted" })] })) : null, _jsxs("button", { ref: toggleRef, type: "button", "aria-expanded": isExpanded, onClick: () => setExpanded(!isExpanded), className: "flex items-center gap-1.5 rounded-lg border border-brand-border bg-card px-3 text-[13px] font-bold text-brand-ink 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]:min-h-8", children: [isExpanded ? _jsx(IconMinimize, { "aria-hidden": true, className: "size-4" }) : _jsx(IconMaximize, { "aria-hidden": true, className: "size-4" }), isExpanded ? collapseLabel : expandLabel] })] })] }), _jsxs("div", { "data-slot": "map-canvas", className: cn("relative z-0 h-60 overflow-hidden rounded-[9px] border border-brand-border bg-[#e8edf2] min-[601px]:h-80 min-[900px]:h-[420px]", isExpanded && "h-auto min-h-0 flex-1"), children: [children, loading && loadingFallback ? (_jsx("div", { role: "status", "aria-label": "Memuat peta", className: "absolute inset-0 flex flex-col gap-3 bg-card p-4", children: loadingFallback })) : null, loading && !loadingFallback ? (_jsxs("div", { role: "status", "aria-busy": "true", "aria-label": "Memuat peta", className: "absolute inset-0 grid content-start gap-3 bg-card p-4", children: [_jsx(Skeleton, { "aria-hidden": "true", className: "h-5 w-44" }), _jsx(Skeleton, { "aria-hidden": "true", className: "h-full w-full" })] })) : null, _jsx("div", { "data-slot": "map-panel-overlay", className: "absolute top-2.5 right-2.5 hidden w-[230px] rounded-[10px] border border-brand-border bg-card p-3.5 shadow-lg min-[900px]:block", "aria-label": panelLabel, children: panel })] }), !isExpanded ? (_jsx("aside", { "data-slot": "map-panel-inline", "aria-label": panelLabel, className: "mt-3 flex min-w-0 flex-wrap items-center gap-x-6 gap-y-2 rounded-[10px] border border-brand-border px-4 py-3 min-[900px]:hidden", children: panel })) : null, isExpanded ? (_jsxs("div", { "data-slot": "map-sheet", className: "absolute inset-x-0 bottom-0 z-[1001] hidden rounded-t-[14px] border-t border-brand-border bg-card shadow-[0_-8px_24px_rgba(15,24,39,.14)] max-[899px]:block", children: [_jsxs("button", { type: "button", "aria-expanded": sheetOpen, onClick: () => setSheetOpen((value) => !value), className: "flex w-full min-h-11 flex-col items-center gap-2 px-4 pt-2.5 pb-2 text-[13px] font-bold text-brand-ink focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-none", children: [_jsx("span", { "aria-hidden": true, className: "h-[5px] w-9 rounded-full bg-brand-border" }), _jsxs("span", { className: "flex w-full items-center justify-center gap-1.5", children: [_jsx("span", { className: "min-w-0 truncate", children: sheetSummary ?? title }), sheetOpen ? _jsx(IconChevronDown, { "aria-hidden": true, className: "size-4" }) : _jsx(IconChevronUp, { "aria-hidden": true, className: "size-4" })] })] }), sheetOpen ? _jsx("div", { className: "min-w-0 px-4 pb-4", children: panel }) : null] })) : null, footer?.length ? _jsx(StatStrip, { items: footer, size: "compact", label: panelLabel }) : null, unmappedNote ? _jsx("p", { className: "mt-2.5 text-xs text-brand-muted", children: unmappedNote }) : null] }));
45
+ }
@@ -32,6 +32,11 @@ export interface MemberCardBlockProps {
32
32
  validUntil?: string | null;
33
33
  graceUntil?: string | null;
34
34
  suspendReason?: string | null;
35
+ /**
36
+ * AD/ART category line under the member number ("Anggota Luar Biasa"). The page passes it for
37
+ * LUAR_BIASA members only and null (or nothing) for everyone else, so the card shows no line.
38
+ */
39
+ categoryLine?: string | null;
35
40
  state: MemberCardState;
36
41
  /** The PPIJ logo shown on the front (the app serves /ppij-logo.png). */
37
42
  logoSrc: string;
@@ -150,7 +150,7 @@ function Front(p) {
150
150
  const l = LABELS[p.locale];
151
151
  const state = p.state;
152
152
  const ended = state === "expired" || state === "expired_grace";
153
- return (_jsxs("div", { className: `mc-face mc-front ${CARD_CLASS[state]}`, "data-theme": p.theme?.id, style: { "--mc-base": p.theme?.base }, children: [p.theme && !p.artworkFailed ? (_jsx("svg", { className: "mc-art", viewBox: p.theme.viewBox, preserveAspectRatio: "xMidYMid meet", "aria-hidden": "true", focusable: "false", children: _jsx("image", { href: p.theme.asset, width: p.theme.width, height: p.theme.height, onError: p.onArtworkError }) })) : null, _jsx("div", { className: "mc-shine", "aria-hidden": "true", style: p.shineStyle }), _jsxs("div", { className: "mc-row", children: [_jsx("img", { className: "mc-logo", src: p.logoSrc, alt: l.brand, width: 92, height: 63 }), _jsx("span", { className: "mc-chip", children: chipText(state, p, l) })] }), _jsxs("div", { className: "mc-id", children: [_jsx("span", { className: "mc-name", children: p.name }), state === "suspended" ? (_jsx("span", { className: "mc-note", children: p.suspendReason ?? "" })) : (_jsx("span", { className: "mc-number", children: p.number })), state === "change_pending" ? _jsx("span", { className: "mc-note", children: l.pending }) : null] }), _jsxs("div", { className: "mc-row mc-foot", children: [_jsxs("div", { className: "mc-col", children: [_jsx("span", { className: "mc-label", children: l.unitLabel }), _jsx("span", { className: "mc-val", children: unitLine(p) })] }), state !== "suspended" && p.validUntil ? (_jsxs("div", { className: "mc-col mc-right", children: [_jsx("span", { className: "mc-label", children: ended ? l.endedLabel : l.validLabel }), _jsx("span", { className: "mc-val", children: shortDate(p.validUntil, p.locale, true) })] })) : null] })] }));
153
+ return (_jsxs("div", { className: `mc-face mc-front ${CARD_CLASS[state]}`, "data-theme": p.theme?.id, style: { "--mc-base": p.theme?.base }, children: [p.theme && !p.artworkFailed ? (_jsx("svg", { className: "mc-art", viewBox: p.theme.viewBox, preserveAspectRatio: "xMidYMid meet", "aria-hidden": "true", focusable: "false", children: _jsx("image", { href: p.theme.asset, width: p.theme.width, height: p.theme.height, onError: p.onArtworkError }) })) : null, _jsx("div", { className: "mc-shine", "aria-hidden": "true", style: p.shineStyle }), _jsxs("div", { className: "mc-row", children: [_jsx("img", { className: "mc-logo", src: p.logoSrc, alt: l.brand, width: 92, height: 63 }), _jsx("span", { className: "mc-chip", children: chipText(state, p, l) })] }), _jsxs("div", { className: "mc-id", children: [_jsx("span", { className: "mc-name", children: p.name }), state === "suspended" ? (_jsx("span", { className: "mc-note", children: p.suspendReason ?? "" })) : (_jsx("span", { className: "mc-number", children: p.number })), p.categoryLine ? _jsx("span", { className: "mc-category", children: p.categoryLine }) : null, state === "change_pending" ? _jsx("span", { className: "mc-note", children: l.pending }) : null] }), _jsxs("div", { className: "mc-row mc-foot", children: [_jsxs("div", { className: "mc-col", children: [_jsx("span", { className: "mc-label", children: l.unitLabel }), _jsx("span", { className: "mc-val", children: unitLine(p) })] }), state !== "suspended" && p.validUntil ? (_jsxs("div", { className: "mc-col mc-right", children: [_jsx("span", { className: "mc-label", children: ended ? l.endedLabel : l.validLabel }), _jsx("span", { className: "mc-val", children: shortDate(p.validUntil, p.locale, true) })] })) : null] })] }));
154
154
  }
155
155
  /** Back face: motto, barcode of the membership number, printed number, hint and decorative map. */
156
156
  function Back(p) {
@@ -19,6 +19,8 @@
19
19
  .mc-front .mc-id { position:absolute; left:20px; top:98px; width:calc(64% - 24px); gap:3px; }
20
20
  .mc-front .mc-name { height:40px; font-size:17px; line-height:20px; text-wrap:wrap; }
21
21
  .mc-front .mc-number { font-size:12px; line-height:15px; }
22
+ /* AD/ART category line, Luar Biasa members only (Anggota Biasa shows no line). */
23
+ .mc-category { font-size:11px; line-height:14px; font-weight:600; letter-spacing:.06em; opacity:.94; }
22
24
  .mc-front .mc-note { font-size:11px; line-height:11px; max-height:22px; display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden; overflow-wrap:anywhere; }
23
25
  .mc-front .mc-foot { position:absolute; left:20px; right:20px; bottom:14px; height:36px; gap:8px; }
24
26
  .mc-front .mc-col:first-child { min-width:0; flex:1; }
@@ -0,0 +1,28 @@
1
+ import * as React from "react";
2
+ export type PanelCardHeadingLevel = 2 | 3 | 4 | 5 | 6;
3
+ export type PanelCardPadding = "page" | "panel" | "none";
4
+ export interface PanelCardProps {
5
+ title?: string;
6
+ description?: string;
7
+ /** Header control slot, for example a Button or an IconButton. */
8
+ action?: React.ReactNode;
9
+ /** Quiet status next to the title, for example a Badge. */
10
+ badge?: React.ReactNode;
11
+ /** Leading Tabler icon; the component sets its size and colour. */
12
+ icon?: React.ReactNode;
13
+ /** `panel` is the standard card, `page` the white page island, `none` for a bare body. */
14
+ padding?: PanelCardPadding;
15
+ /** `compact` is the admin scale: a 14px title and 12px description with 16px padding. */
16
+ density?: "default" | "compact";
17
+ /** Heading level of the title; the page owns the document outline. Default 2. */
18
+ headingLevel?: PanelCardHeadingLevel;
19
+ children: React.ReactNode;
20
+ className?: string;
21
+ }
22
+ /**
23
+ * The white island that every admin page is built from: one title, one quiet
24
+ * description, one header action and a body. Replaces `.work-panel`,
25
+ * `.recent-section`, `.side-card`, `.stat-card`, `.figures-band` and the three
26
+ * page islands. Presentation only; the caller owns the content and the action.
27
+ */
28
+ export declare function PanelCard({ title, description, action, badge, icon, padding, density, headingLevel, children, className, }: PanelCardProps): React.JSX.Element;
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils";
3
+ /** The prototype's islands: `panel` is the standard 22px card, `page` the 28px page island. */
4
+ const paddingClass = {
5
+ panel: "p-[22px] max-[700px]:p-[17px]",
6
+ page: "p-7 max-[900px]:p-5 max-[600px]:p-4",
7
+ none: "p-0",
8
+ };
9
+ const compactPaddingClass = {
10
+ panel: "p-4",
11
+ page: "p-5 max-[600px]:p-4",
12
+ none: "p-0",
13
+ };
14
+ /**
15
+ * The white island that every admin page is built from: one title, one quiet
16
+ * description, one header action and a body. Replaces `.work-panel`,
17
+ * `.recent-section`, `.side-card`, `.stat-card`, `.figures-band` and the three
18
+ * page islands. Presentation only; the caller owns the content and the action.
19
+ */
20
+ export function PanelCard({ title, description, action, badge, icon, padding = "panel", density = "default", headingLevel = 2, children, className, }) {
21
+ const Heading = `h${headingLevel}`;
22
+ const compact = density === "compact";
23
+ const hasHead = Boolean(title || description || badge || icon || action);
24
+ return (_jsxs("div", { "data-slot": "panel-card", "data-padding": padding, "data-density": density, className: cn("min-w-0 rounded-xl border border-brand-border bg-card text-brand-ink", compact ? compactPaddingClass[padding] : paddingClass[padding], className), children: [hasHead ? (_jsxs("div", { className: cn("flex flex-wrap items-start justify-between gap-x-4 gap-y-3", compact ? "mb-3" : "mb-4"), children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2.5", children: [icon ? (_jsx("span", { "aria-hidden": true, className: "mt-0.5 shrink-0 text-brand-red [&_svg]:size-5", children: icon })) : null, _jsxs("div", { className: "min-w-0", children: [title || badge ? (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [title ? _jsx(Heading, { className: cn("leading-snug font-bold tracking-tight", compact ? "text-sm" : "text-lg"), children: title }) : null, badge] })) : null, description ? _jsx("p", { className: cn("mt-1 text-brand-muted", compact ? "text-xs" : "text-[13px]"), children: description }) : null] })] }), action ? (_jsx("div", { className: "flex shrink-0 flex-wrap items-center gap-2 max-[600px]:[&_[data-slot=button]]:min-h-11", children: action })) : null] })) : null, children] }));
25
+ }
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ export interface PieChartDatum {
3
+ label: string;
4
+ value: number;
5
+ /** Defaults to the token ramp: brand red and its tints, then the neutral greys. */
6
+ color?: string;
7
+ }
8
+ export interface PieChartProps {
9
+ data: PieChartDatum[];
10
+ /** Outer diameter in pixels. Default 168. */
11
+ size?: number;
12
+ donut?: boolean;
13
+ /** Quiet line inside or under the chart, for example "Total". */
14
+ centerLabel?: string;
15
+ /** The figure the centre states, already formatted by the page. */
16
+ centerValue?: string;
17
+ /** `list` is the default legend; `none` leaves the chart alone. */
18
+ legend?: "list" | "none";
19
+ /** `table` adds a screen-reader `<table>` carrying the same numbers. */
20
+ fallback?: "table" | "none";
21
+ /** Slices beyond this collapse into one "Lainnya" slice. Default 5. */
22
+ maxSlices?: number;
23
+ formatValue?: (value: number) => string;
24
+ ariaLabel?: string;
25
+ className?: string;
26
+ }
27
+ /**
28
+ * A pie or donut chart drawn by hand: one SVG path per slice, no dependency.
29
+ * The owner's two charts for Statistik (Jenis kelamin and Anggota Biasa vs Luar
30
+ * Biasa) are this component. Every slice is named in the legend beside its
31
+ * count and its percentage, so colour is never the only carrier, and
32
+ * `fallback="table"` puts the same numbers in a table for a screen reader.
33
+ */
34
+ export declare function PieChart({ data, size, donut, centerLabel, centerValue, legend, fallback, maxSlices, formatValue, ariaLabel, className, }: PieChartProps): React.JSX.Element;