ppij-design-system 0.0.0-stage → 0.2.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 (151) hide show
  1. package/dist/banners.d.ts +31 -0
  2. package/dist/banners.js +18 -0
  3. package/dist/card-themes.d.ts +14 -0
  4. package/dist/card-themes.js +325 -0
  5. package/dist/components/account-menu.d.ts +24 -0
  6. package/dist/components/account-menu.js +13 -0
  7. package/dist/components/admin-drawer.d.ts +34 -0
  8. package/dist/components/admin-drawer.js +30 -0
  9. package/dist/components/alert-dialog.d.ts +16 -0
  10. package/dist/components/alert-dialog.js +39 -0
  11. package/dist/components/alert.d.ts +11 -0
  12. package/dist/components/alert.js +29 -0
  13. package/dist/components/avatar.d.ts +11 -0
  14. package/dist/components/avatar.js +22 -0
  15. package/dist/components/badge.d.ts +9 -0
  16. package/dist/components/badge.js +27 -0
  17. package/dist/components/banner-carousel.d.ts +14 -0
  18. package/dist/components/banner-carousel.js +90 -0
  19. package/dist/components/bottom-bar.d.ts +22 -0
  20. package/dist/components/bottom-bar.js +12 -0
  21. package/dist/components/button.d.ts +10 -0
  22. package/dist/components/button.js +37 -0
  23. package/dist/components/card-upload.d.ts +30 -0
  24. package/dist/components/card-upload.js +19 -0
  25. package/dist/components/card.d.ts +9 -0
  26. package/dist/components/card.js +24 -0
  27. package/dist/components/checkbox.d.ts +4 -0
  28. package/dist/components/checkbox.js +9 -0
  29. package/dist/components/data-table.d.ts +26 -0
  30. package/dist/components/data-table.js +17 -0
  31. package/dist/components/dialog.d.ts +17 -0
  32. package/dist/components/dialog.js +37 -0
  33. package/dist/components/drawer.d.ts +13 -0
  34. package/dist/components/drawer.js +34 -0
  35. package/dist/components/dropdown-menu.d.ts +25 -0
  36. package/dist/components/dropdown-menu.js +50 -0
  37. package/dist/components/editable-panel.d.ts +25 -0
  38. package/dist/components/editable-panel.js +18 -0
  39. package/dist/components/empty.d.ts +12 -0
  40. package/dist/components/empty.js +33 -0
  41. package/dist/components/file-upload.d.ts +28 -0
  42. package/dist/components/file-upload.js +21 -0
  43. package/dist/components/form-panel.d.ts +15 -0
  44. package/dist/components/form-panel.js +9 -0
  45. package/dist/components/funding-picker.d.ts +20 -0
  46. package/dist/components/funding-picker.js +16 -0
  47. package/dist/components/input.d.ts +3 -0
  48. package/dist/components/input.js +6 -0
  49. package/dist/components/invitation-card.d.ts +14 -0
  50. package/dist/components/invitation-card.js +13 -0
  51. package/dist/components/label.d.ts +4 -0
  52. package/dist/components/label.js +7 -0
  53. package/dist/components/member-card-block.d.ts +47 -0
  54. package/dist/components/member-card-block.js +329 -0
  55. package/dist/components/member-card.css +92 -0
  56. package/dist/components/member-card.d.ts +29 -0
  57. package/dist/components/member-card.js +22 -0
  58. package/dist/components/month-picker.d.ts +16 -0
  59. package/dist/components/month-picker.js +19 -0
  60. package/dist/components/offline-banner.d.ts +14 -0
  61. package/dist/components/offline-banner.js +14 -0
  62. package/dist/components/page-header.d.ts +14 -0
  63. package/dist/components/page-header.js +10 -0
  64. package/dist/components/phone-field.d.ts +25 -0
  65. package/dist/components/phone-field.js +64 -0
  66. package/dist/components/pill-radio.d.ts +21 -0
  67. package/dist/components/pill-radio.js +15 -0
  68. package/dist/components/portal-shell.d.ts +53 -0
  69. package/dist/components/portal-shell.js +19 -0
  70. package/dist/components/postcode-field.d.ts +26 -0
  71. package/dist/components/postcode-field.js +14 -0
  72. package/dist/components/progress.d.ts +4 -0
  73. package/dist/components/progress.js +8 -0
  74. package/dist/components/radio-group.d.ts +5 -0
  75. package/dist/components/radio-group.js +11 -0
  76. package/dist/components/review-panel.d.ts +20 -0
  77. package/dist/components/review-panel.js +14 -0
  78. package/dist/components/saving-indicator.d.ts +15 -0
  79. package/dist/components/saving-indicator.js +19 -0
  80. package/dist/components/scroll-area.d.ts +5 -0
  81. package/dist/components/scroll-area.js +13 -0
  82. package/dist/components/search-filter.d.ts +39 -0
  83. package/dist/components/search-filter.js +19 -0
  84. package/dist/components/segmented-progress.d.ts +16 -0
  85. package/dist/components/segmented-progress.js +12 -0
  86. package/dist/components/select.d.ts +15 -0
  87. package/dist/components/select.js +38 -0
  88. package/dist/components/separator.d.ts +4 -0
  89. package/dist/components/separator.js +7 -0
  90. package/dist/components/service-tiles.d.ts +26 -0
  91. package/dist/components/service-tiles.js +17 -0
  92. package/dist/components/sheet.d.ts +14 -0
  93. package/dist/components/sheet.js +40 -0
  94. package/dist/components/sidebar.d.ts +69 -0
  95. package/dist/components/sidebar.js +210 -0
  96. package/dist/components/skeleton.d.ts +11 -0
  97. package/dist/components/skeleton.js +11 -0
  98. package/dist/components/sonner.d.ts +9 -0
  99. package/dist/components/sonner.js +25 -0
  100. package/dist/components/spinner.d.ts +14 -0
  101. package/dist/components/spinner.js +22 -0
  102. package/dist/components/stat-card.d.ts +22 -0
  103. package/dist/components/stat-card.js +12 -0
  104. package/dist/components/status-card.d.ts +24 -0
  105. package/dist/components/status-card.js +26 -0
  106. package/dist/components/stepper.d.ts +25 -0
  107. package/dist/components/stepper.js +23 -0
  108. package/dist/components/submit-overlay.d.ts +16 -0
  109. package/dist/components/submit-overlay.js +15 -0
  110. package/dist/components/suggestion-sheet.d.ts +20 -0
  111. package/dist/components/suggestion-sheet.js +36 -0
  112. package/dist/components/switch.d.ts +6 -0
  113. package/dist/components/switch.js +8 -0
  114. package/dist/components/table.d.ts +10 -0
  115. package/dist/components/table.js +28 -0
  116. package/dist/components/tabs.d.ts +11 -0
  117. package/dist/components/tabs.js +28 -0
  118. package/dist/components/tag-input.d.ts +15 -0
  119. package/dist/components/tag-input.js +31 -0
  120. package/dist/components/textarea.d.ts +3 -0
  121. package/dist/components/textarea.js +6 -0
  122. package/dist/components/timeline.d.ts +18 -0
  123. package/dist/components/timeline.js +11 -0
  124. package/dist/components/todo-strip.d.ts +14 -0
  125. package/dist/components/todo-strip.js +11 -0
  126. package/dist/components/tooltip.d.ts +7 -0
  127. package/dist/components/tooltip.js +16 -0
  128. package/dist/components/top-bar.d.ts +23 -0
  129. package/dist/components/top-bar.js +14 -0
  130. package/dist/components/type-ahead.d.ts +20 -0
  131. package/dist/components/type-ahead.js +22 -0
  132. package/dist/components/unit-card.d.ts +26 -0
  133. package/dist/components/unit-card.js +18 -0
  134. package/dist/components/view-toggle.d.ts +20 -0
  135. package/dist/components/view-toggle.js +15 -0
  136. package/dist/components/wizard-footer.d.ts +24 -0
  137. package/dist/components/wizard-footer.js +17 -0
  138. package/dist/components/wizard-header.d.ts +29 -0
  139. package/dist/components/wizard-header.js +18 -0
  140. package/dist/hooks/use-mobile.d.ts +1 -0
  141. package/dist/hooks/use-mobile.js +11 -0
  142. package/dist/index.d.ts +70 -0
  143. package/dist/index.js +70 -0
  144. package/dist/lib/utils.d.ts +2 -0
  145. package/dist/lib/utils.js +5 -0
  146. package/dist/sample-banners.d.ts +8 -0
  147. package/dist/sample-banners.js +28 -0
  148. package/dist/tokens.css +119 -0
  149. package/dist/tokens.json +50 -0
  150. package/package.json +60 -4
  151. package/README.md +0 -3
@@ -0,0 +1,27 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cva } from "class-variance-authority";
3
+ import { cn } from "../lib/utils";
4
+ import { Slot } from "radix-ui";
5
+ const badgeVariants = cva("inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] 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 [&>svg]:pointer-events-none [&>svg]:size-3", {
6
+ variants: {
7
+ variant: {
8
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
9
+ secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
10
+ destructive: "bg-destructive text-white focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40 [a&]:hover:bg-destructive/90",
11
+ success: "bg-success text-success-foreground [a&]:hover:bg-success/90",
12
+ warning: "bg-warning text-warning-foreground [a&]:hover:bg-warning/90",
13
+ info: "bg-info text-info-foreground [a&]:hover:bg-info/90",
14
+ outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
15
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
16
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
17
+ },
18
+ },
19
+ defaultVariants: {
20
+ variant: "default",
21
+ },
22
+ });
23
+ function Badge({ className, variant = "default", asChild = false, ...props }) {
24
+ const Comp = asChild ? Slot.Root : "span";
25
+ return (_jsx(Comp, { "data-slot": "badge", "data-variant": variant, className: cn(badgeVariants({ variant }), className), ...props }));
26
+ }
27
+ export { Badge, badgeVariants };
@@ -0,0 +1,14 @@
1
+ import { type PromoBanner } from "../banners";
2
+ export interface BannerCarouselProps {
3
+ banners: PromoBanner[];
4
+ locale?: "id" | "en";
5
+ }
6
+ /**
7
+ * Rolling promo banner. The 970 x 250 image shows from 768 px wide,
8
+ * the 320 x 100 image below. Rotates every 6 s; pauses on hover,
9
+ * focus, touch and the pause button; never rotates under reduced
10
+ * motion. One banner shows no controls; no active banner renders
11
+ * nothing. Entries are filtered by JST date in the browser, so an
12
+ * expired promotion drops out without a deploy.
13
+ */
14
+ export declare function BannerCarousel({ banners, locale }: BannerCarouselProps): import("react").JSX.Element | null;
@@ -0,0 +1,90 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { IconChevronLeft, IconChevronRight, IconPlayerPause, IconPlayerPlay } from "@tabler/icons-react";
4
+ import { activeBanners, jstToday } from "../banners";
5
+ // Promo banner, ported exactly from ppij-identity-sso
6
+ // portal/BannerCarousel.tsx (portal-promo-banner,
7
+ // revamp-portal-branding 1.2). Image slides follow the banners.json
8
+ // contract in ../banners.ts.
9
+ const INTERVAL_MS = 6000;
10
+ const SWIPE_PX = 40;
11
+ const LABELS = {
12
+ id: {
13
+ label: "Pengumuman",
14
+ prev: "Pengumuman sebelumnya",
15
+ next: "Pengumuman berikutnya",
16
+ goto: "Tampilkan pengumuman {n}",
17
+ pause: "Jeda",
18
+ play: "Putar",
19
+ slide: "{n} dari {total}",
20
+ },
21
+ en: {
22
+ label: "Announcements",
23
+ prev: "Previous announcement",
24
+ next: "Next announcement",
25
+ goto: "Show announcement {n}",
26
+ pause: "Pause",
27
+ play: "Play",
28
+ slide: "{n} of {total}",
29
+ },
30
+ };
31
+ function useReducedMotion() {
32
+ const [reduced, setReduced] = useState(() => typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
33
+ useEffect(() => {
34
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
35
+ const onChange = () => setReduced(mq.matches);
36
+ mq.addEventListener("change", onChange);
37
+ return () => mq.removeEventListener("change", onChange);
38
+ }, []);
39
+ return reduced;
40
+ }
41
+ /**
42
+ * Rolling promo banner. The 970 x 250 image shows from 768 px wide,
43
+ * the 320 x 100 image below. Rotates every 6 s; pauses on hover,
44
+ * focus, touch and the pause button; never rotates under reduced
45
+ * motion. One banner shows no controls; no active banner renders
46
+ * nothing. Entries are filtered by JST date in the browser, so an
47
+ * expired promotion drops out without a deploy.
48
+ */
49
+ export function BannerCarousel({ banners, locale = "id" }) {
50
+ const slides = useMemo(() => activeBanners(banners, jstToday()), [banners]);
51
+ const [index, setIndex] = useState(0);
52
+ const [hovered, setHovered] = useState(false);
53
+ const [focused, setFocused] = useState(false);
54
+ const [touching, setTouching] = useState(false);
55
+ const [manualPause, setManualPause] = useState(false);
56
+ const reduceMotion = useReducedMotion();
57
+ const touchX = useRef(null);
58
+ const count = slides.length;
59
+ const paused = manualPause || hovered || focused || touching || reduceMotion;
60
+ const current = Math.min(index, Math.max(count - 1, 0));
61
+ useEffect(() => {
62
+ if (paused || count < 2)
63
+ return;
64
+ const id = window.setInterval(() => setIndex((i) => (i + 1) % count), INTERVAL_MS);
65
+ return () => window.clearInterval(id);
66
+ }, [paused, count]);
67
+ if (count === 0)
68
+ return null;
69
+ const go = (next) => setIndex((next + count) % count);
70
+ const label = LABELS[locale].label;
71
+ const altOf = (b) => (locale === "en" ? b.alt.en : b.alt.id);
72
+ return (_jsxs("section", { "aria-roledescription": "carousel", "aria-label": label, className: "group relative aspect-[32/10] overflow-hidden rounded-xl bg-brand-ink md:aspect-[97/25]", onMouseEnter: () => setHovered(true), onMouseLeave: () => setHovered(false), onFocus: () => setFocused(true), onBlur: (e) => {
73
+ if (!e.currentTarget.contains(e.relatedTarget))
74
+ setFocused(false);
75
+ }, onTouchStart: (e) => {
76
+ touchX.current = e.touches[0].clientX;
77
+ setTouching(true);
78
+ }, onTouchEnd: (e) => {
79
+ setTouching(false);
80
+ if (touchX.current == null)
81
+ return;
82
+ const dx = e.changedTouches[0].clientX - touchX.current;
83
+ touchX.current = null;
84
+ if (Math.abs(dx) > SWIPE_PX)
85
+ go(current + (dx < 0 ? 1 : -1));
86
+ }, children: [_jsx("div", { className: "absolute inset-0", "aria-live": paused ? "polite" : "off", children: slides.map((b, k) => {
87
+ const shown = k === current;
88
+ return (_jsx("div", { role: "group", "aria-roledescription": "slide", "aria-label": LABELS[locale].slide.replace("{n}", String(k + 1)).replace("{total}", String(count)), "aria-hidden": !shown, inert: !shown, className: `absolute inset-0 transition-opacity duration-500 motion-reduce:transition-none ${shown ? "opacity-100" : "pointer-events-none opacity-0"}`, children: _jsx("a", { href: b.href, target: b.href.startsWith("https://") ? "_blank" : undefined, rel: b.href.startsWith("https://") ? "noopener noreferrer" : undefined, className: "block h-full w-full focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-white", children: _jsxs("picture", { className: "block h-full w-full", children: [_jsx("source", { media: "(min-width: 768px)", srcSet: b.image.desktop, width: 970, height: 250 }), _jsx("img", { src: b.image.mobile, alt: altOf(b), width: 320, height: 100, loading: k === 0 ? "eager" : "lazy", className: "block h-full w-full object-cover" })] }) }) }, b.id));
89
+ }) }), count > 1 ? (_jsxs(_Fragment, { children: [_jsxs("div", { role: "group", "aria-label": label, className: "absolute right-1.5 bottom-1 flex items-center rounded-full bg-brand-ink/60 px-1", children: [_jsx("button", { type: "button", "aria-label": manualPause ? LABELS[locale].play : LABELS[locale].pause, "aria-pressed": manualPause, onClick: () => setManualPause((v) => !v), className: "flex size-7 items-center justify-center rounded-full text-white focus-visible:outline-2 focus-visible:outline-white", children: manualPause ? _jsx(IconPlayerPlay, { className: "size-3.5", "aria-hidden": "true" }) : _jsx(IconPlayerPause, { className: "size-3.5", "aria-hidden": "true" }) }), slides.map((b, k) => (_jsx("button", { type: "button", "aria-label": LABELS[locale].goto.replace("{n}", String(k + 1)), "aria-current": k === current ? "true" : undefined, onClick: () => go(k), className: "flex size-7 items-center justify-center rounded-full focus-visible:outline-2 focus-visible:outline-white", children: _jsx("span", { className: `block size-1.5 rounded-full ${k === current ? "bg-white" : "bg-white/55"}` }) }, b.id)))] }), _jsx("button", { type: "button", "aria-label": LABELS[locale].prev, onClick: () => go(current - 1), className: "absolute top-1/2 left-2 hidden size-10 -translate-y-1/2 items-center justify-center rounded-full bg-brand-ink/55 text-white opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-white md:flex", children: _jsx(IconChevronLeft, { className: "size-5", "aria-hidden": "true" }) }), _jsx("button", { type: "button", "aria-label": LABELS[locale].next, onClick: () => go(current + 1), className: "absolute top-1/2 right-2 hidden size-10 -translate-y-1/2 items-center justify-center rounded-full bg-brand-ink/55 text-white opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 focus-visible:opacity-100 focus-visible:outline-2 focus-visible:outline-white md:flex", children: _jsx(IconChevronRight, { className: "size-5", "aria-hidden": "true" }) })] })) : null] }));
90
+ }
@@ -0,0 +1,22 @@
1
+ import * as React from "react";
2
+ export interface BottomBarItem {
3
+ label: string;
4
+ href: string;
5
+ icon: React.ReactNode;
6
+ current?: boolean;
7
+ /** Live count shown as a small badge; zero or undefined shows nothing. */
8
+ badge?: number;
9
+ }
10
+ export interface BottomBarProps {
11
+ items: BottomBarItem[];
12
+ ariaLabel?: string;
13
+ className?: string;
14
+ }
15
+ /**
16
+ * Phone bottom navigation for the portal and admin on small screens.
17
+ * The sidebar pattern covers desktop and tablet; below that, the four
18
+ * or five destinations a member reaches daily live here, thumb close.
19
+ * The bar itself is a plain nav element: the application shell pins it
20
+ * to the bottom of the viewport and pads page content clear of it.
21
+ */
22
+ export declare function BottomBar({ items, ariaLabel, className }: BottomBarProps): React.JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils";
3
+ /**
4
+ * Phone bottom navigation for the portal and admin on small screens.
5
+ * The sidebar pattern covers desktop and tablet; below that, the four
6
+ * or five destinations a member reaches daily live here, thumb close.
7
+ * The bar itself is a plain nav element: the application shell pins it
8
+ * to the bottom of the viewport and pads page content clear of it.
9
+ */
10
+ export function BottomBar({ items, ariaLabel = "Navigasi utama", className }) {
11
+ return (_jsx("nav", { "data-slot": "bottom-bar", "aria-label": ariaLabel, className: cn("w-full border-t border-brand-border bg-white pb-[env(safe-area-inset-bottom)]", className), children: _jsx("div", { className: "grid", style: { gridTemplateColumns: `repeat(${items.length}, minmax(0, 1fr))` }, children: items.map((item) => (_jsxs("a", { href: item.href, "aria-current": item.current ? "page" : undefined, className: cn("relative flex min-h-14 flex-col items-center justify-center gap-0.5 px-1 text-[11px] no-underline", item.current ? "font-semibold text-brand-red" : "text-brand-muted hover:text-brand-ink"), children: [_jsxs("span", { className: "relative", children: [item.icon, item.badge ? (_jsx("span", { className: "absolute -top-1.5 -right-2.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-brand-red px-1 text-[9px] font-bold text-white", children: item.badge })) : null] }), _jsx("span", { className: "max-w-full truncate", children: item.label })] }, item.label + item.href))) }) }));
12
+ }
@@ -0,0 +1,10 @@
1
+ import * as React from "react";
2
+ import { type VariantProps } from "class-variance-authority";
3
+ declare const buttonVariants: (props?: ({
4
+ variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "success" | "warning" | null | undefined;
5
+ size?: "default" | "xs" | "sm" | "lg" | "icon" | "icon-xs" | "icon-sm" | "icon-lg" | null | undefined;
6
+ } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
+ declare function Button({ className, variant, size, asChild, ...props }: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
8
+ asChild?: boolean;
9
+ }): React.JSX.Element;
10
+ export { Button, buttonVariants };
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cva } from "class-variance-authority";
3
+ import { cn } from "../lib/utils";
4
+ import { Slot } from "radix-ui";
5
+ const buttonVariants = cva("inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", {
6
+ variants: {
7
+ variant: {
8
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
9
+ destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
10
+ outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
11
+ secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
12
+ ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
13
+ link: "text-primary underline-offset-4 hover:underline",
14
+ success: "bg-success text-success-foreground hover:bg-success/90 focus-visible:ring-success/20",
15
+ warning: "bg-warning text-warning-foreground hover:bg-warning/90 focus-visible:ring-warning/20",
16
+ },
17
+ size: {
18
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
19
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
20
+ sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
21
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
22
+ icon: "size-9",
23
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
24
+ "icon-sm": "size-8",
25
+ "icon-lg": "size-10",
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: "default",
30
+ size: "default",
31
+ },
32
+ });
33
+ function Button({ className, variant = "default", size = "default", asChild = false, ...props }) {
34
+ const Comp = asChild ? Slot.Root : "button";
35
+ return (_jsx(Comp, { "data-slot": "button", "data-variant": variant, "data-size": size, className: cn(buttonVariants({ variant, size, className })), ...props }));
36
+ }
37
+ export { Button, buttonVariants };
@@ -0,0 +1,30 @@
1
+ export type CardSideState = "empty" | "processing" | "uploading" | "failed" | "saved";
2
+ export interface CardSide {
3
+ label: string;
4
+ optional?: boolean;
5
+ state: CardSideState;
6
+ /** Image preview URL when saved (or a PDF file name with isPdf). */
7
+ previewSrc?: string;
8
+ fileName?: string;
9
+ isPdf?: boolean;
10
+ error?: string;
11
+ onCapture?: () => void;
12
+ onPick?: () => void;
13
+ onRetry?: () => void;
14
+ onRemove?: () => void;
15
+ }
16
+ export interface CardUploadProps {
17
+ front: CardSide;
18
+ back?: CardSide;
19
+ /** Privacy line under the slots, with the shield icon. */
20
+ note?: string;
21
+ className?: string;
22
+ }
23
+ /**
24
+ * Student card capture, extracted from the wizard CardStep: two
25
+ * sides (front required, back optional), each with its own state
26
+ * (empty, processing, uploading, failed, saved with preview). The
27
+ * image preparation and the upload stay in the application; this
28
+ * is the presentation of both slots and their states.
29
+ */
30
+ export declare function CardUpload({ front, back, note, className }: CardUploadProps): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { IconCamera, IconCheck, IconFileTypePdf, IconPhoto, IconRefresh, IconShieldCheck, IconTrash } from "@tabler/icons-react";
3
+ import { cn } from "../lib/utils";
4
+ import { Button } from "./button";
5
+ import { Spinner } from "./spinner";
6
+ function SideSlot({ side }) {
7
+ const busy = side.state === "processing" || side.state === "uploading";
8
+ return (_jsxs("section", { "aria-label": side.label, className: "flex flex-col gap-2", children: [_jsxs("h3", { className: "text-base font-semibold text-brand-ink", children: [side.label, " ", side.optional ? _jsx("span", { className: "font-normal text-brand-muted", children: "(opsional)" }) : null] }), side.state === "empty" && (_jsxs("div", { className: "grid gap-3 rounded-md border border-dashed border-brand-border bg-card p-4", children: [_jsx("p", { className: "text-sm text-brand-muted", children: "Belum ada foto untuk sisi ini." }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [side.onCapture && (_jsxs(Button, { type: "button", variant: "outline", onClick: side.onCapture, children: [_jsx(IconCamera, { className: "size-4", "aria-hidden": "true" }), "Ambil foto"] })), side.onPick && (_jsxs(Button, { type: "button", variant: "outline", onClick: side.onPick, children: [_jsx(IconPhoto, { className: "size-4", "aria-hidden": "true" }), "Pilih berkas"] }))] })] })), busy && (_jsxs("div", { className: "flex h-48 items-center justify-center gap-2 rounded-md border border-brand-border bg-card text-sm font-medium text-brand-ink", role: "status", children: [_jsx(Spinner, { size: "sm", decorative: true }), side.state === "processing" ? "Memproses foto..." : "Mengunggah..."] })), side.state === "saved" && (_jsxs("div", { className: "flex flex-col gap-2", children: [side.isPdf ? (_jsxs("div", { className: "flex h-48 items-center justify-center gap-3 rounded-md border border-brand-border bg-card p-4 text-sm font-medium text-brand-ink", children: [_jsx(IconFileTypePdf, { className: "size-6 text-brand-red", "aria-hidden": "true" }), side.fileName ?? "dokumen.pdf"] })) : (_jsx("img", { src: side.previewSrc, alt: `Pratinjau ${side.label.toLowerCase()}`, className: "h-48 w-full rounded-md border border-brand-border object-cover" })), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs("span", { className: "flex items-center gap-1.5 text-sm font-medium text-success", children: [_jsx(IconCheck, { className: "size-4", "aria-hidden": "true" }), "Tersimpan"] }), _jsx("span", { className: "flex-1" }), side.onRemove && (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: side.onRemove, children: [_jsx(IconTrash, { className: "size-4", "aria-hidden": "true" }), "Hapus"] }))] })] })), side.state === "failed" && (_jsxs("div", { className: "grid gap-2 rounded-md border border-destructive/50 bg-card p-4", children: [_jsx("p", { role: "alert", className: "text-sm text-destructive", children: side.error ?? "Unggahan gagal. Periksa koneksi lalu coba lagi." }), _jsx("div", { children: side.onRetry && (_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: side.onRetry, children: [_jsx(IconRefresh, { className: "size-4", "aria-hidden": "true" }), "Coba lagi"] })) })] }))] }));
9
+ }
10
+ /**
11
+ * Student card capture, extracted from the wizard CardStep: two
12
+ * sides (front required, back optional), each with its own state
13
+ * (empty, processing, uploading, failed, saved with preview). The
14
+ * image preparation and the upload stay in the application; this
15
+ * is the presentation of both slots and their states.
16
+ */
17
+ export function CardUpload({ front, back, note, className }) {
18
+ return (_jsxs("div", { className: cn("grid gap-6", className), children: [_jsx(SideSlot, { side: front }), back && _jsx(SideSlot, { side: back }), note && (_jsxs("p", { className: "flex items-start gap-2 text-sm text-brand-muted", children: [_jsx(IconShieldCheck, { className: "mt-0.5 size-4 shrink-0 text-brand-red", "aria-hidden": "true" }), note] }))] }));
19
+ }
@@ -0,0 +1,9 @@
1
+ import * as React from "react";
2
+ declare function Card({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
3
+ declare function CardHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
4
+ declare function CardTitle({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
5
+ declare function CardDescription({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
6
+ declare function CardAction({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
7
+ declare function CardContent({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
8
+ declare function CardFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
9
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils";
3
+ function Card({ className, ...props }) {
4
+ return (_jsx("div", { "data-slot": "card", className: cn("flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm", className), ...props }));
5
+ }
6
+ function CardHeader({ className, ...props }) {
7
+ return (_jsx("div", { "data-slot": "card-header", className: cn("@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", className), ...props }));
8
+ }
9
+ function CardTitle({ className, ...props }) {
10
+ return (_jsx("div", { "data-slot": "card-title", className: cn("leading-snug font-semibold", className), ...props }));
11
+ }
12
+ function CardDescription({ className, ...props }) {
13
+ return (_jsx("div", { "data-slot": "card-description", className: cn("text-sm text-muted-foreground", className), ...props }));
14
+ }
15
+ function CardAction({ className, ...props }) {
16
+ return (_jsx("div", { "data-slot": "card-action", className: cn("col-start-2 row-span-2 row-start-1 self-start justify-self-end", className), ...props }));
17
+ }
18
+ function CardContent({ className, ...props }) {
19
+ return (_jsx("div", { "data-slot": "card-content", className: cn("px-6", className), ...props }));
20
+ }
21
+ function CardFooter({ className, ...props }) {
22
+ return (_jsx("div", { "data-slot": "card-footer", className: cn("flex items-center px-6 [.border-t]:pt-6", className), ...props }));
23
+ }
24
+ export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent, };
@@ -0,0 +1,4 @@
1
+ import * as React from "react";
2
+ import { Checkbox as CheckboxPrimitive } from "radix-ui";
3
+ declare function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>): React.JSX.Element;
4
+ export { Checkbox };
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Checkbox as CheckboxPrimitive } from "radix-ui";
4
+ import { IconCheck } from "@tabler/icons-react";
5
+ import { cn } from "../lib/utils";
6
+ function Checkbox({ className, ...props }) {
7
+ return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn("peer size-4 shrink-0 rounded-[4px] border border-input bg-card shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "grid place-content-center text-current transition-none", children: _jsx(IconCheck, { className: "size-3.5" }) }) }));
8
+ }
9
+ export { Checkbox };
@@ -0,0 +1,26 @@
1
+ import * as React from "react";
2
+ export interface DataTableProps {
3
+ searchValue?: string;
4
+ onSearchChange?: (value: string) => void;
5
+ searchPlaceholder?: string;
6
+ actions?: React.ReactNode;
7
+ children: React.ReactNode;
8
+ footerText?: string;
9
+ /** Current page, shown only when pagination has an available direction. */
10
+ pageLabel?: string;
11
+ onPrevious?: () => void;
12
+ onNext?: () => void;
13
+ previousDisabled?: boolean;
14
+ nextDisabled?: boolean;
15
+ previousLabel?: string;
16
+ nextLabel?: string;
17
+ className?: string;
18
+ }
19
+ /**
20
+ * Presentation shell for admin data tables: a search and actions
21
+ * toolbar, the table itself as children, and a compact footer.
22
+ * Pagination is omitted when no direction can be used; a one-page table
23
+ * shows its result count without inert controls.
24
+ * Fetching, sorting state, and row logic stay in the application.
25
+ */
26
+ export declare function DataTable({ searchValue, onSearchChange, searchPlaceholder, actions, children, footerText, pageLabel, onPrevious, onNext, previousDisabled, nextDisabled, previousLabel, nextLabel, className, }: DataTableProps): React.JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { IconSearch, IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
3
+ import { cn } from "../lib/utils";
4
+ import { Button } from "./button";
5
+ import { Input } from "./input";
6
+ /**
7
+ * Presentation shell for admin data tables: a search and actions
8
+ * toolbar, the table itself as children, and a compact footer.
9
+ * Pagination is omitted when no direction can be used; a one-page table
10
+ * shows its result count without inert controls.
11
+ * Fetching, sorting state, and row logic stay in the application.
12
+ */
13
+ export function DataTable({ searchValue, onSearchChange, searchPlaceholder = "Cari...", actions, children, footerText, pageLabel, onPrevious, onNext, previousDisabled, nextDisabled, previousLabel = "Sebelumnya", nextLabel = "Berikutnya", className, }) {
14
+ const hasPagination = (onPrevious !== undefined || onNext !== undefined) && !(previousDisabled && nextDisabled);
15
+ const hasFooter = Boolean(footerText) || hasPagination;
16
+ return (_jsxs("div", { className: cn("overflow-hidden rounded-xl border border-brand-border bg-card", className), children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2 border-b border-brand-border p-3", children: [onSearchChange ? (_jsxs("div", { className: "relative min-w-0 flex-1 sm:max-w-xs", children: [_jsx(IconSearch, { size: 16, className: "pointer-events-none absolute top-1/2 left-2.5 -translate-y-1/2 text-brand-muted" }), _jsx(Input, { value: searchValue ?? "", onChange: (e) => onSearchChange(e.target.value), placeholder: searchPlaceholder, className: "pl-8", "aria-label": searchPlaceholder })] })) : null, actions ? _jsx("div", { className: "ml-auto flex flex-wrap items-center gap-2", children: actions }) : null] }), _jsx("div", { className: "overflow-x-auto", children: children }), hasFooter ? (_jsxs("div", { className: "flex min-h-10 flex-wrap items-center justify-between gap-2 border-t border-brand-border px-3 py-1.5", children: [footerText ? _jsx("p", { className: "text-xs text-brand-muted", children: footerText }) : null, hasPagination ? (_jsxs("nav", { "aria-label": "Halaman tabel", className: "ml-auto flex items-center gap-1", children: [pageLabel ? _jsx("span", { className: cn("mr-1 text-xs text-brand-muted", footerText && "sr-only sm:not-sr-only"), children: pageLabel }) : null, _jsx(Button, { size: "icon-sm", className: "size-11 sm:size-8", variant: "outline", onClick: onPrevious, disabled: previousDisabled || !onPrevious, "aria-label": previousLabel, title: previousLabel, children: _jsx(IconChevronLeft, { size: 16 }) }), _jsx(Button, { size: "icon-sm", className: "size-11 sm:size-8", variant: "outline", onClick: onNext, disabled: nextDisabled || !onNext, "aria-label": nextLabel, title: nextLabel, children: _jsx(IconChevronRight, { size: 16 }) })] })) : null] })) : null] }));
17
+ }
@@ -0,0 +1,17 @@
1
+ import * as React from "react";
2
+ import { Dialog as DialogPrimitive } from "radix-ui";
3
+ declare function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>): React.JSX.Element;
4
+ declare function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>): React.JSX.Element;
5
+ declare function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>): React.JSX.Element;
6
+ declare function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>): React.JSX.Element;
7
+ declare function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>): React.JSX.Element;
8
+ declare function DialogContent({ className, children, showCloseButton, ...props }: React.ComponentProps<typeof DialogPrimitive.Content> & {
9
+ showCloseButton?: boolean;
10
+ }): React.JSX.Element;
11
+ declare function DialogHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
12
+ declare function DialogFooter({ className, showCloseButton, children, ...props }: React.ComponentProps<"div"> & {
13
+ showCloseButton?: boolean;
14
+ }): React.JSX.Element;
15
+ declare function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>): React.JSX.Element;
16
+ declare function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>): React.JSX.Element;
17
+ export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, };
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Dialog as DialogPrimitive } from "radix-ui";
4
+ import { IconX } from "@tabler/icons-react";
5
+ import { cn } from "../lib/utils";
6
+ import { Button } from "./button";
7
+ function Dialog({ ...props }) {
8
+ return _jsx(DialogPrimitive.Root, { "data-slot": "dialog", ...props });
9
+ }
10
+ function DialogTrigger({ ...props }) {
11
+ return _jsx(DialogPrimitive.Trigger, { "data-slot": "dialog-trigger", ...props });
12
+ }
13
+ function DialogPortal({ ...props }) {
14
+ return _jsx(DialogPrimitive.Portal, { "data-slot": "dialog-portal", ...props });
15
+ }
16
+ function DialogClose({ ...props }) {
17
+ return _jsx(DialogPrimitive.Close, { "data-slot": "dialog-close", ...props });
18
+ }
19
+ function DialogOverlay({ className, ...props }) {
20
+ return (_jsx(DialogPrimitive.Overlay, { "data-slot": "dialog-overlay", className: cn("fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", className), ...props }));
21
+ }
22
+ function DialogContent({ className, children, showCloseButton = true, ...props }) {
23
+ return (_jsxs(DialogPortal, { "data-slot": "dialog-portal", children: [_jsx(DialogOverlay, {}), _jsxs(DialogPrimitive.Content, { "data-slot": "dialog-content", className: cn("fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg", className), ...props, children: [children, showCloseButton && (_jsxs(DialogPrimitive.Close, { "data-slot": "dialog-close", className: "absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity outline-none hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", children: [_jsx(IconX, {}), _jsx("span", { className: "sr-only", children: "Tutup" })] }))] })] }));
24
+ }
25
+ function DialogHeader({ className, ...props }) {
26
+ return _jsx("div", { "data-slot": "dialog-header", className: cn("flex flex-col gap-2 text-center sm:text-left", className), ...props });
27
+ }
28
+ function DialogFooter({ className, showCloseButton = false, children, ...props }) {
29
+ return (_jsxs("div", { "data-slot": "dialog-footer", className: cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className), ...props, children: [children, showCloseButton && (_jsx(DialogPrimitive.Close, { asChild: true, children: _jsx(Button, { variant: "outline", children: "Tutup" }) }))] }));
30
+ }
31
+ function DialogTitle({ className, ...props }) {
32
+ return _jsx(DialogPrimitive.Title, { "data-slot": "dialog-title", className: cn("text-lg leading-none font-semibold", className), ...props });
33
+ }
34
+ function DialogDescription({ className, ...props }) {
35
+ return _jsx(DialogPrimitive.Description, { "data-slot": "dialog-description", className: cn("text-sm text-muted-foreground", className), ...props });
36
+ }
37
+ export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, };
@@ -0,0 +1,13 @@
1
+ import * as React from "react";
2
+ import { Drawer as DrawerPrimitive } from "vaul";
3
+ declare function Drawer({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Root>): React.JSX.Element;
4
+ declare function DrawerTrigger({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Trigger>): React.JSX.Element;
5
+ declare function DrawerPortal({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Portal>): React.JSX.Element;
6
+ declare function DrawerClose({ ...props }: React.ComponentProps<typeof DrawerPrimitive.Close>): React.JSX.Element;
7
+ declare function DrawerOverlay({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Overlay>): React.JSX.Element;
8
+ declare function DrawerContent({ className, children, ...props }: React.ComponentProps<typeof DrawerPrimitive.Content>): React.JSX.Element;
9
+ declare function DrawerHeader({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
10
+ declare function DrawerFooter({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
11
+ declare function DrawerTitle({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Title>): React.JSX.Element;
12
+ declare function DrawerDescription({ className, ...props }: React.ComponentProps<typeof DrawerPrimitive.Description>): React.JSX.Element;
13
+ export { Drawer, DrawerPortal, DrawerOverlay, DrawerTrigger, DrawerClose, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription, };
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils";
3
+ import { Drawer as DrawerPrimitive } from "vaul";
4
+ function Drawer({ ...props }) {
5
+ return _jsx(DrawerPrimitive.Root, { "data-slot": "drawer", ...props });
6
+ }
7
+ function DrawerTrigger({ ...props }) {
8
+ return _jsx(DrawerPrimitive.Trigger, { "data-slot": "drawer-trigger", ...props });
9
+ }
10
+ function DrawerPortal({ ...props }) {
11
+ return _jsx(DrawerPrimitive.Portal, { "data-slot": "drawer-portal", ...props });
12
+ }
13
+ function DrawerClose({ ...props }) {
14
+ return _jsx(DrawerPrimitive.Close, { "data-slot": "drawer-close", ...props });
15
+ }
16
+ function DrawerOverlay({ className, ...props }) {
17
+ return (_jsx(DrawerPrimitive.Overlay, { "data-slot": "drawer-overlay", className: cn("fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0", className), ...props }));
18
+ }
19
+ function DrawerContent({ className, children, ...props }) {
20
+ return (_jsxs(DrawerPortal, { "data-slot": "drawer-portal", children: [_jsx(DrawerOverlay, {}), _jsxs(DrawerPrimitive.Content, { "data-slot": "drawer-content", className: cn("group/drawer-content fixed z-50 flex h-auto flex-col bg-background", "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b", "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t", "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm", "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm", className), ...props, children: [_jsx("div", { className: "mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full bg-muted group-data-[vaul-drawer-direction=bottom]/drawer-content:block" }), children] })] }));
21
+ }
22
+ function DrawerHeader({ className, ...props }) {
23
+ return (_jsx("div", { "data-slot": "drawer-header", className: cn("flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left", className), ...props }));
24
+ }
25
+ function DrawerFooter({ className, ...props }) {
26
+ return (_jsx("div", { "data-slot": "drawer-footer", className: cn("mt-auto flex flex-col gap-2 p-4", className), ...props }));
27
+ }
28
+ function DrawerTitle({ className, ...props }) {
29
+ return (_jsx(DrawerPrimitive.Title, { "data-slot": "drawer-title", className: cn("font-semibold text-foreground", className), ...props }));
30
+ }
31
+ function DrawerDescription({ className, ...props }) {
32
+ return (_jsx(DrawerPrimitive.Description, { "data-slot": "drawer-description", className: cn("text-sm text-muted-foreground", className), ...props }));
33
+ }
34
+ export { Drawer, DrawerPortal, DrawerOverlay, DrawerTrigger, DrawerClose, DrawerContent, DrawerHeader, DrawerFooter, DrawerTitle, DrawerDescription, };
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
3
+ declare function DropdownMenu({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>): React.JSX.Element;
4
+ declare function DropdownMenuPortal({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>): React.JSX.Element;
5
+ declare function DropdownMenuTrigger({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>): React.JSX.Element;
6
+ declare function DropdownMenuContent({ className, sideOffset, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>): React.JSX.Element;
7
+ declare function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>): React.JSX.Element;
8
+ declare function DropdownMenuItem({ className, inset, variant, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
9
+ inset?: boolean;
10
+ variant?: "default" | "destructive";
11
+ }): React.JSX.Element;
12
+ declare function DropdownMenuCheckboxItem({ className, children, checked, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>): React.JSX.Element;
13
+ declare function DropdownMenuRadioGroup({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>): React.JSX.Element;
14
+ declare function DropdownMenuRadioItem({ className, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>): React.JSX.Element;
15
+ declare function DropdownMenuLabel({ className, inset, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
16
+ inset?: boolean;
17
+ }): React.JSX.Element;
18
+ declare function DropdownMenuSeparator({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>): React.JSX.Element;
19
+ declare function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">): React.JSX.Element;
20
+ declare function DropdownMenuSub({ ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>): React.JSX.Element;
21
+ declare function DropdownMenuSubTrigger({ className, inset, children, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
22
+ inset?: boolean;
23
+ }): React.JSX.Element;
24
+ declare function DropdownMenuSubContent({ className, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>): React.JSX.Element;
25
+ export { DropdownMenu, DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, };
@@ -0,0 +1,50 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { cn } from "../lib/utils";
3
+ import { IconCheck, IconChevronRight, IconCircleFilled } from "@tabler/icons-react";
4
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
5
+ function DropdownMenu({ ...props }) {
6
+ return _jsx(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
7
+ }
8
+ function DropdownMenuPortal({ ...props }) {
9
+ return (_jsx(DropdownMenuPrimitive.Portal, { "data-slot": "dropdown-menu-portal", ...props }));
10
+ }
11
+ function DropdownMenuTrigger({ ...props }) {
12
+ return (_jsx(DropdownMenuPrimitive.Trigger, { "data-slot": "dropdown-menu-trigger", ...props }));
13
+ }
14
+ function DropdownMenuContent({ className, sideOffset = 4, ...props }) {
15
+ return (_jsx(DropdownMenuPrimitive.Portal, { children: _jsx(DropdownMenuPrimitive.Content, { "data-slot": "dropdown-menu-content", sideOffset: sideOffset, className: cn("z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className), ...props }) }));
16
+ }
17
+ function DropdownMenuGroup({ ...props }) {
18
+ return (_jsx(DropdownMenuPrimitive.Group, { "data-slot": "dropdown-menu-group", ...props }));
19
+ }
20
+ function DropdownMenuItem({ className, inset, variant = "default", ...props }) {
21
+ return (_jsx(DropdownMenuPrimitive.Item, { "data-slot": "dropdown-menu-item", "data-inset": inset, "data-variant": variant, className: cn("relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!", className), ...props }));
22
+ }
23
+ function DropdownMenuCheckboxItem({ className, children, checked, ...props }) {
24
+ return (_jsxs(DropdownMenuPrimitive.CheckboxItem, { "data-slot": "dropdown-menu-checkbox-item", className: cn("relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), checked: checked, ...props, children: [_jsx("span", { className: "pointer-events-none absolute left-2 flex size-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(IconCheck, { className: "size-4" }) }) }), children] }));
25
+ }
26
+ function DropdownMenuRadioGroup({ ...props }) {
27
+ return (_jsx(DropdownMenuPrimitive.RadioGroup, { "data-slot": "dropdown-menu-radio-group", ...props }));
28
+ }
29
+ function DropdownMenuRadioItem({ className, children, ...props }) {
30
+ return (_jsxs(DropdownMenuPrimitive.RadioItem, { "data-slot": "dropdown-menu-radio-item", className: cn("relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [_jsx("span", { className: "pointer-events-none absolute left-2 flex size-3.5 items-center justify-center", children: _jsx(DropdownMenuPrimitive.ItemIndicator, { children: _jsx(IconCircleFilled, { className: "size-2 fill-current" }) }) }), children] }));
31
+ }
32
+ function DropdownMenuLabel({ className, inset, ...props }) {
33
+ return (_jsx(DropdownMenuPrimitive.Label, { "data-slot": "dropdown-menu-label", "data-inset": inset, className: cn("px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className), ...props }));
34
+ }
35
+ function DropdownMenuSeparator({ className, ...props }) {
36
+ return (_jsx(DropdownMenuPrimitive.Separator, { "data-slot": "dropdown-menu-separator", className: cn("-mx-1 my-1 h-px bg-border", className), ...props }));
37
+ }
38
+ function DropdownMenuShortcut({ className, ...props }) {
39
+ return (_jsx("span", { "data-slot": "dropdown-menu-shortcut", className: cn("ml-auto text-xs tracking-widest text-muted-foreground", className), ...props }));
40
+ }
41
+ function DropdownMenuSub({ ...props }) {
42
+ return _jsx(DropdownMenuPrimitive.Sub, { "data-slot": "dropdown-menu-sub", ...props });
43
+ }
44
+ function DropdownMenuSubTrigger({ className, inset, children, ...props }) {
45
+ return (_jsxs(DropdownMenuPrimitive.SubTrigger, { "data-slot": "dropdown-menu-sub-trigger", "data-inset": inset, className: cn("flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground", className), ...props, children: [children, _jsx(IconChevronRight, { className: "ml-auto size-4" })] }));
46
+ }
47
+ function DropdownMenuSubContent({ className, ...props }) {
48
+ return (_jsx(DropdownMenuPrimitive.SubContent, { "data-slot": "dropdown-menu-sub-content", className: cn("z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95", className), ...props }));
49
+ }
50
+ export { DropdownMenu, DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, };
@@ -0,0 +1,25 @@
1
+ export interface EditablePanelProps {
2
+ title: string;
3
+ /** One line on how this panel behaves: what updates directly, or what is reviewed. */
4
+ description?: string;
5
+ /** A change proposal for this panel is waiting for review: shows the pending badge. */
6
+ pending?: boolean;
7
+ pendingLabel?: string;
8
+ onEdit: () => void;
9
+ editLabel?: string;
10
+ /** A pending-proposal block (for example an Alert listing the proposed values), shown under the read values. */
11
+ proposal?: React.ReactNode;
12
+ /** The read view: committed values as text. */
13
+ children: React.ReactNode;
14
+ }
15
+ /**
16
+ * The standard editable surface of the portal: a read-first panel.
17
+ * The panel only ever reads: title, one behaviour line, committed
18
+ * values as text, and an Edit action. Editing itself happens in
19
+ * the AdminDrawer (a right drawer on desktop, a bottom sheet on
20
+ * the phone), one panel at a time; the page owns the drawer.
21
+ * Reviewed changes leave a proposal block inside the panel with a
22
+ * Menunggu badge while the current values stay in effect. This is
23
+ * the profile pattern; every editable surface follows it.
24
+ */
25
+ export declare function EditablePanel({ title, description, pending, pendingLabel, onEdit, editLabel, proposal, children }: EditablePanelProps): import("react").JSX.Element;