@exxatdesignux/ui 0.0.5 → 0.0.7
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/bin/init.mjs +29 -0
- package/package.json +7 -2
- package/template/.nvmrc +1 -0
- package/template/.prettierignore +7 -0
- package/template/.prettierrc +11 -0
- package/template/AGENTS.md +485 -0
- package/template/Logo/Exxat_Prism.svg +39 -0
- package/template/Logo/Exxat_one.svg +36 -0
- package/template/README.md +58 -0
- package/template/app/(app)/compliance/page.tsx +10 -0
- package/template/app/(app)/dashboard/loading.tsx +18 -0
- package/template/app/(app)/dashboard/page.tsx +36 -0
- package/template/app/(app)/data-list/[id]/page.tsx +28 -0
- package/template/app/(app)/data-list/new/page.tsx +31 -0
- package/template/app/(app)/data-list/page.tsx +10 -0
- package/template/app/(app)/error.tsx +43 -0
- package/template/app/(app)/help/page.tsx +34 -0
- package/template/app/(app)/layout.tsx +54 -0
- package/template/app/(app)/loading.tsx +18 -0
- package/template/app/(app)/question-bank/page.tsx +10 -0
- package/template/app/(app)/rotations/page.tsx +15 -0
- package/template/app/(app)/settings/page.tsx +17 -0
- package/template/app/(app)/sites/all/page.tsx +13 -0
- package/template/app/(app)/team/page.tsx +10 -0
- package/template/app/favicon.ico +0 -0
- package/template/app/globals.css +1811 -0
- package/template/app/layout.tsx +95 -0
- package/template/app/page.tsx +9 -0
- package/template/components/.gitkeep +0 -0
- package/template/components/app-sidebar-dynamic.tsx +15 -0
- package/template/components/app-sidebar.tsx +901 -0
- package/template/components/ask-leo-composer.tsx +216 -0
- package/template/components/ask-leo-sidebar.tsx +509 -0
- package/template/components/chart-area-interactive.tsx +293 -0
- package/template/components/charts-overview.tsx +2321 -0
- package/template/components/command-menu-01.tsx +133 -0
- package/template/components/command-menu-02.tsx +386 -0
- package/template/components/command-menu.tsx +182 -0
- package/template/components/compliance-board-view.tsx +134 -0
- package/template/components/compliance-client.tsx +92 -0
- package/template/components/compliance-list-view.tsx +59 -0
- package/template/components/compliance-page-header.tsx +89 -0
- package/template/components/compliance-table.tsx +525 -0
- package/template/components/dashboard-onboarding-gallery.tsx +13 -0
- package/template/components/dashboard-onboarding.tsx +21 -0
- package/template/components/dashboard-promo-banner.tsx +67 -0
- package/template/components/dashboard-quota-progress-card.tsx +369 -0
- package/template/components/dashboard-report-charts.tsx +69 -0
- package/template/components/dashboard-section-heading.tsx +68 -0
- package/template/components/dashboard-tabs.tsx +598 -0
- package/template/components/data-list-client.tsx +239 -0
- package/template/components/data-list-table-cells.test.tsx +22 -0
- package/template/components/data-list-table-cells.tsx +173 -0
- package/template/components/data-list-table.tsx +879 -0
- package/template/components/data-table/filter-date-calendar.tsx +38 -0
- package/template/components/data-table/filter-text-value-input.tsx +77 -0
- package/template/components/data-table/index.tsx +1612 -0
- package/template/components/data-table/pagination.tsx +256 -0
- package/template/components/data-table/types.ts +91 -0
- package/template/components/data-table/use-table-state.ts +566 -0
- package/template/components/data-view-dashboard-charts-compliance.tsx +960 -0
- package/template/components/data-view-dashboard-charts-team.tsx +968 -0
- package/template/components/data-view-dashboard-charts.tsx +1668 -0
- package/template/components/data-views/board-card-primitives.tsx +93 -0
- package/template/components/data-views/index.ts +41 -0
- package/template/components/data-views/list-page-board-card.tsx +192 -0
- package/template/components/data-views/list-page-board-template.tsx +122 -0
- package/template/components/data-views/placement-board-card.tsx +262 -0
- package/template/components/export-drawer.tsx +375 -0
- package/template/components/exxat-product-logo.tsx +453 -0
- package/template/components/form-layout-01.tsx +131 -0
- package/template/components/getting-started.tsx +625 -0
- package/template/components/key-metrics.tsx +920 -0
- package/template/components/leo-insight-indicator.tsx +364 -0
- package/template/components/leo-typing-dots.tsx +121 -0
- package/template/components/list-hub-status-badge.tsx +51 -0
- package/template/components/list-page-dashboard-charts.tsx +18 -0
- package/template/components/nav-documents.tsx +89 -0
- package/template/components/nav-main.tsx +58 -0
- package/template/components/nav-secondary.tsx +64 -0
- package/template/components/nav-user.tsx +190 -0
- package/template/components/new-placement-back-btn.tsx +28 -0
- package/template/components/new-placement-form.tsx +1066 -0
- package/template/components/onboarding/index.ts +4 -0
- package/template/components/onboarding/onboarding-01.tsx +7 -0
- package/template/components/onboarding/onboarding-02.tsx +7 -0
- package/template/components/onboarding/onboarding-03.tsx +7 -0
- package/template/components/onboarding/onboarding-04.tsx +7 -0
- package/template/components/page-header.tsx +57 -0
- package/template/components/placement-detail.tsx +438 -0
- package/template/components/placements-board-view.tsx +404 -0
- package/template/components/placements-list-view.tsx +285 -0
- package/template/components/placements-page-header.tsx +160 -0
- package/template/components/placements-table-columns.tsx +639 -0
- package/template/components/product-switcher.tsx +116 -0
- package/template/components/question-bank-board-view.tsx +205 -0
- package/template/components/question-bank-client.tsx +77 -0
- package/template/components/question-bank-list-view.tsx +59 -0
- package/template/components/question-bank-page-header.tsx +89 -0
- package/template/components/question-bank-table.tsx +586 -0
- package/template/components/rotations-empty-state.tsx +47 -0
- package/template/components/rotations-panel-activator.tsx +8 -0
- package/template/components/secondary-nav.tsx +394 -0
- package/template/components/secondary-panel.tsx +239 -0
- package/template/components/section-cards.tsx +106 -0
- package/template/components/settings-appearance-card.tsx +424 -0
- package/template/components/settings-client.tsx +537 -0
- package/template/components/settings-form-row.tsx +42 -0
- package/template/components/sidebar-auto-collapse.tsx +23 -0
- package/template/components/sidebar-auto-open.tsx +18 -0
- package/template/components/sidebar-shell.tsx +37 -0
- package/template/components/site-header.tsx +93 -0
- package/template/components/sites-all-client.tsx +154 -0
- package/template/components/sites-board-view.tsx +67 -0
- package/template/components/sites-list-view.tsx +47 -0
- package/template/components/sites-table.tsx +312 -0
- package/template/components/system-banner-slot.tsx +66 -0
- package/template/components/table-properties/column-row.tsx +90 -0
- package/template/components/table-properties/draggable-list.ts +49 -0
- package/template/components/table-properties/drawer-button.tsx +231 -0
- package/template/components/table-properties/drawer.tsx +1102 -0
- package/template/components/table-properties/filter-card.tsx +251 -0
- package/template/components/table-properties/index.ts +22 -0
- package/template/components/table-properties/sort-card.tsx +59 -0
- package/template/components/table-properties/types.ts +124 -0
- package/template/components/task-list-panel.tsx +98 -0
- package/template/components/task-priority-badge.tsx +28 -0
- package/template/components/team-board-view.tsx +114 -0
- package/template/components/team-client.tsx +93 -0
- package/template/components/team-list-view.tsx +62 -0
- package/template/components/team-page-header.tsx +92 -0
- package/template/components/team-table.tsx +525 -0
- package/template/components/templates/list-page.tsx +576 -0
- package/template/components/templates/primary-page-template.tsx +56 -0
- package/template/components/theme-color-sync.tsx +32 -0
- package/template/components/theme-provider.tsx +71 -0
- package/template/components/tinted-icon-disc.tsx +53 -0
- package/template/components/ui/ai-thinking-surface.tsx +121 -0
- package/template/components/ui/avatar.tsx +1 -0
- package/template/components/ui/badge.tsx +1 -0
- package/template/components/ui/banner.tsx +1 -0
- package/template/components/ui/breadcrumb.tsx +1 -0
- package/template/components/ui/button.tsx +1 -0
- package/template/components/ui/calendar.tsx +1 -0
- package/template/components/ui/card.tsx +1 -0
- package/template/components/ui/chart.tsx +1 -0
- package/template/components/ui/checkbox.tsx +1 -0
- package/template/components/ui/coach-mark.tsx +1 -0
- package/template/components/ui/collapsible.tsx +1 -0
- package/template/components/ui/command.tsx +1 -0
- package/template/components/ui/date-picker-field.tsx +1 -0
- package/template/components/ui/dialog.tsx +1 -0
- package/template/components/ui/dot-pattern.tsx +159 -0
- package/template/components/ui/drag-handle-grip.tsx +1 -0
- package/template/components/ui/drawer.tsx +1 -0
- package/template/components/ui/dropdown-menu.tsx +1 -0
- package/template/components/ui/field.tsx +1 -0
- package/template/components/ui/form.tsx +1 -0
- package/template/components/ui/input-group.tsx +1 -0
- package/template/components/ui/input-mask.tsx +1 -0
- package/template/components/ui/input.tsx +1 -0
- package/template/components/ui/kbd.tsx +1 -0
- package/template/components/ui/label.tsx +1 -0
- package/template/components/ui/leo-icon.tsx +726 -0
- package/template/components/ui/payment-card-fields.tsx +1 -0
- package/template/components/ui/popover.tsx +1 -0
- package/template/components/ui/radio-group.tsx +1 -0
- package/template/components/ui/select.tsx +1 -0
- package/template/components/ui/selection-tile-grid.tsx +1 -0
- package/template/components/ui/separator.tsx +1 -0
- package/template/components/ui/sheet.tsx +1 -0
- package/template/components/ui/sidebar.tsx +1 -0
- package/template/components/ui/skeleton.tsx +1 -0
- package/template/components/ui/sonner.tsx +1 -0
- package/template/components/ui/status-badge.tsx +1 -0
- package/template/components/ui/table.tsx +1 -0
- package/template/components/ui/tabs.tsx +1 -0
- package/template/components/ui/textarea.tsx +1 -0
- package/template/components/ui/tip.tsx +1 -0
- package/template/components/ui/toggle-group.tsx +1 -0
- package/template/components/ui/toggle-switch.tsx +1 -0
- package/template/components/ui/toggle.tsx +1 -0
- package/template/components/ui/tooltip.tsx +1 -0
- package/template/components/ui/view-segmented-control.tsx +1 -0
- package/template/components.json +27 -0
- package/template/contexts/chart-variant-context.tsx +35 -0
- package/template/contexts/command-menu-context.tsx +28 -0
- package/template/contexts/dashboard-view-context.tsx +35 -0
- package/template/contexts/product-context.tsx +38 -0
- package/template/contexts/system-banner-context.tsx +127 -0
- package/template/docs/command-menu-pattern.md +45 -0
- package/template/docs/data-views-pattern.md +160 -0
- package/template/ecosystem.config.cjs +20 -0
- package/template/eslint.config.mjs +18 -0
- package/template/fontawesome-subset.manifest.json +190 -0
- package/template/hooks/.gitkeep +0 -0
- package/template/hooks/use-app-theme.ts +1 -0
- package/template/hooks/use-coach-mark.ts +1 -0
- package/template/hooks/use-mobile.ts +1 -0
- package/template/hooks/use-mod-key-label.ts +1 -0
- package/template/lib/.gitkeep +0 -0
- package/template/lib/ask-leo-route-context.ts +133 -0
- package/template/lib/chart-keyboard-selection.test.ts +20 -0
- package/template/lib/chart-keyboard-selection.ts +17 -0
- package/template/lib/chart-line-dash.ts +16 -0
- package/template/lib/coach-mark-registry.ts +68 -0
- package/template/lib/command-menu-config.ts +127 -0
- package/template/lib/command-menu-search-data.ts +44 -0
- package/template/lib/conditional-rule-match.ts +32 -0
- package/template/lib/dashboard-customize-coach-mark.ts +18 -0
- package/template/lib/dashboard-layout-merge.ts +63 -0
- package/template/lib/data-list-display-options.ts +35 -0
- package/template/lib/data-list-persistence.ts +280 -0
- package/template/lib/data-list-view-surface.ts +58 -0
- package/template/lib/data-list-view.ts +29 -0
- package/template/lib/data-view-dashboard-storage.ts +101 -0
- package/template/lib/date-filter.ts +8 -0
- package/template/lib/dev-log.test.ts +28 -0
- package/template/lib/dev-log.ts +8 -0
- package/template/lib/editable-target.ts +10 -0
- package/template/lib/floating-sheet-panel.ts +72 -0
- package/template/lib/initials-from-name.ts +7 -0
- package/template/lib/list-page-table-properties.ts +52 -0
- package/template/lib/list-status-badges.ts +168 -0
- package/template/lib/logo-dev.ts +12 -0
- package/template/lib/mock/compliance-kpi.ts +61 -0
- package/template/lib/mock/compliance.ts +146 -0
- package/template/lib/mock/dashboard.ts +105 -0
- package/template/lib/mock/navigation.tsx +231 -0
- package/template/lib/mock/placements-kpi.ts +134 -0
- package/template/lib/mock/placements.ts +183 -0
- package/template/lib/mock/question-bank-kpi.ts +61 -0
- package/template/lib/mock/question-bank.ts +142 -0
- package/template/lib/mock/sites-directory.ts +16 -0
- package/template/lib/mock/sites-kpi.ts +25 -0
- package/template/lib/mock/team-kpi.ts +60 -0
- package/template/lib/mock/team.ts +118 -0
- package/template/lib/motion-ui.ts +17 -0
- package/template/lib/placement-board-card-layout.ts +79 -0
- package/template/lib/placement-lifecycle.ts +5 -0
- package/template/lib/row-height.ts +10 -0
- package/template/lib/stock-portrait.ts +11 -0
- package/template/lib/utils.test.ts +13 -0
- package/template/lib/utils.ts +1 -0
- package/template/next.config.mjs +15 -0
- package/template/package.json +83 -0
- package/template/postcss.config.mjs +8 -0
- package/template/public/.gitkeep +0 -0
- package/template/public/Illustration/Rotation.svg +74 -0
- package/template/public/avatars/user.svg +11 -0
- package/template/public/favicon/favicon.ico +0 -0
- package/template/public/favicon.ico +0 -0
- package/template/public/logos/exxat-one.svg +36 -0
- package/template/public/logos/exxat-prism.svg +39 -0
- package/template/public/mock-schools/emory.svg +4 -0
- package/template/public/mock-schools/rush.svg +4 -0
- package/template/scripts/fontawesome-subset-audit.mjs +190 -0
- package/template/scripts/pm2-startup-macos.sh +13 -0
- package/template/skills-lock.json +10 -0
- package/template/stores/app-store.ts +33 -0
- package/template/tests/setup.ts +1 -0
- package/template/tsconfig.json +35 -0
- package/template/types/react-payment-inputs.d.ts +19 -0
- package/template/vitest.config.ts +18 -0
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SettingsClient — System banner + coach mark utilities.
|
|
5
|
+
*
|
|
6
|
+
* Provides:
|
|
7
|
+
* • System banner: edit copy, variant, action link, dismissibility; Apply / Discard / Reset
|
|
8
|
+
* • Coach marks: list flows, reset, preview on target page
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as React from "react"
|
|
12
|
+
import { useRouter } from "next/navigation"
|
|
13
|
+
import { Button } from "@/components/ui/button"
|
|
14
|
+
import { Badge } from "@/components/ui/badge"
|
|
15
|
+
import { Input } from "@/components/ui/input"
|
|
16
|
+
import { Textarea } from "@/components/ui/textarea"
|
|
17
|
+
import { ToggleSwitch } from "@/components/ui/toggle-switch"
|
|
18
|
+
import {
|
|
19
|
+
Select,
|
|
20
|
+
SelectContent,
|
|
21
|
+
SelectItem,
|
|
22
|
+
SelectTrigger,
|
|
23
|
+
SelectValue,
|
|
24
|
+
} from "@/components/ui/select"
|
|
25
|
+
import { SystemBanner } from "@/components/ui/banner"
|
|
26
|
+
import { Tip } from "@/components/ui/tip"
|
|
27
|
+
import { cn } from "@/lib/utils"
|
|
28
|
+
import { COACH_MARK_FLOWS, type CoachMarkFlowDef } from "@/lib/coach-mark-registry"
|
|
29
|
+
import {
|
|
30
|
+
resetCoachMarkFlow,
|
|
31
|
+
resetAllCoachMarks,
|
|
32
|
+
} from "@/hooks/use-coach-mark"
|
|
33
|
+
import {
|
|
34
|
+
useSystemBanner,
|
|
35
|
+
type SystemBannerConfig,
|
|
36
|
+
type SystemBannerVariant,
|
|
37
|
+
} from "@/contexts/system-banner-context"
|
|
38
|
+
import { SettingsAppearanceCard } from "@/components/settings-appearance-card"
|
|
39
|
+
import { SettingsFormRow } from "@/components/settings-form-row"
|
|
40
|
+
import { FieldGroup } from "@/components/ui/field"
|
|
41
|
+
import { FilterTextValueInput } from "@/components/data-table/filter-text-value-input"
|
|
42
|
+
|
|
43
|
+
const SYSTEM_BANNER_VARIANTS: SystemBannerVariant[] = [
|
|
44
|
+
"info",
|
|
45
|
+
"warning",
|
|
46
|
+
"error",
|
|
47
|
+
"success",
|
|
48
|
+
"promo",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
/* ── Helpers ─────────────────────────────────────────────────────────────── */
|
|
52
|
+
|
|
53
|
+
const STORAGE_PREFIX = "exxat-coach-mark:"
|
|
54
|
+
|
|
55
|
+
function isFlowDismissed(flowId: string): boolean {
|
|
56
|
+
if (typeof window === "undefined") return false
|
|
57
|
+
try {
|
|
58
|
+
return localStorage.getItem(`${STORAGE_PREFIX}${flowId}`) === "dismissed"
|
|
59
|
+
} catch {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ── Flow card ──────────────────────────────────────────────────────────── */
|
|
65
|
+
|
|
66
|
+
function FlowCard({
|
|
67
|
+
flow,
|
|
68
|
+
dismissed,
|
|
69
|
+
onReset,
|
|
70
|
+
onPreview,
|
|
71
|
+
}: {
|
|
72
|
+
flow: CoachMarkFlowDef
|
|
73
|
+
dismissed: boolean
|
|
74
|
+
onReset: () => void
|
|
75
|
+
onPreview: () => void
|
|
76
|
+
}) {
|
|
77
|
+
return (
|
|
78
|
+
<div className="flex items-start gap-4 rounded-lg border border-border bg-card p-4 transition-colors hover:bg-interactive-hover-soft">
|
|
79
|
+
{/* Icon */}
|
|
80
|
+
<span
|
|
81
|
+
className={cn(
|
|
82
|
+
"shrink-0 flex h-10 w-10 items-center justify-center rounded-lg text-sm",
|
|
83
|
+
dismissed
|
|
84
|
+
? "bg-muted text-muted-foreground"
|
|
85
|
+
: "bg-brand/12 text-brand"
|
|
86
|
+
)}
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
>
|
|
89
|
+
<i className={cn("fa-light", dismissed ? "fa-circle-check" : "fa-route")} />
|
|
90
|
+
</span>
|
|
91
|
+
|
|
92
|
+
{/* Content */}
|
|
93
|
+
<div className="flex-1 min-w-0">
|
|
94
|
+
<div className="flex items-center gap-2 mb-1">
|
|
95
|
+
<h3 className="text-sm font-semibold text-foreground">{flow.name}</h3>
|
|
96
|
+
<Badge variant={dismissed ? "secondary" : "default"} className="text-xs h-5">
|
|
97
|
+
{dismissed ? "Completed" : "Active"}
|
|
98
|
+
</Badge>
|
|
99
|
+
</div>
|
|
100
|
+
<p className="text-xs text-muted-foreground leading-relaxed mb-2">
|
|
101
|
+
{flow.description}
|
|
102
|
+
</p>
|
|
103
|
+
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
104
|
+
<span className="flex items-center gap-1">
|
|
105
|
+
<i className="fa-light fa-file text-xs" aria-hidden="true" />
|
|
106
|
+
{flow.page}
|
|
107
|
+
</span>
|
|
108
|
+
<span className="flex items-center gap-1">
|
|
109
|
+
<i className="fa-light fa-list-ol text-xs" aria-hidden="true" />
|
|
110
|
+
{flow.stepCount} steps
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
{/* Actions */}
|
|
116
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
117
|
+
<Tip side="bottom" label="Reset and replay this tour">
|
|
118
|
+
<Button
|
|
119
|
+
type="button"
|
|
120
|
+
size="sm"
|
|
121
|
+
variant="outline"
|
|
122
|
+
onClick={onReset}
|
|
123
|
+
className="h-8 text-xs gap-1.5"
|
|
124
|
+
>
|
|
125
|
+
<i className="fa-light fa-rotate-left text-xs" aria-hidden="true" />
|
|
126
|
+
Reset
|
|
127
|
+
</Button>
|
|
128
|
+
</Tip>
|
|
129
|
+
<Tip side="bottom" label={`Go to ${flow.page} to preview`}>
|
|
130
|
+
<Button
|
|
131
|
+
type="button"
|
|
132
|
+
size="sm"
|
|
133
|
+
variant="ghost"
|
|
134
|
+
onClick={onPreview}
|
|
135
|
+
className="h-8 text-xs gap-1.5"
|
|
136
|
+
>
|
|
137
|
+
<i className="fa-light fa-arrow-up-right text-xs" aria-hidden="true" />
|
|
138
|
+
Preview
|
|
139
|
+
</Button>
|
|
140
|
+
</Tip>
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* ── System banner editor ───────────────────────────────────────────────── */
|
|
147
|
+
|
|
148
|
+
function SystemBannerSettingsCard() {
|
|
149
|
+
const { config, applyConfig, reset } = useSystemBanner()
|
|
150
|
+
const [draft, setDraft] = React.useState<SystemBannerConfig>(() => ({ ...config }))
|
|
151
|
+
const dirty = React.useMemo(() => JSON.stringify(draft) !== JSON.stringify(config), [draft, config])
|
|
152
|
+
|
|
153
|
+
React.useEffect(() => {
|
|
154
|
+
setDraft({ ...config })
|
|
155
|
+
}, [config])
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<section id="banner" className="scroll-mt-20">
|
|
159
|
+
<header className="mb-8 space-y-1">
|
|
160
|
+
<h2 className="text-lg font-semibold text-foreground">System banner & alerts</h2>
|
|
161
|
+
<p className="text-sm text-muted-foreground">
|
|
162
|
+
Top-of-app strip for maintenance, promos, and notices. Stored locally as{" "}
|
|
163
|
+
<span className="font-mono text-xs">exxat:system-banner-config</span>.
|
|
164
|
+
</p>
|
|
165
|
+
</header>
|
|
166
|
+
<div className="flex flex-col gap-8">
|
|
167
|
+
<FieldGroup className="gap-8">
|
|
168
|
+
<SettingsFormRow
|
|
169
|
+
label="Show banner"
|
|
170
|
+
description="When off, the strip stays hidden until you turn it back on."
|
|
171
|
+
htmlFor="banner-enabled"
|
|
172
|
+
>
|
|
173
|
+
<ToggleSwitch
|
|
174
|
+
id="banner-enabled"
|
|
175
|
+
checked={draft.enabled}
|
|
176
|
+
onChange={(enabled) => setDraft((d) => ({ ...d, enabled }))}
|
|
177
|
+
/>
|
|
178
|
+
</SettingsFormRow>
|
|
179
|
+
|
|
180
|
+
<SettingsFormRow
|
|
181
|
+
label="Variant"
|
|
182
|
+
description="Info, warning, error, success, or promo styling for the strip."
|
|
183
|
+
htmlFor="banner-variant"
|
|
184
|
+
>
|
|
185
|
+
<Select
|
|
186
|
+
value={draft.variant}
|
|
187
|
+
onValueChange={(v) =>
|
|
188
|
+
setDraft((d) => ({ ...d, variant: v as SystemBannerVariant }))
|
|
189
|
+
}
|
|
190
|
+
>
|
|
191
|
+
<SelectTrigger id="banner-variant" className="w-full">
|
|
192
|
+
<SelectValue placeholder="Variant" />
|
|
193
|
+
</SelectTrigger>
|
|
194
|
+
<SelectContent>
|
|
195
|
+
{SYSTEM_BANNER_VARIANTS.map((v) => (
|
|
196
|
+
<SelectItem key={v} value={v}>
|
|
197
|
+
{v.charAt(0).toUpperCase() + v.slice(1)}
|
|
198
|
+
</SelectItem>
|
|
199
|
+
))}
|
|
200
|
+
</SelectContent>
|
|
201
|
+
</Select>
|
|
202
|
+
</SettingsFormRow>
|
|
203
|
+
|
|
204
|
+
<SettingsFormRow
|
|
205
|
+
label="Emphasis"
|
|
206
|
+
description="Prominent uses a solid dark fill; subtle uses a soft tinted background."
|
|
207
|
+
htmlFor="banner-emphasis"
|
|
208
|
+
>
|
|
209
|
+
<Select
|
|
210
|
+
value={draft.emphasis}
|
|
211
|
+
onValueChange={(v) =>
|
|
212
|
+
setDraft((d) => ({ ...d, emphasis: v as "prominent" | "subtle" }))
|
|
213
|
+
}
|
|
214
|
+
>
|
|
215
|
+
<SelectTrigger id="banner-emphasis" className="w-full">
|
|
216
|
+
<SelectValue placeholder="Emphasis" />
|
|
217
|
+
</SelectTrigger>
|
|
218
|
+
<SelectContent>
|
|
219
|
+
<SelectItem value="prominent">Prominent</SelectItem>
|
|
220
|
+
<SelectItem value="subtle">Subtle</SelectItem>
|
|
221
|
+
</SelectContent>
|
|
222
|
+
</Select>
|
|
223
|
+
</SettingsFormRow>
|
|
224
|
+
|
|
225
|
+
<SettingsFormRow label="Title" description="Short headline shown in the strip." htmlFor="banner-title">
|
|
226
|
+
<Input
|
|
227
|
+
id="banner-title"
|
|
228
|
+
value={draft.title}
|
|
229
|
+
onChange={(e) => setDraft((d) => ({ ...d, title: e.target.value }))}
|
|
230
|
+
placeholder="Short headline"
|
|
231
|
+
autoComplete="off"
|
|
232
|
+
className="w-full"
|
|
233
|
+
/>
|
|
234
|
+
</SettingsFormRow>
|
|
235
|
+
|
|
236
|
+
<SettingsFormRow
|
|
237
|
+
label="Message"
|
|
238
|
+
description="Supporting line under the title (optional if you only want a title)."
|
|
239
|
+
htmlFor="banner-message"
|
|
240
|
+
>
|
|
241
|
+
<Textarea
|
|
242
|
+
id="banner-message"
|
|
243
|
+
value={draft.message}
|
|
244
|
+
onChange={(e) => setDraft((d) => ({ ...d, message: e.target.value }))}
|
|
245
|
+
placeholder="Supporting line shown under the title"
|
|
246
|
+
rows={3}
|
|
247
|
+
className="min-h-[4.5rem] w-full resize-y"
|
|
248
|
+
/>
|
|
249
|
+
</SettingsFormRow>
|
|
250
|
+
|
|
251
|
+
<SettingsFormRow
|
|
252
|
+
label="Action label"
|
|
253
|
+
description="Optional button text, e.g. Learn more."
|
|
254
|
+
htmlFor="banner-action-label"
|
|
255
|
+
>
|
|
256
|
+
<Input
|
|
257
|
+
id="banner-action-label"
|
|
258
|
+
value={draft.actionLabel ?? ""}
|
|
259
|
+
onChange={(e) =>
|
|
260
|
+
setDraft((d) => ({
|
|
261
|
+
...d,
|
|
262
|
+
actionLabel: e.target.value.trim() ? e.target.value : undefined,
|
|
263
|
+
}))
|
|
264
|
+
}
|
|
265
|
+
placeholder="Learn more"
|
|
266
|
+
autoComplete="off"
|
|
267
|
+
className="w-full"
|
|
268
|
+
/>
|
|
269
|
+
</SettingsFormRow>
|
|
270
|
+
|
|
271
|
+
<SettingsFormRow
|
|
272
|
+
label="Action URL"
|
|
273
|
+
description="Where the action button navigates (https link or #)."
|
|
274
|
+
htmlFor="banner-action-href"
|
|
275
|
+
>
|
|
276
|
+
<Input
|
|
277
|
+
id="banner-action-href"
|
|
278
|
+
value={draft.actionHref ?? ""}
|
|
279
|
+
onChange={(e) =>
|
|
280
|
+
setDraft((d) => ({
|
|
281
|
+
...d,
|
|
282
|
+
actionHref: e.target.value.trim() ? e.target.value : undefined,
|
|
283
|
+
}))
|
|
284
|
+
}
|
|
285
|
+
placeholder="https://… or #"
|
|
286
|
+
autoComplete="off"
|
|
287
|
+
className="w-full"
|
|
288
|
+
/>
|
|
289
|
+
</SettingsFormRow>
|
|
290
|
+
|
|
291
|
+
<SettingsFormRow
|
|
292
|
+
label="Allow dismiss"
|
|
293
|
+
description="Shows a close control; dismissing turns the banner off."
|
|
294
|
+
htmlFor="banner-dismissible"
|
|
295
|
+
>
|
|
296
|
+
<ToggleSwitch
|
|
297
|
+
id="banner-dismissible"
|
|
298
|
+
checked={draft.dismissible}
|
|
299
|
+
onChange={(dismissible) => setDraft((d) => ({ ...d, dismissible }))}
|
|
300
|
+
/>
|
|
301
|
+
</SettingsFormRow>
|
|
302
|
+
|
|
303
|
+
<SettingsFormRow label="Preview" description="Approximates the live strip above your sidebar.">
|
|
304
|
+
<div className="rounded-lg border border-dashed border-border bg-muted/20 p-3 w-full">
|
|
305
|
+
{draft.enabled ? (
|
|
306
|
+
<SystemBanner
|
|
307
|
+
variant={draft.variant}
|
|
308
|
+
emphasis={draft.emphasis}
|
|
309
|
+
title={draft.title || undefined}
|
|
310
|
+
dismissible={false}
|
|
311
|
+
action={
|
|
312
|
+
draft.actionLabel
|
|
313
|
+
? { label: draft.actionLabel, href: draft.actionHref || "#" }
|
|
314
|
+
: undefined
|
|
315
|
+
}
|
|
316
|
+
>
|
|
317
|
+
{draft.message || (draft.title ? "" : "…")}
|
|
318
|
+
</SystemBanner>
|
|
319
|
+
) : (
|
|
320
|
+
<p className="text-sm text-muted-foreground py-2 text-center">
|
|
321
|
+
Banner hidden — turn on “Show banner” to preview.
|
|
322
|
+
</p>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
</SettingsFormRow>
|
|
326
|
+
</FieldGroup>
|
|
327
|
+
|
|
328
|
+
<div className="flex flex-wrap items-center gap-2 pt-8">
|
|
329
|
+
<Button
|
|
330
|
+
type="button"
|
|
331
|
+
size="sm"
|
|
332
|
+
disabled={!dirty}
|
|
333
|
+
onClick={() => applyConfig({ ...draft })}
|
|
334
|
+
>
|
|
335
|
+
Apply to system banner
|
|
336
|
+
</Button>
|
|
337
|
+
<Button
|
|
338
|
+
type="button"
|
|
339
|
+
size="sm"
|
|
340
|
+
variant="outline"
|
|
341
|
+
disabled={!dirty}
|
|
342
|
+
onClick={() => setDraft({ ...config })}
|
|
343
|
+
>
|
|
344
|
+
Discard changes
|
|
345
|
+
</Button>
|
|
346
|
+
<Tip side="top" label="Restore the default copy and turn the banner on">
|
|
347
|
+
<Button type="button" size="sm" variant="ghost" onClick={() => reset()}>
|
|
348
|
+
Reset to defaults
|
|
349
|
+
</Button>
|
|
350
|
+
</Tip>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</section>
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* ── Main component ─────────────────────────────────────────────────────── */
|
|
358
|
+
|
|
359
|
+
function buildFlowStatuses() {
|
|
360
|
+
return COACH_MARK_FLOWS.map((f) => ({
|
|
361
|
+
flow: f,
|
|
362
|
+
dismissed: isFlowDismissed(f.id),
|
|
363
|
+
}))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function SettingsClient() {
|
|
367
|
+
const router = useRouter()
|
|
368
|
+
|
|
369
|
+
const [demoPhone, setDemoPhone] = React.useState("")
|
|
370
|
+
const [demoZip, setDemoZip] = React.useState("")
|
|
371
|
+
const [demoDate, setDemoDate] = React.useState("")
|
|
372
|
+
|
|
373
|
+
/** SSR + first client paint: all undismissed so markup matches; sync from storage after mount. */
|
|
374
|
+
const [flowStatuses, setFlowStatuses] = React.useState(() =>
|
|
375
|
+
COACH_MARK_FLOWS.map((f) => ({ flow: f, dismissed: false })),
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
React.useEffect(() => {
|
|
379
|
+
setFlowStatuses(buildFlowStatuses())
|
|
380
|
+
}, [])
|
|
381
|
+
|
|
382
|
+
const completedCount = flowStatuses.filter((f) => f.dismissed).length
|
|
383
|
+
const totalCount = flowStatuses.length
|
|
384
|
+
|
|
385
|
+
function refreshFlowStatuses() {
|
|
386
|
+
setFlowStatuses(buildFlowStatuses())
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function handleResetFlow(flowId: string) {
|
|
390
|
+
resetCoachMarkFlow(flowId)
|
|
391
|
+
refreshFlowStatuses()
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function handleResetAll() {
|
|
395
|
+
resetAllCoachMarks()
|
|
396
|
+
refreshFlowStatuses()
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function handlePreview(pageUrl: string, flowId: string) {
|
|
400
|
+
resetCoachMarkFlow(flowId)
|
|
401
|
+
refreshFlowStatuses()
|
|
402
|
+
router.push(pageUrl)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<div className="flex w-full min-w-0 flex-col">
|
|
407
|
+
<div>
|
|
408
|
+
<h1
|
|
409
|
+
className="text-2xl font-semibold tracking-tight leading-tight text-foreground"
|
|
410
|
+
style={{ fontFamily: "var(--font-heading)" }}
|
|
411
|
+
>
|
|
412
|
+
Settings
|
|
413
|
+
</h1>
|
|
414
|
+
<p className="mt-1 text-sm leading-relaxed text-muted-foreground">
|
|
415
|
+
Preferences and tools for this workspace. Display options apply on this device and are stored in your browser.
|
|
416
|
+
</p>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<div className="mt-10 flex flex-col gap-20">
|
|
420
|
+
<section id="account" className="scroll-mt-20">
|
|
421
|
+
<header className="mb-6 space-y-1">
|
|
422
|
+
<h2 className="text-lg font-semibold text-foreground">Account</h2>
|
|
423
|
+
<p className="text-sm text-muted-foreground">
|
|
424
|
+
Profile, billing, and notification shortcuts still live in the sidebar avatar menu.
|
|
425
|
+
</p>
|
|
426
|
+
</header>
|
|
427
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
428
|
+
Use <span className="font-medium text-foreground">your avatar</span> at the bottom of the left sidebar for
|
|
429
|
+
account details, billing, and alerts. This page focuses on app-wide display and onboarding preferences.
|
|
430
|
+
</p>
|
|
431
|
+
</section>
|
|
432
|
+
|
|
433
|
+
<SettingsAppearanceCard />
|
|
434
|
+
|
|
435
|
+
<section id="input-formats" className="scroll-mt-20">
|
|
436
|
+
<header className="mb-6 space-y-1">
|
|
437
|
+
<h2 className="text-lg font-semibold text-foreground">Input formats</h2>
|
|
438
|
+
<p className="text-sm text-muted-foreground">
|
|
439
|
+
Phone, ZIP, and date masks match table filters and properties drawer fields. Values here are only a local
|
|
440
|
+
preview.
|
|
441
|
+
</p>
|
|
442
|
+
</header>
|
|
443
|
+
<FieldGroup>
|
|
444
|
+
<SettingsFormRow
|
|
445
|
+
label="Phone"
|
|
446
|
+
description="US NANP display; digits-only matching in filters."
|
|
447
|
+
htmlFor="settings-demo-phone"
|
|
448
|
+
>
|
|
449
|
+
<FilterTextValueInput
|
|
450
|
+
id="settings-demo-phone"
|
|
451
|
+
mask="phone"
|
|
452
|
+
aria-label="Phone (masked preview)"
|
|
453
|
+
placeholder="(555) 555-5555"
|
|
454
|
+
value={demoPhone}
|
|
455
|
+
onValueChange={setDemoPhone}
|
|
456
|
+
className="w-full max-w-sm"
|
|
457
|
+
/>
|
|
458
|
+
</SettingsFormRow>
|
|
459
|
+
<SettingsFormRow
|
|
460
|
+
label="ZIP"
|
|
461
|
+
description="ZIP or ZIP+4."
|
|
462
|
+
htmlFor="settings-demo-zip"
|
|
463
|
+
>
|
|
464
|
+
<FilterTextValueInput
|
|
465
|
+
id="settings-demo-zip"
|
|
466
|
+
mask="zip"
|
|
467
|
+
aria-label="ZIP (masked preview)"
|
|
468
|
+
placeholder="12345 or 12345-6789"
|
|
469
|
+
value={demoZip}
|
|
470
|
+
onValueChange={setDemoZip}
|
|
471
|
+
className="w-full max-w-xs"
|
|
472
|
+
/>
|
|
473
|
+
</SettingsFormRow>
|
|
474
|
+
<SettingsFormRow
|
|
475
|
+
label="Date"
|
|
476
|
+
description="MM/DD/YYYY display; validate separately if you persist it."
|
|
477
|
+
htmlFor="settings-demo-date"
|
|
478
|
+
>
|
|
479
|
+
<FilterTextValueInput
|
|
480
|
+
id="settings-demo-date"
|
|
481
|
+
mask="dateMDY"
|
|
482
|
+
aria-label="Date (masked preview)"
|
|
483
|
+
placeholder="MM/DD/YYYY"
|
|
484
|
+
value={demoDate}
|
|
485
|
+
onValueChange={setDemoDate}
|
|
486
|
+
className="w-full max-w-xs"
|
|
487
|
+
/>
|
|
488
|
+
</SettingsFormRow>
|
|
489
|
+
</FieldGroup>
|
|
490
|
+
</section>
|
|
491
|
+
|
|
492
|
+
<SystemBannerSettingsCard />
|
|
493
|
+
|
|
494
|
+
<section id="tours" className="scroll-mt-20">
|
|
495
|
+
<header className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
496
|
+
<div className="min-w-0 space-y-1">
|
|
497
|
+
<h2 className="text-lg font-semibold text-foreground">Guided tours</h2>
|
|
498
|
+
<p className="text-sm text-muted-foreground">
|
|
499
|
+
Tours start when you first open a page, highlight one control at a time, and stop after you finish or
|
|
500
|
+
skip. They won’t repeat until you reset them here.
|
|
501
|
+
</p>
|
|
502
|
+
</div>
|
|
503
|
+
<div className="flex shrink-0 items-center gap-3">
|
|
504
|
+
<span className="tabular-nums text-xs text-muted-foreground">
|
|
505
|
+
{completedCount}/{totalCount} completed
|
|
506
|
+
</span>
|
|
507
|
+
<Tip side="bottom" label="Reset all tours so they replay on next visit">
|
|
508
|
+
<Button
|
|
509
|
+
type="button"
|
|
510
|
+
size="sm"
|
|
511
|
+
variant="outline"
|
|
512
|
+
onClick={handleResetAll}
|
|
513
|
+
className="h-8 gap-1.5 text-xs"
|
|
514
|
+
>
|
|
515
|
+
<i className="fa-light fa-rotate-left text-xs" aria-hidden="true" />
|
|
516
|
+
Reset all
|
|
517
|
+
</Button>
|
|
518
|
+
</Tip>
|
|
519
|
+
</div>
|
|
520
|
+
</header>
|
|
521
|
+
|
|
522
|
+
<div className="flex flex-col gap-3">
|
|
523
|
+
{flowStatuses.map(({ flow, dismissed }) => (
|
|
524
|
+
<FlowCard
|
|
525
|
+
key={flow.id}
|
|
526
|
+
flow={flow}
|
|
527
|
+
dismissed={dismissed}
|
|
528
|
+
onReset={() => handleResetFlow(flow.id)}
|
|
529
|
+
onPreview={() => handlePreview(flow.pageUrl, flow.id)}
|
|
530
|
+
/>
|
|
531
|
+
))}
|
|
532
|
+
</div>
|
|
533
|
+
</section>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
)
|
|
537
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { Label } from "@/components/ui/label"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Two-column settings row: label + helper on the left, controls on the right.
|
|
9
|
+
*/
|
|
10
|
+
export function SettingsFormRow({
|
|
11
|
+
label,
|
|
12
|
+
description,
|
|
13
|
+
htmlFor,
|
|
14
|
+
children,
|
|
15
|
+
className,
|
|
16
|
+
}: {
|
|
17
|
+
label: string
|
|
18
|
+
description?: string
|
|
19
|
+
htmlFor?: string
|
|
20
|
+
children: React.ReactNode
|
|
21
|
+
className?: string
|
|
22
|
+
}) {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className={cn(
|
|
26
|
+
"grid grid-cols-1 gap-3 sm:gap-4 lg:grid-cols-[minmax(0,220px)_minmax(0,1fr)] lg:gap-10 lg:items-start",
|
|
27
|
+
"border-b border-border/70 pb-8 last:border-0 last:pb-0",
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
<div className="space-y-1 lg:pt-1 text-start">
|
|
32
|
+
<Label htmlFor={htmlFor} className="text-sm font-medium text-foreground">
|
|
33
|
+
{label}
|
|
34
|
+
</Label>
|
|
35
|
+
{description ? (
|
|
36
|
+
<p className="text-xs text-muted-foreground leading-snug text-start">{description}</p>
|
|
37
|
+
) : null}
|
|
38
|
+
</div>
|
|
39
|
+
<div className="min-w-0 space-y-2">{children}</div>
|
|
40
|
+
</div>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react"
|
|
4
|
+
import { useSidebar } from "@/components/ui/sidebar"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Collapses the sidebar on mount. Restores previous state on unmount.
|
|
8
|
+
* Used on the new-placement page so the sidebar collapses when entering
|
|
9
|
+
* and reverts to whatever it was when leaving.
|
|
10
|
+
*/
|
|
11
|
+
export function SidebarAutoCollapse() {
|
|
12
|
+
const { open, setOpen } = useSidebar()
|
|
13
|
+
const prevOpen = useRef(open)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
prevOpen.current = open
|
|
17
|
+
setOpen(false)
|
|
18
|
+
return () => { setOpen(prevOpen.current) }
|
|
19
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
20
|
+
}, [])
|
|
21
|
+
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef } from "react"
|
|
4
|
+
import { useSidebar } from "@/components/ui/sidebar"
|
|
5
|
+
|
|
6
|
+
/** Reopens the sidebar once when the data-list page mounts (after returning from new-placement). */
|
|
7
|
+
export function SidebarAutoOpen() {
|
|
8
|
+
const { setOpen } = useSidebar()
|
|
9
|
+
const ran = useRef(false)
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!ran.current) {
|
|
12
|
+
ran.current = true
|
|
13
|
+
setOpen(true)
|
|
14
|
+
}
|
|
15
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
16
|
+
}, [])
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SidebarShell — SidebarProvider with layout-aware widths.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as React from "react"
|
|
8
|
+
import { SidebarProvider } from "@/components/ui/sidebar"
|
|
9
|
+
|
|
10
|
+
export function SidebarShell({
|
|
11
|
+
children,
|
|
12
|
+
headerHeight = "calc(var(--spacing) * 12)",
|
|
13
|
+
defaultOpen = true,
|
|
14
|
+
wrapperClassName,
|
|
15
|
+
}: {
|
|
16
|
+
children: React.ReactNode
|
|
17
|
+
headerHeight?: string
|
|
18
|
+
defaultOpen?: boolean
|
|
19
|
+
/** Extra classes on the SidebarProvider wrapper div */
|
|
20
|
+
wrapperClassName?: string
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<SidebarProvider
|
|
24
|
+
defaultOpen={defaultOpen}
|
|
25
|
+
className={wrapperClassName}
|
|
26
|
+
style={
|
|
27
|
+
{
|
|
28
|
+
"--sidebar-width": "16rem",
|
|
29
|
+
"--sidebar-width-icon": "3rem",
|
|
30
|
+
"--header-height": headerHeight,
|
|
31
|
+
} as React.CSSProperties
|
|
32
|
+
}
|
|
33
|
+
>
|
|
34
|
+
{children}
|
|
35
|
+
</SidebarProvider>
|
|
36
|
+
)
|
|
37
|
+
}
|