@exxatdesignux/ui 0.2.9 → 0.2.11
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/consumer-extras/cursor-skills/exxat-card-vs-list-rows/SKILL.md +20 -0
- package/consumer-extras/cursor-skills/exxat-collaboration-access/SKILL.md +33 -0
- package/consumer-extras/cursor-skills/exxat-dedicated-search-surfaces/SKILL.md +45 -0
- package/consumer-extras/cursor-skills/exxat-drawer-vs-dialog/SKILL.md +20 -0
- package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +31 -5
- package/consumer-extras/cursor-skills/exxat-kpi-max-four/SKILL.md +19 -0
- package/consumer-extras/cursor-skills/exxat-kpi-trends/SKILL.md +27 -0
- package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +1 -0
- package/consumer-extras/patterns/collaboration-access-pattern.md +114 -0
- package/consumer-extras/patterns/data-views-pattern.md +12 -4
- package/package.json +4 -1
- package/src/components/ui/banner.tsx +20 -7
- package/src/components/ui/date-picker-field.tsx +3 -3
- package/src/components/ui/dropdown-menu.tsx +17 -6
- package/src/components/ui/input-group.tsx +1 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/separator.tsx +2 -2
- package/src/components/ui/sidebar.tsx +31 -3
- package/src/components/ui/textarea.tsx +1 -1
- package/src/globals.css +0 -1
- package/src/index.ts +1 -0
- package/src/lib/date-filter.ts +13 -4
- package/src/lib/dropdown-menu-surface.ts +13 -0
- package/template/.claude/skills/exxat-ds-skill/SKILL.md +27 -9
- package/template/.cursor/rules/exxat-data-tables.mdc +1 -0
- package/template/.nvmrc +1 -1
- package/template/AGENTS.md +82 -27
- package/template/app/(app)/examples/page.tsx +2 -1
- package/template/app/(app)/help/page.tsx +6 -0
- package/template/app/(app)/layout.tsx +7 -4
- package/template/app/(app)/question-bank/find/page.tsx +12 -0
- package/template/app/(app)/question-bank/layout.tsx +46 -0
- package/template/app/(app)/question-bank/library/page.tsx +11 -0
- package/template/app/(app)/question-bank/list/page.tsx +12 -0
- package/template/app/(app)/question-bank/page.tsx +4 -3
- package/template/app/globals.css +1 -2
- package/template/components/app-sidebar.tsx +51 -13
- package/template/components/ask-leo-composer.tsx +173 -45
- package/template/components/ask-leo-sidebar.tsx +9 -1
- package/template/components/chart-area-interactive.tsx +3 -13
- package/template/components/charts-overview.tsx +33 -6
- package/template/components/collaboration-access-flow.tsx +144 -0
- package/template/components/compliance-page-header.tsx +1 -1
- package/template/components/compliance-table.tsx +2 -2
- package/template/components/dashboard-tabs.tsx +4 -3
- package/template/components/data-list-table-cells.tsx +1 -1
- package/template/components/data-list-table.tsx +1 -1
- package/template/components/data-table/index.tsx +5 -5
- package/template/components/data-table/use-table-state.ts +18 -2
- package/template/components/data-view-dashboard-charts-compliance.tsx +8 -5
- package/template/components/data-view-dashboard-charts-team.tsx +8 -5
- package/template/components/data-view-dashboard-charts.tsx +62 -227
- package/template/components/dedicated-search-recents.tsx +96 -0
- package/template/components/dedicated-search-url-composer.tsx +112 -0
- package/template/components/getting-started.tsx +1 -1
- package/template/components/hub-tree-panel-view.tsx +10 -26
- package/template/components/invite-collaborators-drawer.tsx +453 -0
- package/template/components/key-metrics.tsx +54 -8
- package/template/components/nav-documents.tsx +1 -1
- package/template/components/new-placement-form.tsx +3 -3
- package/template/components/page-header.tsx +76 -59
- package/template/components/placements-board-view.tsx +3 -3
- package/template/components/placements-page-header.tsx +1 -1
- package/template/components/placements-table-columns.tsx +3 -2
- package/template/components/product-switcher.tsx +0 -1
- package/template/components/question-bank-board-view.tsx +35 -47
- package/template/components/question-bank-client.tsx +293 -81
- package/template/components/question-bank-dashboard-charts.tsx +174 -0
- package/template/components/question-bank-favorite-button.tsx +46 -0
- package/template/components/question-bank-hub-client.tsx +436 -0
- package/template/components/question-bank-list-view.tsx +26 -19
- package/template/components/question-bank-new-folder-sheet.tsx +56 -42
- package/template/components/question-bank-os-folder-view.tsx +3 -14
- package/template/components/question-bank-page-header.tsx +85 -53
- package/template/components/question-bank-panel-activator.tsx +3 -4
- package/template/components/question-bank-secondary-nav.tsx +523 -65
- package/template/components/question-bank-table.tsx +125 -343
- package/template/components/secondary-panel.tsx +130 -63
- package/template/components/settings-client.tsx +3 -1
- package/template/components/sidebar-shell.tsx +2 -0
- package/template/components/sites-all-client.tsx +1 -1
- package/template/components/sites-table.tsx +1 -1
- package/template/components/system-banner-slot.tsx +2 -1
- package/template/components/table-properties/drawer.tsx +3 -3
- package/template/components/table-properties/sort-card.tsx +1 -1
- package/template/components/team-page-header.tsx +1 -1
- package/template/components/team-table.tsx +8 -4
- package/template/components/templates/dedicated-search-landing-template.tsx +58 -0
- package/template/components/templates/dedicated-search-results-template.tsx +19 -0
- package/template/components/templates/discovery-hub-template.tsx +273 -0
- package/template/components/templates/list-page.tsx +11 -4
- package/template/components/templates/nested-secondary-panel-shell.tsx +57 -0
- package/template/components/templates/secondary-panel-hub-template.tsx +54 -0
- package/template/docs/card-vs-rows-pattern.md +36 -0
- package/template/docs/collaboration-access-pattern.md +114 -0
- package/template/docs/data-views-pattern.md +12 -4
- package/template/docs/drawer-vs-dialog-pattern.md +50 -0
- package/template/docs/kpi-strip-max-four-pattern.md +29 -0
- package/template/docs/kpi-trend-pattern.md +43 -0
- package/template/fontawesome-subset.manifest.json +2 -2
- package/template/hooks/use-location-hash.ts +14 -8
- package/template/hooks/use-secondary-panel-hub-nav.ts +98 -0
- package/template/lib/ask-leo-route-context.ts +24 -0
- package/template/lib/collaborator-access.ts +92 -0
- package/template/lib/command-menu-config.ts +8 -1
- package/template/lib/command-menu-search-data.ts +11 -8
- package/template/lib/data-list-display-options.ts +1 -1
- package/template/lib/data-view-dashboard-placements-layout.ts +215 -0
- package/template/lib/date-filter.ts +1 -0
- package/template/lib/dedicated-search-recents.ts +76 -0
- package/template/lib/dedicated-search-url.ts +23 -0
- package/template/lib/discovery-hub.ts +15 -0
- package/template/lib/list-status-badges.ts +1 -21
- package/template/lib/mock/navigation.tsx +4 -2
- package/template/lib/mock/placements.ts +9 -9
- package/template/lib/mock/question-bank-folders.ts +7 -0
- package/template/lib/mock/question-bank-header-collaborators.ts +45 -5
- package/template/lib/mock/question-bank-inspector.ts +1 -2
- package/template/lib/mock/question-bank-kpi.ts +38 -26
- package/template/lib/mock/question-bank.ts +43 -16
- package/template/lib/question-bank-dedicated-search.ts +19 -0
- package/template/lib/question-bank-hub-search.ts +90 -0
- package/template/lib/question-bank-nav.ts +322 -6
- package/template/lib/question-bank-recent-searches.ts +22 -0
- package/template/package.json +1 -2
|
@@ -67,12 +67,16 @@ import {
|
|
|
67
67
|
KEY_METRICS_KPI_COUNT_DEFAULT,
|
|
68
68
|
KEY_METRICS_KPI_COUNT_MAX,
|
|
69
69
|
KEY_METRICS_KPI_COUNT_MIN,
|
|
70
|
-
mergeDashboardLayoutGeneric,
|
|
71
70
|
} from "@/lib/dashboard-layout-merge"
|
|
72
71
|
import {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
type ChartType,
|
|
73
|
+
type DashboardCardDef,
|
|
74
|
+
KEY_METRICS_CARD_ID,
|
|
75
|
+
ALL_DASHBOARD_CARDS,
|
|
76
|
+
DEFAULT_SPANS,
|
|
77
|
+
DEFAULT_CHART_TYPES,
|
|
78
|
+
applyVisibleReorder,
|
|
79
|
+
} from "@/lib/data-view-dashboard-placements-layout"
|
|
76
80
|
import { cn } from "@/lib/utils"
|
|
77
81
|
import {
|
|
78
82
|
ChartContainer,
|
|
@@ -87,6 +91,19 @@ import {
|
|
|
87
91
|
CHART_KBD_ACTIVE_PIE_SHAPE,
|
|
88
92
|
} from "@/lib/chart-keyboard-selection"
|
|
89
93
|
|
|
94
|
+
export type { ChartType, ChartTypeOption, DashboardCardDef, DashboardLayout } from "@/lib/data-view-dashboard-placements-layout"
|
|
95
|
+
export {
|
|
96
|
+
KEY_METRICS_CARD_ID,
|
|
97
|
+
ALL_DASHBOARD_CARDS,
|
|
98
|
+
DEFAULT_VISIBLE_CARDS,
|
|
99
|
+
DEFAULT_SPANS,
|
|
100
|
+
DEFAULT_CHART_TYPES,
|
|
101
|
+
loadDashboardLayout,
|
|
102
|
+
mergeDashboardLayout,
|
|
103
|
+
saveDashboardLayout,
|
|
104
|
+
applyVisibleReorder,
|
|
105
|
+
} from "@/lib/data-view-dashboard-placements-layout"
|
|
106
|
+
|
|
90
107
|
/* ── Chart colour tokens ───────────────────────────────────────────────── */
|
|
91
108
|
|
|
92
109
|
const STATUS_COLORS: Record<string, string> = {
|
|
@@ -117,32 +134,16 @@ const READINESS_COLORS: Record<string, string> = {
|
|
|
117
134
|
const BAR_CFG: ChartConfig = { value: { label: "Placements", color: "var(--primary)" } }
|
|
118
135
|
const AREA_CFG: ChartConfig = { count: { label: "Starting", color: "var(--color-chart-1)" } }
|
|
119
136
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
export type ChartType = "bar" | "horizontal-bar" | "pie" | "area" | "line" | "radial" | "stacked-bar"
|
|
123
|
-
|
|
124
|
-
export interface ChartTypeOption {
|
|
125
|
-
type: ChartType
|
|
126
|
-
label: string
|
|
127
|
-
icon: string
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/* ── Card definitions ──────────────────────────────────────────────────── */
|
|
131
|
-
|
|
132
|
-
export interface DashboardCardDef {
|
|
133
|
-
id: string
|
|
134
|
-
title: string
|
|
135
|
-
description: string
|
|
136
|
-
/** Default grid column span: 1 = half width, 2 = full width */
|
|
137
|
-
defaultSpan: 1 | 2
|
|
138
|
-
/** Default chart type (unused when chartTypes is empty) */
|
|
139
|
-
defaultChartType: ChartType
|
|
140
|
-
/** Available chart types; empty = KPI / non-chart block (no type switcher) */
|
|
141
|
-
chartTypes: ChartTypeOption[]
|
|
142
|
-
}
|
|
137
|
+
const CHART_MARGIN = { top: 8, right: 8, left: 0, bottom: 0 } as const
|
|
138
|
+
const CHART_MARGIN_HORIZONTAL = { top: 8, right: 8, left: 4, bottom: 0 } as const
|
|
143
139
|
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
const PALETTE_COLORS = [
|
|
141
|
+
"var(--color-chart-1)",
|
|
142
|
+
"var(--color-chart-2)",
|
|
143
|
+
"var(--color-chart-3)",
|
|
144
|
+
"var(--color-chart-4)",
|
|
145
|
+
"var(--color-chart-5)",
|
|
146
|
+
] as const
|
|
146
147
|
|
|
147
148
|
/** Demo Leo “smart scan” copy per chart (swap for model output when wired). */
|
|
148
149
|
const PLACEMENTS_CHART_LEO_INSIGHTS: Partial<Record<string, ChartLeoInsight>> = {
|
|
@@ -173,181 +174,6 @@ const PLACEMENTS_CHART_LEO_INSIGHTS: Partial<Record<string, ChartLeoInsight>> =
|
|
|
173
174
|
},
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
export const ALL_DASHBOARD_CARDS: DashboardCardDef[] = [
|
|
177
|
-
{
|
|
178
|
-
id: KEY_METRICS_CARD_ID,
|
|
179
|
-
title: "Key metrics",
|
|
180
|
-
description: "Summary KPIs for filtered placements",
|
|
181
|
-
defaultSpan: 1,
|
|
182
|
-
defaultChartType: "bar",
|
|
183
|
-
chartTypes: [],
|
|
184
|
-
},
|
|
185
|
-
{
|
|
186
|
-
id: "status-pipeline",
|
|
187
|
-
title: "Status Pipeline",
|
|
188
|
-
description: "Where placements are in the workflow",
|
|
189
|
-
defaultSpan: 1,
|
|
190
|
-
defaultChartType: "bar",
|
|
191
|
-
chartTypes: [
|
|
192
|
-
{ type: "bar", label: "Bar", icon: "fa-light fa-chart-bar" },
|
|
193
|
-
{ type: "horizontal-bar", label: "Horizontal Bar", icon: "fa-light fa-chart-bar fa-rotate-90" },
|
|
194
|
-
{ type: "pie", label: "Donut", icon: "fa-light fa-chart-pie" },
|
|
195
|
-
],
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
id: "program-mix",
|
|
199
|
-
title: "Placements by Program",
|
|
200
|
-
description: "Distribution across active programs",
|
|
201
|
-
defaultSpan: 1,
|
|
202
|
-
defaultChartType: "pie",
|
|
203
|
-
chartTypes: [
|
|
204
|
-
{ type: "pie", label: "Donut", icon: "fa-light fa-chart-pie" },
|
|
205
|
-
{ type: "bar", label: "Bar", icon: "fa-light fa-chart-bar" },
|
|
206
|
-
{ type: "horizontal-bar", label: "Horizontal Bar", icon: "fa-light fa-chart-bar fa-rotate-90" },
|
|
207
|
-
],
|
|
208
|
-
},
|
|
209
|
-
{
|
|
210
|
-
id: "compliance-status",
|
|
211
|
-
title: "Compliance Status",
|
|
212
|
-
description: "Document readiness for upcoming placements",
|
|
213
|
-
defaultSpan: 1,
|
|
214
|
-
defaultChartType: "horizontal-bar",
|
|
215
|
-
chartTypes: [
|
|
216
|
-
{ type: "horizontal-bar", label: "Horizontal Bar", icon: "fa-light fa-chart-bar fa-rotate-90" },
|
|
217
|
-
{ type: "bar", label: "Bar", icon: "fa-light fa-chart-bar" },
|
|
218
|
-
{ type: "pie", label: "Donut", icon: "fa-light fa-chart-pie" },
|
|
219
|
-
],
|
|
220
|
-
},
|
|
221
|
-
{
|
|
222
|
-
id: "readiness-overview",
|
|
223
|
-
title: "Student Readiness",
|
|
224
|
-
description: "How prepared students are for their placements",
|
|
225
|
-
defaultSpan: 1,
|
|
226
|
-
defaultChartType: "bar",
|
|
227
|
-
chartTypes: [
|
|
228
|
-
{ type: "bar", label: "Bar", icon: "fa-light fa-chart-bar" },
|
|
229
|
-
{ type: "horizontal-bar", label: "Horizontal Bar", icon: "fa-light fa-chart-bar fa-rotate-90" },
|
|
230
|
-
{ type: "pie", label: "Donut", icon: "fa-light fa-chart-pie" },
|
|
231
|
-
],
|
|
232
|
-
},
|
|
233
|
-
{
|
|
234
|
-
id: "progress-tracker",
|
|
235
|
-
title: "Ongoing Progress",
|
|
236
|
-
description: "How far along each ongoing placement is",
|
|
237
|
-
defaultSpan: 2,
|
|
238
|
-
defaultChartType: "stacked-bar",
|
|
239
|
-
chartTypes: [
|
|
240
|
-
{ type: "stacked-bar", label: "Stacked Bar", icon: "fa-light fa-layer-group" },
|
|
241
|
-
{ type: "bar", label: "Bar", icon: "fa-light fa-chart-bar" },
|
|
242
|
-
],
|
|
243
|
-
},
|
|
244
|
-
{
|
|
245
|
-
id: "site-utilisation",
|
|
246
|
-
title: "Site Utilisation",
|
|
247
|
-
description: "Which clinical sites have the most placements",
|
|
248
|
-
defaultSpan: 1,
|
|
249
|
-
defaultChartType: "horizontal-bar",
|
|
250
|
-
chartTypes: [
|
|
251
|
-
{ type: "horizontal-bar", label: "Horizontal Bar", icon: "fa-light fa-chart-bar fa-rotate-90" },
|
|
252
|
-
{ type: "bar", label: "Bar", icon: "fa-light fa-chart-bar" },
|
|
253
|
-
{ type: "pie", label: "Donut", icon: "fa-light fa-chart-pie" },
|
|
254
|
-
],
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
id: "completion-outcomes",
|
|
258
|
-
title: "Completion Outcomes",
|
|
259
|
-
description: "Pass rate and average ratings for completed placements",
|
|
260
|
-
defaultSpan: 1,
|
|
261
|
-
defaultChartType: "radial",
|
|
262
|
-
chartTypes: [
|
|
263
|
-
{ type: "radial", label: "Radial", icon: "fa-light fa-circle-notch" },
|
|
264
|
-
{ type: "bar", label: "Bar", icon: "fa-light fa-chart-bar" },
|
|
265
|
-
],
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
id: "upcoming-timeline",
|
|
269
|
-
title: "Upcoming Start Dates",
|
|
270
|
-
description: "New placements starting in the next 8 weeks",
|
|
271
|
-
defaultSpan: 2,
|
|
272
|
-
defaultChartType: "area",
|
|
273
|
-
chartTypes: [
|
|
274
|
-
{ type: "area", label: "Area", icon: "fa-light fa-chart-area" },
|
|
275
|
-
{ type: "line", label: "Line", icon: "fa-light fa-chart-line" },
|
|
276
|
-
{ type: "bar", label: "Bar", icon: "fa-light fa-chart-bar" },
|
|
277
|
-
],
|
|
278
|
-
},
|
|
279
|
-
]
|
|
280
|
-
|
|
281
|
-
export const DEFAULT_VISIBLE_CARDS = ALL_DASHBOARD_CARDS.map(c => c.id)
|
|
282
|
-
export const DEFAULT_SPANS: Record<string, 1 | 2> = Object.fromEntries(ALL_DASHBOARD_CARDS.map(c => [c.id, c.defaultSpan]))
|
|
283
|
-
export const DEFAULT_CHART_TYPES: Record<string, ChartType> = Object.fromEntries(ALL_DASHBOARD_CARDS.map(c => [c.id, c.defaultChartType]))
|
|
284
|
-
|
|
285
|
-
/* ── Persistence (centralized bundle — see `lib/data-view-dashboard-storage`) ─ */
|
|
286
|
-
|
|
287
|
-
export interface DashboardLayout {
|
|
288
|
-
visible: string[]
|
|
289
|
-
order: string[]
|
|
290
|
-
spans?: Record<string, 1 | 2>
|
|
291
|
-
chartTypes?: Record<string, ChartType>
|
|
292
|
-
/** Key metrics card: show first N KPIs (1–4). */
|
|
293
|
-
keyMetricsKpiCount?: number
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export function loadDashboardLayout(): DashboardLayout | null {
|
|
297
|
-
const v = loadStoredDataViewLayout("placements")
|
|
298
|
-
if (!v) return null
|
|
299
|
-
return {
|
|
300
|
-
visible: v.visible,
|
|
301
|
-
order: v.order,
|
|
302
|
-
spans: v.spans,
|
|
303
|
-
chartTypes: v.chartTypes as Record<string, ChartType> | undefined,
|
|
304
|
-
keyMetricsKpiCount: v.keyMetricsKpiCount,
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/** Merge saved layout with defaults so every card id has span + chart type. */
|
|
309
|
-
export function mergeDashboardLayout(saved: DashboardLayout | null): DashboardLayout {
|
|
310
|
-
const defaults = {
|
|
311
|
-
visible: [...DEFAULT_VISIBLE_CARDS],
|
|
312
|
-
order: ALL_DASHBOARD_CARDS.map(c => c.id),
|
|
313
|
-
spans: { ...DEFAULT_SPANS },
|
|
314
|
-
chartTypes: { ...DEFAULT_CHART_TYPES } as Record<string, string>,
|
|
315
|
-
keyMetricsKpiCount: KEY_METRICS_KPI_COUNT_DEFAULT,
|
|
316
|
-
}
|
|
317
|
-
const ids = ALL_DASHBOARD_CARDS.map(c => c.id)
|
|
318
|
-
const m = mergeDashboardLayoutGeneric(saved, defaults, ids)
|
|
319
|
-
return {
|
|
320
|
-
visible: m.visible,
|
|
321
|
-
order: m.order,
|
|
322
|
-
spans: m.spans as Record<string, 1 | 2>,
|
|
323
|
-
chartTypes: m.chartTypes as Record<string, ChartType>,
|
|
324
|
-
keyMetricsKpiCount: m.keyMetricsKpiCount,
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
export function saveDashboardLayout(layout: DashboardLayout) {
|
|
329
|
-
saveStoredDataViewLayout("placements", {
|
|
330
|
-
visible: layout.visible,
|
|
331
|
-
order: layout.order,
|
|
332
|
-
spans: layout.spans,
|
|
333
|
-
chartTypes: layout.chartTypes as Record<string, string> | undefined,
|
|
334
|
-
keyMetricsKpiCount: layout.keyMetricsKpiCount,
|
|
335
|
-
})
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/** Rebuild full `cardOrder` after reordering only the visible subset (order of hidden ids is preserved). */
|
|
339
|
-
export function applyVisibleReorder(
|
|
340
|
-
fullOrder: string[],
|
|
341
|
-
visible: Set<string>,
|
|
342
|
-
newVisibleOrder: string[],
|
|
343
|
-
): string[] {
|
|
344
|
-
let vi = 0
|
|
345
|
-
return fullOrder.map(id => {
|
|
346
|
-
if (!visible.has(id)) return id
|
|
347
|
-
return newVisibleOrder[vi++]!
|
|
348
|
-
})
|
|
349
|
-
}
|
|
350
|
-
|
|
351
177
|
/* ── Individual chart renderers (ChartFigure + ChartDataTable from charts-overview) ─ */
|
|
352
178
|
/* Keyboard highlight: `CHART_KBD_*` — same ring-on-active pattern as `charts-overview`. */
|
|
353
179
|
|
|
@@ -378,7 +204,7 @@ function StatusPipelineChart({ rows, chartType }: { rows: Placement[]; chartType
|
|
|
378
204
|
<HBarChartRenderer data={data} colored activeIndex={activeIndex} />
|
|
379
205
|
) : (
|
|
380
206
|
<ChartContainer config={BAR_CFG} className="h-[220px] w-full">
|
|
381
|
-
<BarChart data={data} margin={
|
|
207
|
+
<BarChart data={data} margin={CHART_MARGIN}>
|
|
382
208
|
<CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
|
|
383
209
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
384
210
|
<YAxis allowDecimals={false} width={32} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -417,7 +243,11 @@ function ProgramMixChart({ rows, chartType }: { rows: Placement[]; chartType: Ch
|
|
|
417
243
|
.sort((a, b) => b.value - a.value)
|
|
418
244
|
}, [rows])
|
|
419
245
|
|
|
420
|
-
const colors =
|
|
246
|
+
const colors = PALETTE_COLORS
|
|
247
|
+
const coloredData = React.useMemo(
|
|
248
|
+
() => data.map((d, i) => ({ ...d, fill: colors[i % colors.length] })),
|
|
249
|
+
[data, colors],
|
|
250
|
+
)
|
|
421
251
|
|
|
422
252
|
if (data.length === 0) return <EmptyChart />
|
|
423
253
|
|
|
@@ -429,7 +259,7 @@ function ProgramMixChart({ rows, chartType }: { rows: Placement[]; chartType: Ch
|
|
|
429
259
|
<>
|
|
430
260
|
{chartType === "bar" ? (
|
|
431
261
|
<ChartContainer config={BAR_CFG} className="h-[220px] w-full">
|
|
432
|
-
<BarChart data={
|
|
262
|
+
<BarChart data={coloredData} margin={CHART_MARGIN}>
|
|
433
263
|
<CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
|
|
434
264
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
435
265
|
<YAxis allowDecimals={false} width={32} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -441,15 +271,15 @@ function ProgramMixChart({ rows, chartType }: { rows: Placement[]; chartType: Ch
|
|
|
441
271
|
activeBar={CHART_KBD_ACTIVE_BAR}
|
|
442
272
|
activeIndex={activeIndex ?? undefined}
|
|
443
273
|
>
|
|
444
|
-
{
|
|
445
|
-
<Cell key={i} fill={
|
|
274
|
+
{coloredData.map((d, i) => (
|
|
275
|
+
<Cell key={i} fill={d.fill} />
|
|
446
276
|
))}
|
|
447
277
|
</Bar>
|
|
448
278
|
</BarChart>
|
|
449
279
|
</ChartContainer>
|
|
450
280
|
) : chartType === "horizontal-bar" ? (
|
|
451
281
|
<ChartContainer config={BAR_CFG} className="h-[220px] w-full">
|
|
452
|
-
<BarChart data={
|
|
282
|
+
<BarChart data={coloredData} layout="vertical" margin={CHART_MARGIN_HORIZONTAL}>
|
|
453
283
|
<CartesianGrid horizontal={false} strokeDasharray="3 3" className="stroke-border" />
|
|
454
284
|
<XAxis type="number" allowDecimals={false} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
455
285
|
<YAxis type="category" dataKey="name" width={100} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -461,14 +291,14 @@ function ProgramMixChart({ rows, chartType }: { rows: Placement[]; chartType: Ch
|
|
|
461
291
|
activeBar={CHART_KBD_ACTIVE_BAR}
|
|
462
292
|
activeIndex={activeIndex ?? undefined}
|
|
463
293
|
>
|
|
464
|
-
{
|
|
465
|
-
<Cell key={i} fill={
|
|
294
|
+
{coloredData.map((d, i) => (
|
|
295
|
+
<Cell key={i} fill={d.fill} />
|
|
466
296
|
))}
|
|
467
297
|
</Bar>
|
|
468
298
|
</BarChart>
|
|
469
299
|
</ChartContainer>
|
|
470
300
|
) : (
|
|
471
|
-
<PieChartRenderer data={
|
|
301
|
+
<PieChartRenderer data={coloredData} activeIndex={activeIndex} />
|
|
472
302
|
)}
|
|
473
303
|
<ChartDataTable
|
|
474
304
|
caption="Placements by Program data"
|
|
@@ -505,7 +335,7 @@ function ComplianceChart({ rows, chartType }: { rows: Placement[]; chartType: Ch
|
|
|
505
335
|
<>
|
|
506
336
|
{chartType === "bar" ? (
|
|
507
337
|
<ChartContainer config={BAR_CFG} className="h-[220px] w-full">
|
|
508
|
-
<BarChart data={data} margin={
|
|
338
|
+
<BarChart data={data} margin={CHART_MARGIN}>
|
|
509
339
|
<CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
|
|
510
340
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
511
341
|
<YAxis allowDecimals={false} width={32} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -567,7 +397,7 @@ function ReadinessChart({ rows, chartType }: { rows: Placement[]; chartType: Cha
|
|
|
567
397
|
<HBarChartRenderer data={data} colored activeIndex={activeIndex} />
|
|
568
398
|
) : (
|
|
569
399
|
<ChartContainer config={BAR_CFG} className="h-[220px] w-full">
|
|
570
|
-
<BarChart data={data} margin={
|
|
400
|
+
<BarChart data={data} margin={CHART_MARGIN}>
|
|
571
401
|
<CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
|
|
572
402
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
573
403
|
<YAxis allowDecimals={false} width={32} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -624,7 +454,7 @@ function ProgressTrackerChart({ rows, chartType }: { rows: Placement[]; chartTyp
|
|
|
624
454
|
<>
|
|
625
455
|
{chartType === "bar" ? (
|
|
626
456
|
<ChartContainer config={cfg} className="h-[220px] w-full">
|
|
627
|
-
<BarChart data={data} margin={
|
|
457
|
+
<BarChart data={data} margin={CHART_MARGIN}>
|
|
628
458
|
<CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
|
|
629
459
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
630
460
|
<YAxis allowDecimals={false} width={32} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -651,7 +481,7 @@ function ProgressTrackerChart({ rows, chartType }: { rows: Placement[]; chartTyp
|
|
|
651
481
|
</ChartContainer>
|
|
652
482
|
) : (
|
|
653
483
|
<ChartContainer config={cfg} className="h-[220px] w-full">
|
|
654
|
-
<BarChart data={data} layout="vertical" margin={
|
|
484
|
+
<BarChart data={data} layout="vertical" margin={CHART_MARGIN_HORIZONTAL}>
|
|
655
485
|
<CartesianGrid horizontal={false} strokeDasharray="3 3" className="stroke-border" />
|
|
656
486
|
<XAxis type="number" allowDecimals={false} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
657
487
|
<YAxis type="category" dataKey="name" width={72} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -701,10 +531,15 @@ function SiteUtilisationChart({ rows, chartType }: { rows: Placement[]; chartTyp
|
|
|
701
531
|
.slice(0, 8)
|
|
702
532
|
}, [rows])
|
|
703
533
|
|
|
534
|
+
const colors = PALETTE_COLORS
|
|
535
|
+
const coloredData = React.useMemo(
|
|
536
|
+
() => data.map((d, i) => ({ ...d, fill: colors[i % colors.length] })),
|
|
537
|
+
[data, colors],
|
|
538
|
+
)
|
|
539
|
+
|
|
704
540
|
if (data.length === 0) return <EmptyChart />
|
|
705
541
|
|
|
706
542
|
const summary = `Top ${data.length} sites. Busiest: ${data[0].name} with ${data[0].value} placements.`
|
|
707
|
-
const colors = ["var(--color-chart-1)", "var(--color-chart-2)", "var(--color-chart-3)", "var(--color-chart-4)", "var(--color-chart-5)"]
|
|
708
543
|
|
|
709
544
|
return (
|
|
710
545
|
<ChartFigure label="Site Utilisation" summary={summary} dataLength={data.length}>
|
|
@@ -712,7 +547,7 @@ function SiteUtilisationChart({ rows, chartType }: { rows: Placement[]; chartTyp
|
|
|
712
547
|
<>
|
|
713
548
|
{chartType === "bar" ? (
|
|
714
549
|
<ChartContainer config={BAR_CFG} className="h-[220px] w-full">
|
|
715
|
-
<BarChart data={data} margin={
|
|
550
|
+
<BarChart data={data} margin={CHART_MARGIN}>
|
|
716
551
|
<CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
|
|
717
552
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
718
553
|
<YAxis allowDecimals={false} width={32} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -732,10 +567,10 @@ function SiteUtilisationChart({ rows, chartType }: { rows: Placement[]; chartTyp
|
|
|
732
567
|
</BarChart>
|
|
733
568
|
</ChartContainer>
|
|
734
569
|
) : chartType === "pie" ? (
|
|
735
|
-
<PieChartRenderer data={
|
|
570
|
+
<PieChartRenderer data={coloredData} activeIndex={activeIndex} />
|
|
736
571
|
) : (
|
|
737
572
|
<ChartContainer config={BAR_CFG} className="h-[220px] w-full">
|
|
738
|
-
<BarChart data={data} layout="vertical" margin={
|
|
573
|
+
<BarChart data={data} layout="vertical" margin={CHART_MARGIN_HORIZONTAL}>
|
|
739
574
|
<CartesianGrid horizontal={false} strokeDasharray="3 3" className="stroke-border" />
|
|
740
575
|
<XAxis type="number" allowDecimals={false} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
741
576
|
<YAxis type="category" dataKey="name" width={130} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -792,7 +627,7 @@ function CompletionOutcomesChart({ rows, chartType }: { rows: Placement[]; chart
|
|
|
792
627
|
{(activeIndex) => (
|
|
793
628
|
<>
|
|
794
629
|
<ChartContainer config={BAR_CFG} className="h-[220px] w-full">
|
|
795
|
-
<BarChart data={barData} margin={
|
|
630
|
+
<BarChart data={barData} margin={CHART_MARGIN}>
|
|
796
631
|
<CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
|
|
797
632
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
798
633
|
<YAxis allowDecimals={false} width={32} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} domain={[0, 100]} />
|
|
@@ -923,7 +758,7 @@ function UpcomingTimelineChart({ rows, chartType }: { rows: Placement[]; chartTy
|
|
|
923
758
|
{chartType === "line" ? (
|
|
924
759
|
<div className="relative w-full">
|
|
925
760
|
<ChartContainer config={AREA_CFG} className="h-[200px] w-full">
|
|
926
|
-
<LineChart data={data} margin={
|
|
761
|
+
<LineChart data={data} margin={CHART_MARGIN}>
|
|
927
762
|
<CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
|
|
928
763
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
929
764
|
<YAxis allowDecimals={false} width={32} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -961,7 +796,7 @@ function UpcomingTimelineChart({ rows, chartType }: { rows: Placement[]; chartTy
|
|
|
961
796
|
) : chartType === "bar" ? (
|
|
962
797
|
<div className="relative w-full">
|
|
963
798
|
<ChartContainer config={AREA_CFG} className="h-[200px] w-full">
|
|
964
|
-
<BarChart data={data} margin={
|
|
799
|
+
<BarChart data={data} margin={CHART_MARGIN}>
|
|
965
800
|
<CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
|
|
966
801
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
967
802
|
<YAxis allowDecimals={false} width={32} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -989,7 +824,7 @@ function UpcomingTimelineChart({ rows, chartType }: { rows: Placement[]; chartTy
|
|
|
989
824
|
) : (
|
|
990
825
|
<div className="relative w-full">
|
|
991
826
|
<ChartContainer config={AREA_CFG} className="h-[200px] w-full">
|
|
992
|
-
<AreaChart data={data} margin={
|
|
827
|
+
<AreaChart data={data} margin={CHART_MARGIN}>
|
|
993
828
|
<CartesianGrid vertical={false} strokeDasharray="3 3" className="stroke-border" />
|
|
994
829
|
<XAxis dataKey="name" tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
995
830
|
<YAxis allowDecimals={false} width={32} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -1052,7 +887,7 @@ function PieChartRenderer({
|
|
|
1052
887
|
data: { name: string; value: number; fill?: string }[]
|
|
1053
888
|
activeIndex?: number | null
|
|
1054
889
|
}) {
|
|
1055
|
-
const colors =
|
|
890
|
+
const colors = PALETTE_COLORS
|
|
1056
891
|
return (
|
|
1057
892
|
<ChartContainer config={BAR_CFG} className="h-[220px] w-full">
|
|
1058
893
|
<PieChart>
|
|
@@ -1091,7 +926,7 @@ function HBarChartRenderer({
|
|
|
1091
926
|
}) {
|
|
1092
927
|
return (
|
|
1093
928
|
<ChartContainer config={BAR_CFG} className="h-[220px] w-full">
|
|
1094
|
-
<BarChart data={data} layout="vertical" margin={
|
|
929
|
+
<BarChart data={data} layout="vertical" margin={CHART_MARGIN_HORIZONTAL}>
|
|
1095
930
|
<CartesianGrid horizontal={false} strokeDasharray="3 3" className="stroke-border" />
|
|
1096
931
|
<XAxis type="number" allowDecimals={false} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
1097
932
|
<YAxis type="category" dataKey="name" width={120} tick={{ fontSize: 11 }} tickLine={false} axisLine={false} />
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useRouter } from "next/navigation"
|
|
5
|
+
|
|
6
|
+
import { Button } from "@/components/ui/button"
|
|
7
|
+
import { Tip } from "@/components/ui/tip"
|
|
8
|
+
import type { DedicatedSearchRecentsController } from "@/lib/dedicated-search-recents"
|
|
9
|
+
|
|
10
|
+
export interface DedicatedSearchRecentsProps {
|
|
11
|
+
recents: Pick<DedicatedSearchRecentsController, "read" | "record" | "clear" | "eventName">
|
|
12
|
+
searchParamsKey: string
|
|
13
|
+
replacePath: string
|
|
14
|
+
patchSearchParams: (current: URLSearchParams, submittedText: string) => URLSearchParams
|
|
15
|
+
sectionTitle?: string
|
|
16
|
+
clearLabel?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Recent query rows — reads storage only after mount so SSR and first client paint match (no hydration drift).
|
|
21
|
+
*/
|
|
22
|
+
export function DedicatedSearchRecents({
|
|
23
|
+
recents,
|
|
24
|
+
searchParamsKey,
|
|
25
|
+
replacePath,
|
|
26
|
+
patchSearchParams,
|
|
27
|
+
sectionTitle = "Recently searched",
|
|
28
|
+
clearLabel = "Clear",
|
|
29
|
+
}: DedicatedSearchRecentsProps) {
|
|
30
|
+
const router = useRouter()
|
|
31
|
+
const [items, setItems] = React.useState<string[]>([])
|
|
32
|
+
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
const sync = () => setItems(recents.read())
|
|
35
|
+
sync()
|
|
36
|
+
window.addEventListener(recents.eventName, sync)
|
|
37
|
+
window.addEventListener("storage", sync)
|
|
38
|
+
return () => {
|
|
39
|
+
window.removeEventListener(recents.eventName, sync)
|
|
40
|
+
window.removeEventListener("storage", sync)
|
|
41
|
+
}
|
|
42
|
+
}, [recents])
|
|
43
|
+
|
|
44
|
+
const runQuery = React.useCallback(
|
|
45
|
+
(q: string) => {
|
|
46
|
+
recents.record(q)
|
|
47
|
+
const next = patchSearchParams(new URLSearchParams(searchParamsKey), q)
|
|
48
|
+
const qs = next.toString()
|
|
49
|
+
router.replace(qs ? `${replacePath}?${qs}` : replacePath, { scroll: false })
|
|
50
|
+
},
|
|
51
|
+
[patchSearchParams, recents, replacePath, router, searchParamsKey],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if (items.length === 0) {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const headingId = "dedicated-search-recents-heading"
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<section className="min-w-0" aria-labelledby={headingId}>
|
|
62
|
+
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-2">
|
|
63
|
+
<h2 id={headingId} className="text-base font-semibold tracking-tight text-foreground">
|
|
64
|
+
{sectionTitle}
|
|
65
|
+
</h2>
|
|
66
|
+
<Button
|
|
67
|
+
type="button"
|
|
68
|
+
variant="ghost"
|
|
69
|
+
size="sm"
|
|
70
|
+
className="h-auto min-h-6 shrink-0 px-2 py-1 text-sm text-muted-foreground"
|
|
71
|
+
onClick={() => recents.clear()}
|
|
72
|
+
>
|
|
73
|
+
{clearLabel}
|
|
74
|
+
</Button>
|
|
75
|
+
</div>
|
|
76
|
+
<div className="mt-2 divide-y divide-border overflow-hidden rounded-xl border border-border bg-card sm:mt-2.5">
|
|
77
|
+
{items.map((q, i) => (
|
|
78
|
+
<div key={`${q}-${i}`} className="min-w-0">
|
|
79
|
+
<Tip side="bottom" label={`Run this search again — ${q}`}>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
aria-label={`Run this search again — ${q}`}
|
|
83
|
+
className="flex w-full min-h-11 items-center gap-3 px-4 py-3 text-left text-sm transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset sm:px-5 sm:py-3.5"
|
|
84
|
+
onClick={() => runQuery(q)}
|
|
85
|
+
>
|
|
86
|
+
<i className="fa-light fa-clock-rotate-left size-4 shrink-0 text-muted-foreground" aria-hidden="true" />
|
|
87
|
+
<span className="min-w-0 flex-1 truncate font-medium text-foreground">{q}</span>
|
|
88
|
+
<i className="fa-light fa-chevron-right shrink-0 text-xs text-muted-foreground" aria-hidden="true" />
|
|
89
|
+
</button>
|
|
90
|
+
</Tip>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
</section>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useRouter } from "next/navigation"
|
|
5
|
+
|
|
6
|
+
import { AskLeoComposer } from "@/components/ask-leo-composer"
|
|
7
|
+
import { cn } from "@/lib/utils"
|
|
8
|
+
|
|
9
|
+
export interface DedicatedSearchUrlComposerProps {
|
|
10
|
+
/** Serialized `URLSearchParams` for the active route (stable string from parent). */
|
|
11
|
+
searchParamsKey: string
|
|
12
|
+
/** Base path for `router.replace` (no query). */
|
|
13
|
+
replacePath: string
|
|
14
|
+
/**
|
|
15
|
+
* Merge submitted text into the next query string. Hub supplies domain rules
|
|
16
|
+
* (e.g. preserve `scope=` / toggles while updating `q=`).
|
|
17
|
+
*/
|
|
18
|
+
patchSearchParams: (current: URLSearchParams, submittedText: string) => URLSearchParams
|
|
19
|
+
/** Optional — record a successful non-empty submission (e.g. recents). */
|
|
20
|
+
onRecordSubmission?: (trimmed: string) => void
|
|
21
|
+
/** `hero` — centered landing; `default` — inset under a list header. */
|
|
22
|
+
layout?: "default" | "hero"
|
|
23
|
+
animatedPlaceholders: readonly string[] | string[]
|
|
24
|
+
animatedPlaceholderIntervalMs?: number
|
|
25
|
+
animatedPlaceholderMaxLines?: 1 | 2
|
|
26
|
+
placeholder?: string
|
|
27
|
+
inputLabel?: string
|
|
28
|
+
submitAppearance?: "search" | "send"
|
|
29
|
+
submitButtonAriaLabel?: string
|
|
30
|
+
/** Screen-reader-only instructions for the field (not the sole format hint). */
|
|
31
|
+
srOnlyDescription: React.ReactNode
|
|
32
|
+
composerClassName?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* AI-styled composer that updates the URL via `router.replace` — does not open Ask Leo.
|
|
37
|
+
*/
|
|
38
|
+
export function DedicatedSearchUrlComposer({
|
|
39
|
+
searchParamsKey,
|
|
40
|
+
replacePath,
|
|
41
|
+
patchSearchParams,
|
|
42
|
+
onRecordSubmission,
|
|
43
|
+
layout = "default",
|
|
44
|
+
animatedPlaceholders,
|
|
45
|
+
animatedPlaceholderIntervalMs = 4800,
|
|
46
|
+
animatedPlaceholderMaxLines = 2,
|
|
47
|
+
placeholder = "Search…",
|
|
48
|
+
inputLabel = "Search",
|
|
49
|
+
submitAppearance = "search",
|
|
50
|
+
submitButtonAriaLabel = "Run search",
|
|
51
|
+
srOnlyDescription,
|
|
52
|
+
composerClassName,
|
|
53
|
+
}: DedicatedSearchUrlComposerProps) {
|
|
54
|
+
const router = useRouter()
|
|
55
|
+
const sp = React.useMemo(() => new URLSearchParams(searchParamsKey), [searchParamsKey])
|
|
56
|
+
const qFromUrl = sp.get("q") ?? ""
|
|
57
|
+
const [value, setValue] = React.useState(qFromUrl)
|
|
58
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
59
|
+
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
setValue(qFromUrl)
|
|
62
|
+
}, [qFromUrl])
|
|
63
|
+
|
|
64
|
+
const onSubmit = React.useCallback(
|
|
65
|
+
(message: string) => {
|
|
66
|
+
const trimmed = message.trim()
|
|
67
|
+
if (trimmed) onRecordSubmission?.(trimmed)
|
|
68
|
+
const next = patchSearchParams(new URLSearchParams(searchParamsKey), trimmed)
|
|
69
|
+
const qs = next.toString()
|
|
70
|
+
router.replace(qs ? `${replacePath}?${qs}` : replacePath, { scroll: false })
|
|
71
|
+
},
|
|
72
|
+
[onRecordSubmission, patchSearchParams, replacePath, router, searchParamsKey],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className={cn(layout === "hero" ? "min-w-0" : "px-4 pb-3 lg:px-6")}>
|
|
77
|
+
<p className="sr-only">{srOnlyDescription}</p>
|
|
78
|
+
<div
|
|
79
|
+
className={cn(
|
|
80
|
+
"min-w-0 max-w-full border border-[color:var(--control-border)] bg-card shadow-sm transition-[border-radius,padding,box-shadow] duration-200 ease-out",
|
|
81
|
+
layout === "hero" && "shadow-md",
|
|
82
|
+
expanded
|
|
83
|
+
? layout === "hero"
|
|
84
|
+
? "rounded-2xl p-1.5 shadow-md"
|
|
85
|
+
: "rounded-2xl p-1.5 shadow-md"
|
|
86
|
+
: layout === "hero"
|
|
87
|
+
? "rounded-2xl px-1.5 py-1.5 sm:px-2 sm:py-2"
|
|
88
|
+
: "rounded-full px-1 py-1",
|
|
89
|
+
)}
|
|
90
|
+
>
|
|
91
|
+
<AskLeoComposer
|
|
92
|
+
value={value}
|
|
93
|
+
onChange={setValue}
|
|
94
|
+
onSubmit={onSubmit}
|
|
95
|
+
onExpandedChange={setExpanded}
|
|
96
|
+
animatedPlaceholders={[...animatedPlaceholders]}
|
|
97
|
+
animatedPlaceholderIntervalMs={animatedPlaceholderIntervalMs}
|
|
98
|
+
animatedPlaceholderMaxLines={animatedPlaceholderMaxLines}
|
|
99
|
+
leadingSlot="ai-mark"
|
|
100
|
+
inputLabel={inputLabel}
|
|
101
|
+
submitAppearance={submitAppearance}
|
|
102
|
+
submitButtonAriaLabel={submitButtonAriaLabel}
|
|
103
|
+
placeholder={placeholder}
|
|
104
|
+
className={cn(
|
|
105
|
+
"[&_form>div]:rounded-none [&_form>div]:border-0 [&_form>div]:bg-transparent [&_form>div]:shadow-none",
|
|
106
|
+
composerClassName,
|
|
107
|
+
)}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -237,7 +237,7 @@ function ChecklistVariant() {
|
|
|
237
237
|
<i className="fa-light fa-ellipsis-vertical" aria-hidden="true" />
|
|
238
238
|
</Button>
|
|
239
239
|
</DropdownMenuTrigger>
|
|
240
|
-
<DropdownMenuContent align="end"
|
|
240
|
+
<DropdownMenuContent align="end">
|
|
241
241
|
<DropdownMenuItem onSelect={() => setDismissed(true)}>
|
|
242
242
|
<i className="fa-light fa-box-archive me-2 text-xs" aria-hidden="true" />
|
|
243
243
|
Dismiss
|