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.
- package/dist/banners.d.ts +31 -0
- package/dist/banners.js +18 -0
- package/dist/card-themes.d.ts +14 -0
- package/dist/card-themes.js +325 -0
- package/dist/components/account-menu.d.ts +24 -0
- package/dist/components/account-menu.js +13 -0
- package/dist/components/admin-drawer.d.ts +34 -0
- package/dist/components/admin-drawer.js +30 -0
- package/dist/components/alert-dialog.d.ts +16 -0
- package/dist/components/alert-dialog.js +39 -0
- package/dist/components/alert.d.ts +11 -0
- package/dist/components/alert.js +29 -0
- package/dist/components/avatar.d.ts +11 -0
- package/dist/components/avatar.js +22 -0
- package/dist/components/badge.d.ts +9 -0
- package/dist/components/badge.js +27 -0
- package/dist/components/banner-carousel.d.ts +14 -0
- package/dist/components/banner-carousel.js +90 -0
- package/dist/components/bottom-bar.d.ts +22 -0
- package/dist/components/bottom-bar.js +12 -0
- package/dist/components/button.d.ts +10 -0
- package/dist/components/button.js +37 -0
- package/dist/components/card-upload.d.ts +30 -0
- package/dist/components/card-upload.js +19 -0
- package/dist/components/card.d.ts +9 -0
- package/dist/components/card.js +24 -0
- package/dist/components/checkbox.d.ts +4 -0
- package/dist/components/checkbox.js +9 -0
- package/dist/components/data-table.d.ts +26 -0
- package/dist/components/data-table.js +17 -0
- package/dist/components/dialog.d.ts +17 -0
- package/dist/components/dialog.js +37 -0
- package/dist/components/drawer.d.ts +13 -0
- package/dist/components/drawer.js +34 -0
- package/dist/components/dropdown-menu.d.ts +25 -0
- package/dist/components/dropdown-menu.js +50 -0
- package/dist/components/editable-panel.d.ts +25 -0
- package/dist/components/editable-panel.js +18 -0
- package/dist/components/empty.d.ts +12 -0
- package/dist/components/empty.js +33 -0
- package/dist/components/file-upload.d.ts +28 -0
- package/dist/components/file-upload.js +21 -0
- package/dist/components/form-panel.d.ts +15 -0
- package/dist/components/form-panel.js +9 -0
- package/dist/components/funding-picker.d.ts +20 -0
- package/dist/components/funding-picker.js +16 -0
- package/dist/components/input.d.ts +3 -0
- package/dist/components/input.js +6 -0
- package/dist/components/invitation-card.d.ts +14 -0
- package/dist/components/invitation-card.js +13 -0
- package/dist/components/label.d.ts +4 -0
- package/dist/components/label.js +7 -0
- package/dist/components/member-card-block.d.ts +47 -0
- package/dist/components/member-card-block.js +329 -0
- package/dist/components/member-card.css +92 -0
- package/dist/components/member-card.d.ts +29 -0
- package/dist/components/member-card.js +22 -0
- package/dist/components/month-picker.d.ts +16 -0
- package/dist/components/month-picker.js +19 -0
- package/dist/components/offline-banner.d.ts +14 -0
- package/dist/components/offline-banner.js +14 -0
- package/dist/components/page-header.d.ts +14 -0
- package/dist/components/page-header.js +10 -0
- package/dist/components/phone-field.d.ts +25 -0
- package/dist/components/phone-field.js +64 -0
- package/dist/components/pill-radio.d.ts +21 -0
- package/dist/components/pill-radio.js +15 -0
- package/dist/components/portal-shell.d.ts +53 -0
- package/dist/components/portal-shell.js +19 -0
- package/dist/components/postcode-field.d.ts +26 -0
- package/dist/components/postcode-field.js +14 -0
- package/dist/components/progress.d.ts +4 -0
- package/dist/components/progress.js +8 -0
- package/dist/components/radio-group.d.ts +5 -0
- package/dist/components/radio-group.js +11 -0
- package/dist/components/review-panel.d.ts +20 -0
- package/dist/components/review-panel.js +14 -0
- package/dist/components/saving-indicator.d.ts +15 -0
- package/dist/components/saving-indicator.js +19 -0
- package/dist/components/scroll-area.d.ts +5 -0
- package/dist/components/scroll-area.js +13 -0
- package/dist/components/search-filter.d.ts +39 -0
- package/dist/components/search-filter.js +19 -0
- package/dist/components/segmented-progress.d.ts +16 -0
- package/dist/components/segmented-progress.js +12 -0
- package/dist/components/select.d.ts +15 -0
- package/dist/components/select.js +38 -0
- package/dist/components/separator.d.ts +4 -0
- package/dist/components/separator.js +7 -0
- package/dist/components/service-tiles.d.ts +26 -0
- package/dist/components/service-tiles.js +17 -0
- package/dist/components/sheet.d.ts +14 -0
- package/dist/components/sheet.js +40 -0
- package/dist/components/sidebar.d.ts +69 -0
- package/dist/components/sidebar.js +210 -0
- package/dist/components/skeleton.d.ts +11 -0
- package/dist/components/skeleton.js +11 -0
- package/dist/components/sonner.d.ts +9 -0
- package/dist/components/sonner.js +25 -0
- package/dist/components/spinner.d.ts +14 -0
- package/dist/components/spinner.js +22 -0
- package/dist/components/stat-card.d.ts +22 -0
- package/dist/components/stat-card.js +12 -0
- package/dist/components/status-card.d.ts +24 -0
- package/dist/components/status-card.js +26 -0
- package/dist/components/stepper.d.ts +25 -0
- package/dist/components/stepper.js +23 -0
- package/dist/components/submit-overlay.d.ts +16 -0
- package/dist/components/submit-overlay.js +15 -0
- package/dist/components/suggestion-sheet.d.ts +20 -0
- package/dist/components/suggestion-sheet.js +36 -0
- package/dist/components/switch.d.ts +6 -0
- package/dist/components/switch.js +8 -0
- package/dist/components/table.d.ts +10 -0
- package/dist/components/table.js +28 -0
- package/dist/components/tabs.d.ts +11 -0
- package/dist/components/tabs.js +28 -0
- package/dist/components/tag-input.d.ts +15 -0
- package/dist/components/tag-input.js +31 -0
- package/dist/components/textarea.d.ts +3 -0
- package/dist/components/textarea.js +6 -0
- package/dist/components/timeline.d.ts +18 -0
- package/dist/components/timeline.js +11 -0
- package/dist/components/todo-strip.d.ts +14 -0
- package/dist/components/todo-strip.js +11 -0
- package/dist/components/tooltip.d.ts +7 -0
- package/dist/components/tooltip.js +16 -0
- package/dist/components/top-bar.d.ts +23 -0
- package/dist/components/top-bar.js +14 -0
- package/dist/components/type-ahead.d.ts +20 -0
- package/dist/components/type-ahead.js +22 -0
- package/dist/components/unit-card.d.ts +26 -0
- package/dist/components/unit-card.js +18 -0
- package/dist/components/view-toggle.d.ts +20 -0
- package/dist/components/view-toggle.js +15 -0
- package/dist/components/wizard-footer.d.ts +24 -0
- package/dist/components/wizard-footer.js +17 -0
- package/dist/components/wizard-header.d.ts +29 -0
- package/dist/components/wizard-header.js +18 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +11 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +70 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/sample-banners.d.ts +8 -0
- package/dist/sample-banners.js +28 -0
- package/dist/tokens.css +119 -0
- package/dist/tokens.json +50 -0
- package/package.json +60 -4
- package/README.md +0 -3
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/* Member card (openspec/changes/revamp-member-card). Values follow reference/styles.css; the flip is CSS, not react-card-flip. */
|
|
2
|
+
|
|
3
|
+
.mc-fit { width:100%; max-width:350px; margin:0 auto; }
|
|
4
|
+
.mc-hit { all:unset; display:block; width:350px; height:221px; cursor:pointer; border-radius:16px; margin:14px 0; touch-action:pan-y; -webkit-tap-highlight-color:transparent; }
|
|
5
|
+
.mc-hit:focus-visible { outline:2px solid var(--brand-red, #c8102e); outline-offset:4px; }
|
|
6
|
+
.mc-tilt { width:350px; height:221px; border-radius:16px; }
|
|
7
|
+
.mc-tilt > div, .mc-flip { width:350px; height:221px; }
|
|
8
|
+
.mc-flip { position:relative; border-radius:16px; transform-style:preserve-3d; transition:transform .6s; }
|
|
9
|
+
.mc-flip.is-flipped { transform:rotateY(180deg); }
|
|
10
|
+
.mc-face-wrap { position:absolute; left:0; top:0; width:350px; height:221px; border-radius:16px; backface-visibility:hidden; -webkit-backface-visibility:hidden; }
|
|
11
|
+
.mc-back-wrap { transform:rotateY(180deg); }
|
|
12
|
+
@media (prefers-reduced-motion: reduce) { .mc-flip { transition:none; } }
|
|
13
|
+
|
|
14
|
+
.mc-face { box-shadow:0 26px 30px -14px rgba(28,31,35,.4); position:relative; overflow:hidden; width:350px; height:221px; border-radius:16px; border:1px solid #e3e5e8; padding:20px; display:flex; flex-direction:column; justify-content:space-between; color:#1c1f23;
|
|
15
|
+
background: repeating-linear-gradient(45deg, rgba(28,31,35,.028) 0 1px, transparent 1px 6px), linear-gradient(135deg,#fff 0%,#f1f2f4 100%); }
|
|
16
|
+
/* Artwork has its own clipped viewport; it cannot enter the name column or footer. */
|
|
17
|
+
.mc-front { background:var(--mc-base, #fff); }
|
|
18
|
+
.mc-art { position:absolute; left:64%; top:23%; width:31%; height:47%; overflow:hidden; pointer-events:none; }
|
|
19
|
+
.mc-front .mc-id { position:absolute; left:20px; top:98px; width:calc(64% - 24px); gap:3px; }
|
|
20
|
+
.mc-front .mc-name { height:40px; font-size:17px; line-height:20px; text-wrap:wrap; }
|
|
21
|
+
.mc-front .mc-number { font-size:12px; line-height:15px; }
|
|
22
|
+
.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
|
+
.mc-front .mc-foot { position:absolute; left:20px; right:20px; bottom:14px; height:36px; gap:8px; }
|
|
24
|
+
.mc-front .mc-col:first-child { min-width:0; flex:1; }
|
|
25
|
+
.mc-front .mc-right { flex:0 0 92px; }
|
|
26
|
+
.mc-front .mc-label { font-size:10px; line-height:11px; }
|
|
27
|
+
.mc-front .mc-val { font-size:11.5px; line-height:12px; overflow-wrap:anywhere; display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden; }
|
|
28
|
+
.mc-front.mc-grace, .mc-front.mc-expired { background:#eceded; }
|
|
29
|
+
.mc-grace .mc-art, .mc-expired .mc-art { filter:grayscale(1); opacity:.5; }
|
|
30
|
+
.mc-front.mc-suspended { background:#fef2f2; }
|
|
31
|
+
.mc-suspended .mc-art { display:none; }
|
|
32
|
+
.mc-theme-grid { display:grid; gap:20px; }
|
|
33
|
+
.mc-theme-group h3 { margin:0 0 8px; font-size:13px; font-weight:600; color:#1c1f23; }
|
|
34
|
+
.mc-theme-tiles { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; }
|
|
35
|
+
@media (min-width:480px) { .mc-theme-tiles { grid-template-columns:repeat(3,minmax(0,1fr)); } }
|
|
36
|
+
.mc-tile { position:relative; display:grid; gap:6px; padding:6px; border:1px solid #e3e5e8; border-radius:10px; background:#fff; cursor:pointer; font:inherit; text-align:left; }
|
|
37
|
+
.mc-tile:hover { border-color:#c9ccd1; }
|
|
38
|
+
.mc-tile:focus-visible { outline:2px solid #c8102e; outline-offset:2px; }
|
|
39
|
+
.mc-tile-on { border-color:#c8102e; box-shadow:0 0 0 1px #c8102e; }
|
|
40
|
+
.mc-thumb { display:block; aspect-ratio:350/221; border-radius:6px; overflow:hidden; }
|
|
41
|
+
.mc-thumb svg { display:block; width:100%; height:100%; }
|
|
42
|
+
.mc-thumb-auto { display:grid; place-items:center; background:#f5f5f4; color:#5b6168; font-size:12px; font-weight:600; }
|
|
43
|
+
.mc-tile-label { font-size:12px; line-height:1.35; color:#1c1f23; overflow-wrap:anywhere; }
|
|
44
|
+
.mc-tile-check { position:absolute; top:12px; right:12px; width:18px; height:18px; color:#c8102e; background:#fff; border-radius:50%; padding:2px; }
|
|
45
|
+
|
|
46
|
+
.mc-row { position:relative; z-index:1; display:flex; justify-content:space-between; align-items:flex-start; }
|
|
47
|
+
.mc-logo { width:92px; height:63px; object-fit:contain; display:block; }
|
|
48
|
+
.mc-chip { margin-top:2px; font-size:12px; font-weight:700; background:#047857; color:#fff; border-radius:6px; padding:4px 10px; }
|
|
49
|
+
.mc-id { position:relative; z-index:1; display:flex; flex-direction:column; gap:4px; margin-top:14px; }
|
|
50
|
+
.mc-name { height:48px; font-size:20px; font-weight:700; line-height:1.2; display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; overflow:hidden; overflow-wrap:anywhere; text-wrap:balance; }
|
|
51
|
+
.mc-number { font-family:ui-monospace,Menlo,monospace; font-size:15px; letter-spacing:.08em; }
|
|
52
|
+
.mc-note { font-size:14px; line-height:1.4; }
|
|
53
|
+
.mc-foot { align-items:flex-end; }
|
|
54
|
+
.mc-col { display:flex; flex-direction:column; gap:2px; }
|
|
55
|
+
.mc-right { text-align:right; }
|
|
56
|
+
.mc-label { font-size:11px; letter-spacing:.04em; color:#5b6168; }
|
|
57
|
+
.mc-val { font-size:14px; font-weight:600; }
|
|
58
|
+
|
|
59
|
+
.mc-expiring .mc-chip { background:#fcd34d; color:#451a03; }
|
|
60
|
+
.mc-grace, .mc-expired { background:linear-gradient(135deg,#eceded,#dfe1e3); color:#4a5057; }
|
|
61
|
+
.mc-expired .mc-chip { background:#4a5057; }
|
|
62
|
+
.mc-grace .mc-chip { background:#fcd34d; color:#451a03; }
|
|
63
|
+
.mc-grace .mc-label, .mc-expired .mc-label { color:#4a5057; }
|
|
64
|
+
.mc-grace .mc-logo, .mc-expired .mc-logo { filter:grayscale(1); opacity:.65; }
|
|
65
|
+
.mc-suspended { background:#fef2f2; border-color:#fecaca; color:#7f1d1d; }
|
|
66
|
+
.mc-suspended .mc-chip { background:#7f1d1d; }
|
|
67
|
+
.mc-suspended .mc-label { color:#7f1d1d; }
|
|
68
|
+
.mc-suspended .mc-logo { filter:grayscale(1); opacity:.6; }
|
|
69
|
+
|
|
70
|
+
.mc-back { padding:0; justify-content:flex-start; }
|
|
71
|
+
.mc-map { position:absolute; left:50%; top:66px; width:372px; height:auto; transform:translateX(-50%); opacity:.17; pointer-events:none; user-select:none; }
|
|
72
|
+
.mc-stripe { position:relative; z-index:1; height:40px; margin-top:18px; background:#1c1f23; color:#fff; display:flex; align-items:center; padding:0 20px; font-size:10.5px; font-weight:600; line-height:1.35; letter-spacing:.03em; text-wrap:balance; }
|
|
73
|
+
.mc-codewrap { position:relative; z-index:1; flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px; padding:0 16px 12px; text-align:center; }
|
|
74
|
+
.mc-quiet { background:#fff; padding:8px 14px; border-radius:6px; line-height:0; box-shadow:0 1px 0 rgba(28,31,35,.06), 0 6px 14px -8px rgba(28,31,35,.35); }
|
|
75
|
+
.mc-small { font-size:13px; }
|
|
76
|
+
.mc-hint { font-size:12px; color:#5b6168; }
|
|
77
|
+
.mc-suspended .mc-hint { color:#7f1d1d; }
|
|
78
|
+
.mc-shine { position:absolute; left:0; top:0; right:0; bottom:0; z-index:0; pointer-events:none; border-radius:16px; }
|
|
79
|
+
.mc-backnote { max-width:260px; font-size:13px; line-height:1.4; }
|
|
80
|
+
|
|
81
|
+
.mc-sr-only { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); clip-path:inset(50%); white-space:nowrap; }
|
|
82
|
+
.mc-block { width:100%; max-width:350px; margin:0 auto; }
|
|
83
|
+
.mc-actions { display:flex; gap:8px; margin-top:4px; }
|
|
84
|
+
.mc-btn { flex:1; min-height:44px; padding:0 12px; border-radius:8px; border:1px solid #e3e5e8; background:#fff; color:#1c1f23; font:600 14px inherit; cursor:pointer; }
|
|
85
|
+
.mc-btn:disabled { opacity:.6; cursor:progress; }
|
|
86
|
+
.mc-btn-primary { background:#c8102e; border-color:#c8102e; color:#fff; }
|
|
87
|
+
.mc-btn:focus-visible { outline:2px solid #c8102e; outline-offset:2px; }
|
|
88
|
+
.mc-msg { min-height:20px; margin:8px 0 0; font-size:13px; color:#047857; text-align:center; }
|
|
89
|
+
.mc-msg-err { color:#b91c1c; }
|
|
90
|
+
.mc-sheet-wrap { position:fixed; left:-10000px; top:0; pointer-events:none; }
|
|
91
|
+
.mc-sheet { width:398px; padding:24px 24px 44px; box-sizing:border-box; background:#f6f6f4; display:flex; flex-direction:column; gap:36px; }
|
|
92
|
+
@media (min-width:768px) { .mc-block { margin:0; } }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type MemberCardTheme = "red" | "ink";
|
|
3
|
+
export interface MemberCardProps {
|
|
4
|
+
name: string;
|
|
5
|
+
memberId: string;
|
|
6
|
+
komsat: string;
|
|
7
|
+
affiliation?: string;
|
|
8
|
+
validUntil: string;
|
|
9
|
+
statusLabel: string;
|
|
10
|
+
statusVariant?: "default" | "secondary" | "destructive" | "outline";
|
|
11
|
+
validLabel?: string;
|
|
12
|
+
theme?: MemberCardTheme;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Presentation pattern for the PPIJ member card face.
|
|
17
|
+
* Display data arrives as props; issuing, verification, and card state
|
|
18
|
+
* logic stay in the application. The interactive card in the portal
|
|
19
|
+
* (flip, tilt, live barcode) builds on this face.
|
|
20
|
+
*/
|
|
21
|
+
export declare function MemberCard({ name, memberId, komsat, affiliation, validUntil, statusLabel, statusVariant, validLabel, theme, className, }: MemberCardProps): React.JSX.Element;
|
|
22
|
+
export interface MemberCardBackProps {
|
|
23
|
+
memberId: string;
|
|
24
|
+
note: string;
|
|
25
|
+
barcodeLabel?: string;
|
|
26
|
+
className?: string;
|
|
27
|
+
}
|
|
28
|
+
/** Back face: deterministic presentation bars derived from the member id. */
|
|
29
|
+
export declare function MemberCardBack({ memberId, note, barcodeLabel, className }: MemberCardBackProps): React.JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { Badge } from "./badge";
|
|
4
|
+
/**
|
|
5
|
+
* Presentation pattern for the PPIJ member card face.
|
|
6
|
+
* Display data arrives as props; issuing, verification, and card state
|
|
7
|
+
* logic stay in the application. The interactive card in the portal
|
|
8
|
+
* (flip, tilt, live barcode) builds on this face.
|
|
9
|
+
*/
|
|
10
|
+
export function MemberCard({ name, memberId, komsat, affiliation, validUntil, statusLabel, statusVariant = "secondary", validLabel = "Berlaku hingga", theme = "red", className, }) {
|
|
11
|
+
return (_jsxs("div", { "data-slot": "member-card", className: cn("relative flex aspect-[1.586] w-full max-w-[420px] flex-col justify-between overflow-hidden rounded-2xl p-5 text-white shadow-md", theme === "red"
|
|
12
|
+
? "bg-gradient-to-br from-[#e11436] via-brand-red to-[#8f0c22]"
|
|
13
|
+
: "bg-gradient-to-br from-[#2a2e33] via-brand-ink to-black", className), children: [_jsxs("div", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsx("p", { className: "text-[11px] font-bold tracking-[0.18em]", children: "PPI JEPANG" }), _jsx("p", { className: "text-[10px] text-white/75", children: "Kartu Anggota" })] }), _jsx(Badge, { variant: statusVariant, children: statusLabel })] }), _jsxs("div", { className: "grid gap-1", children: [_jsx("p", { className: "truncate text-lg leading-tight font-bold", children: name }), _jsx("p", { className: "font-mono text-sm tracking-widest text-white/90", children: memberId }), _jsxs("div", { className: "mt-1 flex items-end justify-between gap-3 text-[11px] text-white/80", children: [_jsx("span", { className: "min-w-0", children: _jsxs("span", { className: "block truncate", children: [komsat, affiliation ? ` · ${affiliation}` : ""] }) }), _jsxs("span", { className: "shrink-0 text-right", children: [_jsx("span", { className: "block text-[9px] tracking-wider uppercase", children: validLabel }), _jsx("span", { className: "text-xs font-semibold text-white", children: validUntil })] })] })] })] }));
|
|
14
|
+
}
|
|
15
|
+
/** Back face: deterministic presentation bars derived from the member id. */
|
|
16
|
+
export function MemberCardBack({ memberId, note, barcodeLabel = "Nomor anggota", className }) {
|
|
17
|
+
const bars = Array.from(memberId.replace(/\D/g, "") || "0").map((ch, i) => {
|
|
18
|
+
const w = (Number(ch) % 3) + 1;
|
|
19
|
+
return _jsx("span", { style: { width: w }, className: "inline-block h-full bg-brand-ink" }, i);
|
|
20
|
+
});
|
|
21
|
+
return (_jsxs("div", { "data-slot": "member-card-back", className: cn("flex aspect-[1.586] w-full max-w-[420px] flex-col justify-between overflow-hidden rounded-2xl bg-[#f1f1ec] p-5 text-brand-ink shadow-md", className), children: [_jsx("div", { className: "h-8 rounded bg-brand-ink" }), _jsxs("div", { children: [_jsx("p", { className: "mb-1 text-[10px] tracking-wider text-brand-muted uppercase", children: barcodeLabel }), _jsx("div", { className: "flex h-12 items-stretch gap-[2px] bg-white p-1.5", children: bars }), _jsx("p", { className: "mt-1 text-center font-mono text-xs tracking-[0.3em]", children: memberId })] }), _jsx("p", { className: "text-[10px] leading-snug text-brand-muted", children: note })] }));
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface MonthPickerProps {
|
|
2
|
+
/** Group label; the two menus are meaningless apart. */
|
|
3
|
+
label: string;
|
|
4
|
+
/** Wire value "YYYY-MM"; either part may still be empty while the member is choosing. */
|
|
5
|
+
value: string;
|
|
6
|
+
onChange: (value: string) => void;
|
|
7
|
+
locale?: "id" | "en";
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Month and year entry (WIZ106-09), extracted from the wizard: two
|
|
12
|
+
* Selects, named months plus a year, in a 1.35fr / 1fr grid. The
|
|
13
|
+
* native month input is deliberately not used (WebKit-hostile). The
|
|
14
|
+
* composed "YYYY-MM" wire format never changes.
|
|
15
|
+
*/
|
|
16
|
+
export declare function MonthPicker({ label, value, onChange, locale, className }: MonthPickerProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select";
|
|
4
|
+
const MONTHS_ID = ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"];
|
|
5
|
+
const MONTHS_EN = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
|
6
|
+
/**
|
|
7
|
+
* Month and year entry (WIZ106-09), extracted from the wizard: two
|
|
8
|
+
* Selects, named months plus a year, in a 1.35fr / 1fr grid. The
|
|
9
|
+
* native month input is deliberately not used (WebKit-hostile). The
|
|
10
|
+
* composed "YYYY-MM" wire format never changes.
|
|
11
|
+
*/
|
|
12
|
+
export function MonthPicker({ label, value, onChange, locale = "id", className }) {
|
|
13
|
+
const [year = "", month = ""] = value ? value.split("-") : [];
|
|
14
|
+
const months = locale === "en" ? MONTHS_EN : MONTHS_ID;
|
|
15
|
+
const [monthWord, yearWord] = locale === "en" ? ["Month", "Year"] : ["Bulan", "Tahun"];
|
|
16
|
+
const years = Array.from({ length: new Date().getFullYear() - 1989 + 15 }, (_, i) => String(1990 + i)).reverse();
|
|
17
|
+
const compose = (y, m) => (y || m ? `${y}-${m}` : "");
|
|
18
|
+
return (_jsxs("div", { role: "group", "aria-label": label, className: cn("min-w-0 w-full", className), children: [_jsx("p", { className: "mb-2 text-sm font-medium text-brand-ink", children: label }), _jsxs("div", { className: "grid w-full grid-cols-[minmax(0,1.35fr)_minmax(0,1fr)] gap-2", children: [_jsxs(Select, { value: month || "", onValueChange: (next) => onChange(compose(year, next)), children: [_jsx(SelectTrigger, { "aria-label": `${label}: ${monthWord}`, className: "min-h-11 w-full min-w-0 bg-card", children: _jsx(SelectValue, { placeholder: monthWord }) }), _jsx(SelectContent, { position: "popper", align: "start", avoidCollisions: false, className: "max-h-72", children: months.map((name, i) => (_jsx(SelectItem, { value: String(i + 1).padStart(2, "0"), children: name }, name))) })] }), _jsxs(Select, { value: year || "", onValueChange: (next) => onChange(compose(next, month)), children: [_jsx(SelectTrigger, { "aria-label": `${label}: ${yearWord}`, className: "min-h-11 w-full min-w-0 bg-card", children: _jsx(SelectValue, { placeholder: yearWord }) }), _jsx(SelectContent, { position: "popper", align: "start", avoidCollisions: false, className: "max-h-72", children: years.map((y) => (_jsx(SelectItem, { value: y, children: y }, y))) })] })] })] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface OfflineBannerProps {
|
|
2
|
+
/** Rendered only when true; the consumer owns the online check (the app uses its useOnline hook). */
|
|
3
|
+
show?: boolean;
|
|
4
|
+
/** Defaults to the app's id string; pass the en string or any label to override. */
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Offline banner, extracted exactly from ppij-identity-sso
|
|
9
|
+
* portal/OfflineBanner.tsx: an amber strip under the header (it sits
|
|
10
|
+
* in PortalShell's banner slot) shown while the browser reports no
|
|
11
|
+
* network. Copy: "Tidak ada koneksi. Data Anda tersimpan." /
|
|
12
|
+
* "No connection. Your data is saved."
|
|
13
|
+
*/
|
|
14
|
+
export declare function OfflineBanner({ show, label }: OfflineBannerProps): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { IconWifiOff } from "@tabler/icons-react";
|
|
3
|
+
/**
|
|
4
|
+
* Offline banner, extracted exactly from ppij-identity-sso
|
|
5
|
+
* portal/OfflineBanner.tsx: an amber strip under the header (it sits
|
|
6
|
+
* in PortalShell's banner slot) shown while the browser reports no
|
|
7
|
+
* network. Copy: "Tidak ada koneksi. Data Anda tersimpan." /
|
|
8
|
+
* "No connection. Your data is saved."
|
|
9
|
+
*/
|
|
10
|
+
export function OfflineBanner({ show = true, label = "Tidak ada koneksi. Data Anda tersimpan." }) {
|
|
11
|
+
if (!show)
|
|
12
|
+
return null;
|
|
13
|
+
return (_jsx("div", { role: "status", className: "border-b border-amber-300 bg-amber-50 text-amber-900", children: _jsxs("p", { className: "mx-auto flex max-w-2xl items-center gap-2 px-4 py-2 text-sm font-semibold", children: [_jsx(IconWifiOff, { className: "size-4 shrink-0", "aria-hidden": "true" }), label] }) }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface PageHeaderProps {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
eyebrow?: string;
|
|
6
|
+
actions?: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Admin page header pattern, following the portal admin convention:
|
|
11
|
+
* a 2xl bold ink title, a muted description, and actions on the right
|
|
12
|
+
* that wrap under the title on narrow screens.
|
|
13
|
+
*/
|
|
14
|
+
export declare function PageHeader({ title, description, eyebrow, actions, className }: PageHeaderProps): React.JSX.Element;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
/**
|
|
4
|
+
* Admin page header pattern, following the portal admin convention:
|
|
5
|
+
* a 2xl bold ink title, a muted description, and actions on the right
|
|
6
|
+
* that wrap under the title on narrow screens.
|
|
7
|
+
*/
|
|
8
|
+
export function PageHeader({ title, description, eyebrow, actions, className }) {
|
|
9
|
+
return (_jsxs("div", { className: cn("flex flex-wrap items-start justify-between gap-3", className), children: [_jsxs("div", { className: "min-w-0", children: [eyebrow ? (_jsx("p", { className: "mb-1 text-[11px] font-bold tracking-[0.14em] text-brand-red uppercase", children: eyebrow })) : null, _jsx("h1", { className: "text-2xl leading-tight font-bold text-brand-ink", children: title }), description ? _jsx("p", { className: "mt-1 max-w-prose text-sm text-brand-muted", children: description }) : null] }), actions ? _jsx("div", { className: "flex shrink-0 flex-wrap items-center gap-2", children: actions }) : null] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* The curated calling codes from the portal PhoneField (WIZ106-14):
|
|
4
|
+
* Japan and Indonesia pinned under "Sering dipakai", the rest under
|
|
5
|
+
* "Negara lain". Entries are [code, ISO, Indonesian name, English name].
|
|
6
|
+
*/
|
|
7
|
+
export declare const PHONE_COUNTRY_CODES: ReadonlyArray<readonly [code: string, iso: string, idName: string, enName: string]>;
|
|
8
|
+
export interface PhoneFieldProps {
|
|
9
|
+
id?: string;
|
|
10
|
+
/** Composed value, for example "+81 90-1234-5678". Empty means no number yet. */
|
|
11
|
+
value: string;
|
|
12
|
+
onChange: (value: string) => void;
|
|
13
|
+
/** Prefix assumed when the stored value carries none: +81 for the member, +62 for the emergency contact. */
|
|
14
|
+
defaultCode?: string;
|
|
15
|
+
locale?: "id" | "en";
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Phone entry, extracted from the wizard PhoneField: a grouped
|
|
20
|
+
* country-code Select (104px, showing "JP +81") paired with the
|
|
21
|
+
* national number input. The pair composes one stored string,
|
|
22
|
+
* "+81 90-1234-5678". Both controls are field height (44px); the
|
|
23
|
+
* label is composed by the parent like every other field.
|
|
24
|
+
*/
|
|
25
|
+
export declare function PhoneField({ id, value, onChange, defaultCode, locale, className }: PhoneFieldProps): React.JSX.Element;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
import { Input } from "./input";
|
|
5
|
+
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue, } from "./select";
|
|
6
|
+
/**
|
|
7
|
+
* The curated calling codes from the portal PhoneField (WIZ106-14):
|
|
8
|
+
* Japan and Indonesia pinned under "Sering dipakai", the rest under
|
|
9
|
+
* "Negara lain". Entries are [code, ISO, Indonesian name, English name].
|
|
10
|
+
*/
|
|
11
|
+
export const PHONE_COUNTRY_CODES = [
|
|
12
|
+
["+81", "JP", "Jepang", "Japan"],
|
|
13
|
+
["+62", "ID", "Indonesia", "Indonesia"],
|
|
14
|
+
["+60", "MY", "Malaysia", "Malaysia"],
|
|
15
|
+
["+65", "SG", "Singapura", "Singapore"],
|
|
16
|
+
["+673", "BN", "Brunei", "Brunei"],
|
|
17
|
+
["+66", "TH", "Thailand", "Thailand"],
|
|
18
|
+
["+63", "PH", "Filipina", "Philippines"],
|
|
19
|
+
["+84", "VN", "Vietnam", "Vietnam"],
|
|
20
|
+
["+82", "KR", "Korea Selatan", "South Korea"],
|
|
21
|
+
["+86", "CN", "Tiongkok", "China"],
|
|
22
|
+
["+61", "AU", "Australia", "Australia"],
|
|
23
|
+
["+64", "NZ", "Selandia Baru", "New Zealand"],
|
|
24
|
+
["+1", "US", "Amerika Serikat", "United States"],
|
|
25
|
+
["+44", "GB", "Inggris Raya", "United Kingdom"],
|
|
26
|
+
["+31", "NL", "Belanda", "Netherlands"],
|
|
27
|
+
["+49", "DE", "Jerman", "Germany"],
|
|
28
|
+
["+33", "FR", "Prancis", "France"],
|
|
29
|
+
["+966", "SA", "Arab Saudi", "Saudi Arabia"],
|
|
30
|
+
["+971", "AE", "Uni Emirat Arab", "United Arab Emirates"],
|
|
31
|
+
["+974", "QA", "Qatar", "Qatar"],
|
|
32
|
+
["+968", "OM", "Oman", "Oman"],
|
|
33
|
+
["+20", "EG", "Mesir", "Egypt"],
|
|
34
|
+
["+90", "TR", "Turki", "Türkiye"],
|
|
35
|
+
];
|
|
36
|
+
/** Split by the longest matching curated code, so +673 never reads as +67 or +6. */
|
|
37
|
+
function splitPhone(stored) {
|
|
38
|
+
const codes = PHONE_COUNTRY_CODES.map((c) => c[0]).sort((a, b) => b.length - a.length);
|
|
39
|
+
for (const code of codes) {
|
|
40
|
+
if (stored.startsWith(code))
|
|
41
|
+
return { code, number: stored.slice(code.length).trim() };
|
|
42
|
+
}
|
|
43
|
+
return { code: "", number: stored };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Phone entry, extracted from the wizard PhoneField: a grouped
|
|
47
|
+
* country-code Select (104px, showing "JP +81") paired with the
|
|
48
|
+
* national number input. The pair composes one stored string,
|
|
49
|
+
* "+81 90-1234-5678". Both controls are field height (44px); the
|
|
50
|
+
* label is composed by the parent like every other field.
|
|
51
|
+
*/
|
|
52
|
+
export function PhoneField({ id, value, onChange, defaultCode = "+81", locale = "id", className }) {
|
|
53
|
+
const [chosen, setChosen] = React.useState("");
|
|
54
|
+
const split = splitPhone(value);
|
|
55
|
+
const selected = split.code || chosen || defaultCode;
|
|
56
|
+
const number = split.number;
|
|
57
|
+
const en = locale === "en";
|
|
58
|
+
const compose = (code, national) => (national.trim() ? `${code} ${national}` : "");
|
|
59
|
+
const item = ([code, iso, idName, enName]) => (_jsxs(SelectItem, { value: code, textValue: `${en ? enName : idName} ${code}`, children: [iso, " ", code] }, code));
|
|
60
|
+
return (_jsxs("div", { className: cn("grid min-w-0 grid-cols-[104px_minmax(0,1fr)] gap-2", className), children: [_jsxs(Select, { value: selected, onValueChange: (code) => {
|
|
61
|
+
setChosen(code);
|
|
62
|
+
onChange(compose(code, number));
|
|
63
|
+
}, children: [_jsx(SelectTrigger, { "aria-label": en ? "Country code" : "Kode negara", className: "h-11 min-h-11 w-full bg-card", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { position: "popper", align: "start", avoidCollisions: false, className: "max-h-72", children: [_jsxs(SelectGroup, { children: [_jsx(SelectLabel, { children: en ? "Frequently used" : "Sering dipakai" }), PHONE_COUNTRY_CODES.slice(0, 2).map(item)] }), _jsxs(SelectGroup, { children: [_jsx(SelectLabel, { children: en ? "Other countries" : "Negara lain" }), PHONE_COUNTRY_CODES.slice(2).map(item)] })] })] }), _jsx(Input, { id: id, type: "tel", inputMode: "tel", autoComplete: "tel-national", className: "h-11 min-h-11 min-w-0", value: number, onChange: (event) => onChange(compose(selected, event.target.value)) })] }));
|
|
64
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface PillRadioOption {
|
|
2
|
+
value: string;
|
|
3
|
+
label: string;
|
|
4
|
+
}
|
|
5
|
+
export interface PillRadioProps {
|
|
6
|
+
options: PillRadioOption[];
|
|
7
|
+
value: string;
|
|
8
|
+
onChange: (value: string) => void;
|
|
9
|
+
ariaLabel: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
}
|
|
12
|
+
/** The wizard pill treatment (WIZ106-16): the selected pill takes the primary tint. */
|
|
13
|
+
export declare const pillRadioClass = "min-h-11 max-w-full rounded-full border border-border bg-card px-4 py-2 text-sm outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[state=checked]:border-primary data-[state=checked]:bg-primary/10 data-[state=checked]:text-primary";
|
|
14
|
+
/**
|
|
15
|
+
* Pill radio group, extracted from the wizard steps (Jenis kelamin,
|
|
16
|
+
* Jenjang): one control per group, options as 44px pills that wrap,
|
|
17
|
+
* the chosen pill tinted with the primary color. Use for short,
|
|
18
|
+
* mutually exclusive choice sets where every option should stay
|
|
19
|
+
* visible; longer sets belong in a Select.
|
|
20
|
+
*/
|
|
21
|
+
export declare function PillRadio({ options, value, onChange, ariaLabel, className }: PillRadioProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
/** The wizard pill treatment (WIZ106-16): the selected pill takes the primary tint. */
|
|
5
|
+
export const pillRadioClass = "min-h-11 max-w-full rounded-full border border-border bg-card px-4 py-2 text-sm outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 data-[state=checked]:border-primary data-[state=checked]:bg-primary/10 data-[state=checked]:text-primary";
|
|
6
|
+
/**
|
|
7
|
+
* Pill radio group, extracted from the wizard steps (Jenis kelamin,
|
|
8
|
+
* Jenjang): one control per group, options as 44px pills that wrap,
|
|
9
|
+
* the chosen pill tinted with the primary color. Use for short,
|
|
10
|
+
* mutually exclusive choice sets where every option should stay
|
|
11
|
+
* visible; longer sets belong in a Select.
|
|
12
|
+
*/
|
|
13
|
+
export function PillRadio({ options, value, onChange, ariaLabel, className }) {
|
|
14
|
+
return (_jsx(RadioGroupPrimitive.Root, { value: value, onValueChange: onChange, "aria-label": ariaLabel, className: cn("flex flex-wrap gap-2", className), children: options.map((option) => (_jsx(RadioGroupPrimitive.Item, { value: option.value, className: pillRadioClass, children: option.label }, option.value))) }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface PortalNavItem {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
/** Tabler icon element, shown in the phone bottom nav. */
|
|
6
|
+
icon: React.ReactNode;
|
|
7
|
+
current?: boolean;
|
|
8
|
+
onClick?: (event: React.MouseEvent<HTMLAnchorElement>) => void;
|
|
9
|
+
}
|
|
10
|
+
export interface PortalLanguage {
|
|
11
|
+
label: string;
|
|
12
|
+
current?: boolean;
|
|
13
|
+
onSelect?: () => void;
|
|
14
|
+
}
|
|
15
|
+
export interface PortalShellProps {
|
|
16
|
+
/** Logo image source; the consuming app serves it (in the portal app: /ppij-logo.png). */
|
|
17
|
+
logoSrc: string;
|
|
18
|
+
logoAlt?: string;
|
|
19
|
+
/** The word beside the logo, separated by a thin divider. */
|
|
20
|
+
brandWord?: string;
|
|
21
|
+
homeHref?: string;
|
|
22
|
+
/** Primary navigation. Beranda is always present; Profil appears for active members, Verifikasi while a decision is pending. The shell renders what it is given. */
|
|
23
|
+
navItems: PortalNavItem[];
|
|
24
|
+
languages: PortalLanguage[];
|
|
25
|
+
languageLabel?: string;
|
|
26
|
+
accountItems: {
|
|
27
|
+
label: string;
|
|
28
|
+
href: string;
|
|
29
|
+
}[];
|
|
30
|
+
accountLabel?: string;
|
|
31
|
+
/** Slot for the Portal|Admin switch, rendered before the language control. */
|
|
32
|
+
viewSwitch?: React.ReactNode;
|
|
33
|
+
/** Slot under the header, for example the offline banner. */
|
|
34
|
+
banner?: React.ReactNode;
|
|
35
|
+
skipLabel?: string;
|
|
36
|
+
helpText?: string;
|
|
37
|
+
legalItems?: {
|
|
38
|
+
label: string;
|
|
39
|
+
href: string;
|
|
40
|
+
}[];
|
|
41
|
+
children: React.ReactNode;
|
|
42
|
+
className?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The portal chrome, extracted from the portal AppShell: sticky white
|
|
46
|
+
* top bar with the brand lockup (logo, divider, the word Portal),
|
|
47
|
+
* desktop navigation inside the bar, an icon-only language menu with
|
|
48
|
+
* the current language checked, an icon-only account menu, and a fixed
|
|
49
|
+
* bottom nav on phones whose columns follow the item count. Content
|
|
50
|
+
* sits in a max-w-5xl column with room for the bottom nav. All state
|
|
51
|
+
* (who is signed in, which nav applies) stays in the application.
|
|
52
|
+
*/
|
|
53
|
+
export declare function PortalShell({ logoSrc, logoAlt, brandWord, homeHref, navItems, languages, languageLabel, accountItems, accountLabel, viewSwitch, banner, skipLabel, helpText, legalItems, children, className, }: PortalShellProps): React.JSX.Element;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { IconCheck, IconLanguage, IconUserCircle } from "@tabler/icons-react";
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "./dropdown-menu";
|
|
5
|
+
/**
|
|
6
|
+
* The portal chrome, extracted from the portal AppShell: sticky white
|
|
7
|
+
* top bar with the brand lockup (logo, divider, the word Portal),
|
|
8
|
+
* desktop navigation inside the bar, an icon-only language menu with
|
|
9
|
+
* the current language checked, an icon-only account menu, and a fixed
|
|
10
|
+
* bottom nav on phones whose columns follow the item count. Content
|
|
11
|
+
* sits in a max-w-5xl column with room for the bottom nav. All state
|
|
12
|
+
* (who is signed in, which nav applies) stays in the application.
|
|
13
|
+
*/
|
|
14
|
+
export function PortalShell({ logoSrc, logoAlt = "PPI Jepang", brandWord = "Portal", homeHref = "#", navItems, languages, languageLabel = "Bahasa", accountItems, accountLabel = "Menu akun", viewSwitch, banner, skipLabel = "Lewati ke konten", helpText, legalItems = [], children, className, }) {
|
|
15
|
+
const navList = (mobile) => (_jsx("nav", { "aria-label": "Portal", className: mobile ? "fixed inset-x-0 bottom-0 z-40 border-t border-brand-border bg-card pb-[env(safe-area-inset-bottom)] md:hidden" : "hidden md:block", children: _jsx("ul", { className: mobile ? "grid" : "flex items-center gap-1", style: mobile ? { gridTemplateColumns: "repeat(" + navItems.length + ", minmax(0, 1fr))" } : undefined, children: navItems.map((item) => (_jsx("li", { children: _jsxs("a", { href: item.href, onClick: item.onClick, "aria-current": item.current ? "page" : undefined, className: mobile
|
|
16
|
+
? cn("flex min-h-14 w-full flex-col items-center justify-center gap-0.5 px-1 text-xs font-semibold focus-visible:outline-2 focus-visible:outline-brand-red", item.current ? "text-brand-red" : "text-brand-muted")
|
|
17
|
+
: cn("flex min-h-11 items-center rounded-md px-3 text-sm font-semibold focus-visible:outline-2 focus-visible:outline-brand-red", item.current ? "text-brand-red" : "text-brand-muted hover:text-brand-ink"), children: [mobile ? item.icon : null, item.label] }) }, item.label))) }) }));
|
|
18
|
+
return (_jsxs("div", { className: cn("min-h-dvh bg-brand-bg text-brand-ink", className), children: [_jsx("a", { href: "#konten", className: "sr-only focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:z-50 focus:rounded-md focus:bg-card focus:px-4 focus:py-3 focus:font-semibold focus:outline-2 focus:outline-brand-red", children: skipLabel }), _jsx("header", { className: "sticky top-0 z-40 border-b border-brand-border bg-card", children: _jsxs("div", { className: "mx-auto flex min-h-14 max-w-5xl flex-wrap items-center justify-between gap-x-2 gap-y-1 px-4", children: [_jsxs("a", { href: homeHref, className: "flex min-h-11 min-w-0 items-center gap-2 rounded-md text-brand-ink no-underline focus-visible:outline-2 focus-visible:outline-brand-red", children: [_jsx("img", { className: "block h-10 w-auto shrink-0", src: logoSrc, alt: logoAlt, width: 58, height: 40 }), _jsx("span", { className: "border-l border-brand-border pl-3 text-xl leading-7 font-bold whitespace-nowrap", children: brandWord })] }), navList(false), _jsxs("div", { className: "ms-auto flex min-w-0 shrink-0 items-center gap-1", children: [viewSwitch, _jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { "aria-label": languageLabel, className: "flex size-11 shrink-0 items-center justify-center rounded-md text-brand-muted hover:text-brand-ink focus-visible:outline-2 focus-visible:outline-brand-red data-[state=open]:text-brand-ink", children: _jsx(IconLanguage, { className: "size-5", stroke: 1.75, "aria-hidden": "true" }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-52", children: languages.map((language) => (_jsxs(DropdownMenuItem, { className: "min-h-11 justify-between", onSelect: language.onSelect, children: [language.label, language.current ? _jsx(IconCheck, { className: "size-4", "aria-hidden": "true" }) : null] }, language.label))) })] }), _jsxs(DropdownMenu, { modal: false, children: [_jsx(DropdownMenuTrigger, { "aria-label": accountLabel, className: "flex size-11 shrink-0 items-center justify-center rounded-md text-brand-ink focus-visible:outline-2 focus-visible:outline-brand-red data-[state=open]:bg-secondary", children: _jsx(IconUserCircle, { className: "size-6", stroke: 1.75, "aria-hidden": "true" }) }), _jsx(DropdownMenuContent, { align: "end", className: "w-56", children: accountItems.map((item) => (_jsx(DropdownMenuItem, { className: "min-h-11", asChild: true, children: _jsx("a", { href: item.href, children: item.label }) }, item.label))) })] })] })] }) }), banner, _jsxs("main", { id: "konten", className: "mx-auto max-w-5xl px-4 pt-6 pb-28 md:pb-10", children: [children, (helpText || legalItems.length > 0) && (_jsxs("footer", { className: "mx-auto mt-8 max-w-3xl border-t border-brand-border pt-4 text-sm", children: [helpText && _jsx("p", { className: "text-brand-muted", children: helpText }), legalItems.length > 0 && (_jsx("nav", { "aria-label": "Legal", className: "mt-1 flex flex-wrap items-center gap-x-5 gap-y-1", children: legalItems.map((item) => (_jsx("a", { href: item.href, className: "flex min-h-11 items-center font-normal text-brand-muted underline-offset-4 hover:text-brand-ink hover:underline focus-visible:outline-2 focus-visible:outline-brand-red", children: item.label }, item.label))) }))] }))] }), navList(true)] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface PostcodeCandidate {
|
|
2
|
+
label: string;
|
|
3
|
+
onSelect: () => void;
|
|
4
|
+
}
|
|
5
|
+
export interface PostcodeFieldProps {
|
|
6
|
+
value: string;
|
|
7
|
+
onChange: (value: string) => void;
|
|
8
|
+
/** Runs the address lookup for the current code; the data source stays in the application. */
|
|
9
|
+
onSearch: () => void;
|
|
10
|
+
searching?: boolean;
|
|
11
|
+
searchLabel?: string;
|
|
12
|
+
/** Line under the field: a resolved address, an instruction, or an error. */
|
|
13
|
+
statusText?: string;
|
|
14
|
+
/** When a lookup returns several places, each becomes a switchable candidate button. */
|
|
15
|
+
candidates?: PostcodeCandidate[];
|
|
16
|
+
placeholder?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Japanese postcode entry with address lookup, extracted from the
|
|
21
|
+
* wizard PostcodeField: the code input and a "Cari alamat" button on
|
|
22
|
+
* one row (both field height), a status line, and candidate buttons
|
|
23
|
+
* when the code maps to more than one place. The lookup itself is
|
|
24
|
+
* the application's; this is the presentation and the states.
|
|
25
|
+
*/
|
|
26
|
+
export declare function PostcodeField({ value, onChange, onSearch, searching, searchLabel, statusText, candidates, placeholder, className, }: PostcodeFieldProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { Button } from "./button";
|
|
4
|
+
import { Input } from "./input";
|
|
5
|
+
/**
|
|
6
|
+
* Japanese postcode entry with address lookup, extracted from the
|
|
7
|
+
* wizard PostcodeField: the code input and a "Cari alamat" button on
|
|
8
|
+
* one row (both field height), a status line, and candidate buttons
|
|
9
|
+
* when the code maps to more than one place. The lookup itself is
|
|
10
|
+
* the application's; this is the presentation and the states.
|
|
11
|
+
*/
|
|
12
|
+
export function PostcodeField({ value, onChange, onSearch, searching = false, searchLabel = "Cari alamat", statusText, candidates = [], placeholder = "700-0001", className, }) {
|
|
13
|
+
return (_jsxs("div", { className: cn("grid gap-2", className), children: [_jsxs("div", { className: "flex flex-wrap gap-2", children: [_jsx(Input, { value: value, onChange: (event) => onChange(event.target.value), inputMode: "numeric", autoComplete: "postal-code", placeholder: placeholder, className: "min-h-11 min-w-0 flex-1" }), _jsx(Button, { type: "button", variant: "outline", className: "min-h-11", disabled: searching, onClick: onSearch, children: searchLabel })] }), statusText && (_jsx("p", { role: "status", className: "text-sm text-brand-muted", children: statusText })), candidates.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: candidates.map((candidate) => (_jsx(Button, { type: "button", variant: "outline", className: "min-h-11", onClick: candidate.onSelect, children: candidate.label }, candidate.label))) }))] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Progress as ProgressPrimitive } from "radix-ui";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
function Progress({ className, value, ...props }) {
|
|
6
|
+
return (_jsx(ProgressPrimitive.Root, { "data-slot": "progress", className: cn("relative h-2 w-full overflow-hidden rounded-full bg-secondary", className), ...props, children: _jsx(ProgressPrimitive.Indicator, { "data-slot": "progress-indicator", className: "h-full flex-1 bg-brand-red transition-all", style: { transform: "translateX(-" + (100 - (value || 0)) + "%)" } }) }));
|
|
7
|
+
}
|
|
8
|
+
export { Progress };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
|
|
3
|
+
declare function RadioGroup({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Root>): React.JSX.Element;
|
|
4
|
+
declare function RadioGroupItem({ className, ...props }: React.ComponentProps<typeof RadioGroupPrimitive.Item>): React.JSX.Element;
|
|
5
|
+
export { RadioGroup, RadioGroupItem };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
function RadioGroup({ className, ...props }) {
|
|
6
|
+
return _jsx(RadioGroupPrimitive.Root, { "data-slot": "radio-group", className: cn("grid gap-3", className), ...props });
|
|
7
|
+
}
|
|
8
|
+
function RadioGroupItem({ className, ...props }) {
|
|
9
|
+
return (_jsx(RadioGroupPrimitive.Item, { "data-slot": "radio-group-item", className: cn("aspect-square size-4 shrink-0 rounded-full 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", className), ...props, children: _jsx(RadioGroupPrimitive.Indicator, { "data-slot": "radio-group-indicator", className: "relative flex items-center justify-center", children: _jsx("span", { className: "absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary" }) }) }));
|
|
10
|
+
}
|
|
11
|
+
export { RadioGroup, RadioGroupItem };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface ReviewPanelProps {
|
|
3
|
+
title: string;
|
|
4
|
+
children: React.ReactNode;
|
|
5
|
+
editable?: boolean;
|
|
6
|
+
onEdit?: () => void;
|
|
7
|
+
editLabel?: string;
|
|
8
|
+
/** Incomplete sections get the heavy destructive border and a "Belum lengkap" line. */
|
|
9
|
+
incomplete?: boolean;
|
|
10
|
+
incompleteLabel?: string;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* One review panel, extracted from the wizard Review: a FormPanel
|
|
15
|
+
* with an h2 title, the section's data as content, and the shared
|
|
16
|
+
* "Edit bagian ini" outline action in the footer when the section
|
|
17
|
+
* may be edited. This is the panel the profile's read-first
|
|
18
|
+
* sections mirror.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ReviewPanel({ title, children, editable, onEdit, editLabel, incomplete, incompleteLabel, className, }: ReviewPanelProps): React.JSX.Element;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { IconPencil } from "@tabler/icons-react";
|
|
3
|
+
import { Button } from "./button";
|
|
4
|
+
import { FormPanel } from "./form-panel";
|
|
5
|
+
/**
|
|
6
|
+
* One review panel, extracted from the wizard Review: a FormPanel
|
|
7
|
+
* with an h2 title, the section's data as content, and the shared
|
|
8
|
+
* "Edit bagian ini" outline action in the footer when the section
|
|
9
|
+
* may be edited. This is the panel the profile's read-first
|
|
10
|
+
* sections mirror.
|
|
11
|
+
*/
|
|
12
|
+
export function ReviewPanel({ title, children, editable = true, onEdit, editLabel = "Edit bagian ini", incomplete = false, incompleteLabel = "Belum lengkap", className, }) {
|
|
13
|
+
return (_jsxs(FormPanel, { title: title, titleAs: "h2", className: incomplete ? "border-2 border-destructive" : className, footer: editable ? (_jsxs(Button, { type: "button", variant: "outline", onClick: onEdit, children: [_jsx(IconPencil, { size: 16, "aria-hidden": "true" }), editLabel] })) : undefined, children: [incomplete && _jsx("p", { className: "mb-3 text-sm font-semibold text-destructive", children: incompleteLabel }), children] }));
|
|
14
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type SavingStatus = "idle" | "saving" | "saved" | "error";
|
|
3
|
+
export interface SavingIndicatorProps {
|
|
4
|
+
status: SavingStatus;
|
|
5
|
+
locale?: "id" | "en";
|
|
6
|
+
className?: string;
|
|
7
|
+
savingText?: string;
|
|
8
|
+
savedText?: string;
|
|
9
|
+
errorText?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Shared accessible saving indicator component.
|
|
13
|
+
* Tailwind only, respects motion-reduce, role="status", screen-reader text, aria-busy.
|
|
14
|
+
*/
|
|
15
|
+
export declare function SavingIndicator({ status, locale, className, savingText, savedText, errorText, }: SavingIndicatorProps): React.JSX.Element | null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { IconCheck, IconAlertCircle } from "@tabler/icons-react";
|
|
3
|
+
import { Spinner } from "./spinner";
|
|
4
|
+
import { cn } from "../lib/utils";
|
|
5
|
+
/**
|
|
6
|
+
* Shared accessible saving indicator component.
|
|
7
|
+
* Tailwind only, respects motion-reduce, role="status", screen-reader text, aria-busy.
|
|
8
|
+
*/
|
|
9
|
+
export function SavingIndicator({ status, locale = "id", className, savingText, savedText, errorText, }) {
|
|
10
|
+
if (status === "idle")
|
|
11
|
+
return null;
|
|
12
|
+
const defaultSaving = locale === "en" ? "Saving…" : "Menyimpan…";
|
|
13
|
+
const defaultSaved = locale === "en" ? "Saved" : "Tersimpan";
|
|
14
|
+
const defaultError = locale === "en" ? "Failed to save" : "Gagal menyimpan";
|
|
15
|
+
const isSaving = status === "saving";
|
|
16
|
+
const isSaved = status === "saved";
|
|
17
|
+
const isError = status === "error";
|
|
18
|
+
return (_jsxs("div", { role: "status", "aria-busy": isSaving, "data-saving-indicator": status, className: cn("inline-flex items-center gap-1.5 text-xs font-medium transition-opacity motion-reduce:transition-none", isSaving && "text-brand-muted", isSaved && "text-success", isError && "text-brand-red", className), children: [isSaving && _jsx(Spinner, { size: "xs", decorative: true }), isSaved && _jsx(IconCheck, { className: "size-3.5 text-success", "aria-hidden": "true" }), isError && _jsx(IconAlertCircle, { className: "size-3.5 text-brand-red", "aria-hidden": "true" }), _jsxs("span", { children: [isSaving && (savingText ?? defaultSaving), isSaved && (savedText ?? defaultSaved), isError && (errorText ?? defaultError)] })] }));
|
|
19
|
+
}
|