@exxatdesignux/ui 0.1.0 → 0.2.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.
Files changed (155) hide show
  1. package/bin/cli.mjs +176 -0
  2. package/bin/init.mjs +15 -1
  3. package/bin/sync-extras.mjs +65 -0
  4. package/consumer-extras/README.md +21 -0
  5. package/consumer-extras/cursor-skills/exxat-accessibility/SKILL.md +282 -0
  6. package/consumer-extras/cursor-skills/exxat-board-cards/SKILL.md +68 -0
  7. package/consumer-extras/cursor-skills/exxat-centralized-list-dataset/SKILL.md +99 -0
  8. package/consumer-extras/cursor-skills/exxat-ds-skill/SKILL.md +713 -0
  9. package/consumer-extras/cursor-skills/exxat-fontawesome-icons/SKILL.md +31 -0
  10. package/consumer-extras/cursor-skills/exxat-list-page-view-shells/SKILL.md +36 -0
  11. package/consumer-extras/cursor-skills/exxat-primary-nav-secondary-panel/SKILL.md +27 -0
  12. package/consumer-extras/patterns/command-menu-pattern.md +45 -0
  13. package/consumer-extras/patterns/data-views-pattern.md +167 -0
  14. package/package.json +7 -3
  15. package/src/components/ui/sidebar.tsx +7 -2
  16. package/template/.agents/skills/shadcn/SKILL.md +242 -0
  17. package/template/.agents/skills/shadcn/agents/openai.yml +5 -0
  18. package/template/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
  19. package/template/.agents/skills/shadcn/assets/shadcn.png +0 -0
  20. package/template/.agents/skills/shadcn/cli.md +257 -0
  21. package/template/.agents/skills/shadcn/customization.md +202 -0
  22. package/template/.agents/skills/shadcn/evals/evals.json +47 -0
  23. package/template/.agents/skills/shadcn/mcp.md +94 -0
  24. package/template/.agents/skills/shadcn/rules/base-vs-radix.md +306 -0
  25. package/template/.agents/skills/shadcn/rules/composition.md +195 -0
  26. package/template/.agents/skills/shadcn/rules/forms.md +192 -0
  27. package/template/.agents/skills/shadcn/rules/icons.md +101 -0
  28. package/template/.agents/skills/shadcn/rules/styling.md +162 -0
  29. package/template/.claude/skills/exxat-ds-skill/SKILL.md +712 -0
  30. package/template/.cursor/rules/exxat-accessibility.mdc +33 -0
  31. package/template/.cursor/rules/exxat-command-menu.mdc +23 -0
  32. package/template/.cursor/rules/exxat-dashboard-view-charts.mdc +53 -0
  33. package/template/.cursor/rules/exxat-data-tables.mdc +31 -0
  34. package/template/.cursor/rules/exxat-ds-agents.mdc +26 -0
  35. package/template/.cursor/rules/exxat-kbd-shortcuts.mdc +100 -0
  36. package/template/.cursor/rules/exxat-list-page-connected-views.mdc +16 -0
  37. package/template/.cursor/rules/exxat-no-toast.mdc +26 -0
  38. package/template/.cursor/rules/exxat-page-vs-drawer.mdc +22 -0
  39. package/template/.cursor/rules/exxat-table-properties-drawer.mdc +40 -0
  40. package/template/AGENTS.md +52 -11
  41. package/template/app/(app)/dashboard/page.tsx +1 -1
  42. package/template/app/(app)/data-list/[id]/page.tsx +24 -8
  43. package/template/app/(app)/data-list/new/page.tsx +7 -4
  44. package/template/app/(app)/data-list/page.tsx +1 -1
  45. package/template/app/(app)/examples/page.tsx +41 -0
  46. package/template/app/(app)/question-bank/page.tsx +3 -3
  47. package/template/app/globals.css +1 -1
  48. package/template/components/app-sidebar.tsx +52 -35
  49. package/template/components/compliance-table.tsx +79 -0
  50. package/template/components/data-list-client.tsx +36 -25
  51. package/template/components/data-list-table.tsx +797 -10
  52. package/template/components/data-views/finder-panel-view.tsx +405 -0
  53. package/template/components/data-views/folder-grid-view.tsx +86 -0
  54. package/template/components/data-views/index.ts +59 -0
  55. package/template/components/data-views/list-page-split-details-placeholder.tsx +39 -0
  56. package/template/components/data-views/list-page-split-hub-chrome.tsx +60 -0
  57. package/template/components/data-views/list-page-split-hub-tokens.ts +16 -0
  58. package/template/components/data-views/list-page-tree-column-header.tsx +31 -0
  59. package/template/components/data-views/list-page-tree-panel-shell.tsx +91 -0
  60. package/template/components/data-views/list-page-view-frame.tsx +53 -0
  61. package/template/components/data-views/os-folder-glyph.tsx +121 -0
  62. package/template/components/folder-details-shell.tsx +230 -0
  63. package/template/components/hub-tree-panel-view.tsx +672 -0
  64. package/template/components/list-hub-status-badge.tsx +17 -3
  65. package/template/components/page-header.tsx +149 -7
  66. package/template/components/placements-page-header.tsx +14 -8
  67. package/template/components/placements-table-columns.tsx +8 -8
  68. package/template/components/question-bank-client.tsx +157 -39
  69. package/template/components/question-bank-new-folder-sheet.tsx +248 -0
  70. package/template/components/question-bank-os-folder-view.tsx +648 -0
  71. package/template/components/question-bank-page-header.tsx +31 -2
  72. package/template/components/question-bank-panel-activator.tsx +9 -0
  73. package/template/components/question-bank-secondary-nav.tsx +226 -0
  74. package/template/components/question-bank-table.tsx +707 -22
  75. package/template/components/secondary-panel.tsx +41 -107
  76. package/template/components/sites-table.tsx +66 -0
  77. package/template/components/team-client.tsx +7 -0
  78. package/template/components/team-table.tsx +156 -1
  79. package/template/components/templates/list-page.tsx +2 -2
  80. package/template/components/ui/avatar.tsx +1 -1
  81. package/template/components/ui/badge.tsx +1 -1
  82. package/template/components/ui/banner.tsx +1 -1
  83. package/template/components/ui/breadcrumb.tsx +1 -1
  84. package/template/components/ui/button.tsx +1 -1
  85. package/template/components/ui/calendar.tsx +1 -1
  86. package/template/components/ui/card.tsx +1 -1
  87. package/template/components/ui/chart.tsx +1 -1
  88. package/template/components/ui/checkbox.tsx +1 -1
  89. package/template/components/ui/coach-mark.tsx +1 -1
  90. package/template/components/ui/collapsible.tsx +1 -1
  91. package/template/components/ui/command.tsx +1 -1
  92. package/template/components/ui/date-picker-field.tsx +1 -1
  93. package/template/components/ui/dialog.tsx +1 -1
  94. package/template/components/ui/drag-handle-grip.tsx +1 -1
  95. package/template/components/ui/drawer.tsx +1 -1
  96. package/template/components/ui/dropdown-menu.tsx +1 -1
  97. package/template/components/ui/field.tsx +1 -1
  98. package/template/components/ui/form.tsx +1 -1
  99. package/template/components/ui/input-group.tsx +1 -1
  100. package/template/components/ui/input-mask.tsx +1 -1
  101. package/template/components/ui/input.tsx +1 -1
  102. package/template/components/ui/kbd.tsx +1 -1
  103. package/template/components/ui/label.tsx +1 -1
  104. package/template/components/ui/payment-card-fields.tsx +1 -1
  105. package/template/components/ui/popover.tsx +1 -1
  106. package/template/components/ui/radio-group.tsx +1 -1
  107. package/template/components/ui/resizable.tsx +68 -0
  108. package/template/components/ui/select.tsx +1 -1
  109. package/template/components/ui/selection-tile-grid.tsx +1 -1
  110. package/template/components/ui/separator.tsx +1 -1
  111. package/template/components/ui/sheet.tsx +1 -1
  112. package/template/components/ui/sidebar.tsx +1 -1
  113. package/template/components/ui/skeleton.tsx +1 -1
  114. package/template/components/ui/sonner.tsx +1 -1
  115. package/template/components/ui/status-badge.tsx +1 -1
  116. package/template/components/ui/table.tsx +1 -1
  117. package/template/components/ui/tabs.tsx +1 -1
  118. package/template/components/ui/textarea.tsx +1 -1
  119. package/template/components/ui/tip.tsx +1 -1
  120. package/template/components/ui/toggle-group.tsx +1 -1
  121. package/template/components/ui/toggle-switch.tsx +1 -1
  122. package/template/components/ui/toggle.tsx +1 -1
  123. package/template/components/ui/tooltip.tsx +1 -1
  124. package/template/components/ui/view-segmented-control.tsx +1 -1
  125. package/template/docs/data-views-pattern.md +7 -0
  126. package/template/hooks/use-app-theme.ts +1 -1
  127. package/template/hooks/use-coach-mark.ts +1 -1
  128. package/template/hooks/use-location-hash.ts +15 -0
  129. package/template/hooks/use-mobile.ts +1 -1
  130. package/template/hooks/use-mod-key-label.ts +1 -1
  131. package/template/hooks/use-sidebar-reflow-zoom.ts +40 -0
  132. package/template/lib/ask-leo-route-context.ts +25 -57
  133. package/template/lib/coach-mark-registry.ts +13 -13
  134. package/template/lib/command-menu-config.ts +28 -23
  135. package/template/lib/command-menu-search-data.ts +10 -9
  136. package/template/lib/data-list-view-surface.ts +12 -1
  137. package/template/lib/data-list-view.ts +6 -3
  138. package/template/lib/date-filter.ts +1 -1
  139. package/template/lib/mock/dashboard.ts +11 -11
  140. package/template/lib/mock/navigation.tsx +22 -63
  141. package/template/lib/mock/placements-kpi.ts +19 -19
  142. package/template/lib/mock/question-bank-folders.ts +167 -0
  143. package/template/lib/mock/question-bank-header-collaborators.ts +14 -0
  144. package/template/lib/mock/question-bank-inspector.ts +109 -0
  145. package/template/lib/mock/question-bank-kpi.ts +1 -1
  146. package/template/lib/mock/question-bank.ts +80 -0
  147. package/template/lib/question-bank-nav.ts +91 -0
  148. package/template/lib/utils.ts +1 -1
  149. package/template/next.config.mjs +8 -0
  150. package/template/package.json +1 -0
  151. package/template/public/folders/icons8-folder-windows-11.svg +1 -0
  152. package/template/app/(app)/compliance/page.tsx +0 -10
  153. package/template/app/(app)/rotations/page.tsx +0 -15
  154. package/template/app/(app)/sites/all/page.tsx +0 -13
  155. package/template/app/(app)/team/page.tsx +0 -10
@@ -2,7 +2,11 @@
2
2
 
3
3
  import * as React from "react"
4
4
  import { Button } from "@/components/ui/button"
5
- import { PageHeader } from "@/components/page-header"
5
+ import {
6
+ PageHeader,
7
+ type PageHeaderCollaborator,
8
+ type PageHeaderVariant,
9
+ } from "@/components/page-header"
6
10
  import {
7
11
  DropdownMenu,
8
12
  DropdownMenuContent,
@@ -11,31 +15,56 @@ import {
11
15
  DropdownMenuTrigger,
12
16
  } from "@/components/ui/dropdown-menu"
13
17
  import { Tip } from "@/components/ui/tip"
18
+ import { QUESTION_BANK_HEADER_COLLABORATORS } from "@/lib/mock/question-bank-header-collaborators"
14
19
 
15
20
  export interface QuestionBankPageHeaderProps {
21
+ /** Scoped hub title (All / My / folder name) — keep in sync with `SiteHeader`. */
22
+ title: string
16
23
  questionCount: number
17
24
  onNewQuestion: () => void
18
25
  onExport: () => void
19
26
  showMetrics: boolean
20
27
  onToggleMetrics: () => void
21
28
  showTitleBlock?: boolean
29
+ /** `collaboration` adds access line + collaborator stack + invite before CTAs. */
30
+ variant?: PageHeaderVariant
31
+ /** Optional role / access row when `variant="collaboration"` (badge + copy). */
32
+ accessInfo?: React.ReactNode
33
+ collaborators?: PageHeaderCollaborator[]
34
+ collaboratorDisplayLimit?: number
35
+ onAddCollaborator?: () => void
36
+ addCollaboratorLabel?: string
22
37
  }
23
38
 
24
39
  export function QuestionBankPageHeader({
40
+ title,
25
41
  questionCount,
26
42
  onNewQuestion,
27
43
  onExport,
28
44
  showMetrics,
29
45
  onToggleMetrics,
30
46
  showTitleBlock = true,
47
+ variant = "default",
48
+ accessInfo,
49
+ collaborators = QUESTION_BANK_HEADER_COLLABORATORS,
50
+ collaboratorDisplayLimit = 4,
51
+ onAddCollaborator = () => {},
52
+ addCollaboratorLabel = "Invite people",
31
53
  }: QuestionBankPageHeaderProps) {
32
54
  const [moreOpen, setMoreOpen] = React.useState(false)
33
55
  const countLine = `${questionCount} ${questionCount === 1 ? "question" : "questions"} · Last updated now`
56
+ const resolvedAccess = variant === "collaboration" ? accessInfo : undefined
34
57
 
35
58
  return (
36
59
  <PageHeader
37
- title="Question bank"
60
+ title={title}
38
61
  subtitle={countLine}
62
+ variant={variant}
63
+ accessInfo={resolvedAccess}
64
+ collaborators={variant === "collaboration" ? collaborators : undefined}
65
+ collaboratorDisplayLimit={collaboratorDisplayLimit}
66
+ onAddCollaborator={variant === "collaboration" ? onAddCollaborator : undefined}
67
+ addCollaboratorLabel={addCollaboratorLabel}
39
68
  showTitleBlock={showTitleBlock}
40
69
  actions={(
41
70
  <div className="flex items-center gap-2" role="group" aria-label="Question bank actions">
@@ -0,0 +1,9 @@
1
+ "use client"
2
+
3
+ import { useAutoPanel } from "@/components/secondary-panel"
4
+
5
+ /** Opens the Question bank secondary panel while this route is mounted (same pattern as rotations). */
6
+ export function QuestionBankPanelActivator() {
7
+ useAutoPanel("question-bank")
8
+ return null
9
+ }
@@ -0,0 +1,226 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Question bank secondary sidebar — All / My / folder tree (Font Awesome only).
5
+ * Scope syncs to the main hub via `?scope=` + optional `folderId=` (`lib/question-bank-nav.ts`).
6
+ */
7
+
8
+ import * as React from "react"
9
+ import Link from "next/link"
10
+ import { usePathname, useSearchParams } from "next/navigation"
11
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"
12
+ import { Tip } from "@/components/ui/tip"
13
+ import { cn } from "@/lib/utils"
14
+ import type { QuestionBankFolder } from "@/lib/mock/question-bank-folders"
15
+ import { QUESTION_BANK_FOLDER_ICON_COLORS } from "@/lib/mock/question-bank-folders"
16
+ import { DEFAULT_QUESTION_BANK_FOLDERS } from "@/lib/mock/question-bank-folders"
17
+ import { useSecondaryPanel } from "@/components/secondary-panel"
18
+ import {
19
+ parseQuestionBankNav,
20
+ questionBankNavHref,
21
+ type QuestionBankNavScope,
22
+ } from "@/lib/question-bank-nav"
23
+
24
+ function matchesQuery(q: string, ...parts: (string | undefined)[]) {
25
+ if (!q) return true
26
+ return parts.some(p => p && p.toLowerCase().includes(q))
27
+ }
28
+
29
+ function isNavActive(
30
+ pathname: string,
31
+ nav: ReturnType<typeof parseQuestionBankNav>,
32
+ scope: QuestionBankNavScope,
33
+ folderId?: string | null,
34
+ ) {
35
+ if (pathname !== "/question-bank") return false
36
+ if (scope === "all") return nav.scope === "all"
37
+ if (scope === "my") return nav.scope === "my"
38
+ if (scope === "folder" && folderId) {
39
+ return nav.scope === "folder" && nav.folderId === folderId
40
+ }
41
+ return false
42
+ }
43
+
44
+ function NavRow({
45
+ href,
46
+ active,
47
+ iconClass,
48
+ label,
49
+ onClick,
50
+ }: {
51
+ href: string
52
+ active: boolean
53
+ iconClass: string
54
+ label: string
55
+ /** e.g. reopen secondary panel on same-route “All questions” */
56
+ onClick?: () => void
57
+ }) {
58
+ return (
59
+ <li className="min-w-0">
60
+ <Tip label={label} side="right">
61
+ <Link
62
+ href={href}
63
+ onClick={() => onClick?.()}
64
+ aria-current={active ? "page" : undefined}
65
+ className={cn(
66
+ "flex w-full min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors",
67
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
68
+ active
69
+ ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground"
70
+ : "text-sidebar-foreground hover:bg-sidebar-accent/50",
71
+ )}
72
+ >
73
+ <span className="size-4 shrink-0 text-center text-[13px] leading-none" aria-hidden>
74
+ <i className={cn(active ? "fa-solid" : "fa-light", iconClass)} aria-hidden />
75
+ </span>
76
+ <span className="min-w-0 flex-1 truncate">{label}</span>
77
+ </Link>
78
+ </Tip>
79
+ </li>
80
+ )
81
+ }
82
+
83
+ function PanelFolderBranch({
84
+ folder,
85
+ folders,
86
+ query,
87
+ depth,
88
+ pathname,
89
+ nav,
90
+ }: {
91
+ folder: QuestionBankFolder
92
+ folders: QuestionBankFolder[]
93
+ query: string
94
+ depth: number
95
+ pathname: string
96
+ nav: ReturnType<typeof parseQuestionBankNav>
97
+ }) {
98
+ const childFolders = folders
99
+ .filter(f => f.parentId === folder.id)
100
+ .sort((a, b) => a.name.localeCompare(b.name))
101
+
102
+ const visibleFolders = React.useMemo(
103
+ () => childFolders.filter(f => matchesQuery(query, f.name)),
104
+ [childFolders, query],
105
+ )
106
+
107
+ const hasSubfolders = childFolders.length > 0
108
+ const indent = depth * 10
109
+
110
+ if (query && !matchesQuery(query, folder.name) && visibleFolders.length === 0) {
111
+ return null
112
+ }
113
+
114
+ const folderHref = questionBankNavHref({ scope: "folder", folderId: folder.id })
115
+ const folderActive = isNavActive(pathname, nav, "folder", folder.id)
116
+
117
+ return (
118
+ <Collapsible defaultOpen={depth < 1} className="group">
119
+ <div className="flex min-w-0 items-center rounded-md hover:bg-sidebar-accent/40">
120
+ <div style={{ width: indent }} className="shrink-0" aria-hidden />
121
+ {hasSubfolders ? (
122
+ <CollapsibleTrigger asChild>
123
+ <button
124
+ type="button"
125
+ className="flex size-8 shrink-0 items-center justify-center text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
126
+ aria-label={`${folder.name} — expand or collapse`}
127
+ >
128
+ <i
129
+ className="fa-light fa-chevron-right text-xs transition-transform duration-150 group-data-[state=open]:rotate-90"
130
+ aria-hidden
131
+ />
132
+ </button>
133
+ </CollapsibleTrigger>
134
+ ) : (
135
+ <div className="size-8 shrink-0" aria-hidden />
136
+ )}
137
+ <Tip label={folder.name} side="right">
138
+ <Link
139
+ href={folderHref}
140
+ aria-current={folderActive ? "page" : undefined}
141
+ className={cn(
142
+ "flex min-w-0 flex-1 items-center gap-2 py-1.5 pr-2 text-left text-sm transition-colors",
143
+ "rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
144
+ folderActive
145
+ ? "bg-sidebar-accent font-medium text-sidebar-accent-foreground"
146
+ : "text-sidebar-foreground hover:bg-sidebar-accent/50",
147
+ )}
148
+ >
149
+ <i
150
+ className={cn("fa-light shrink-0 text-sm", folder.icon, QUESTION_BANK_FOLDER_ICON_COLORS[folder.colorKey])}
151
+ aria-hidden
152
+ />
153
+ <span className="min-w-0 flex-1 truncate leading-tight">{folder.name}</span>
154
+ {hasSubfolders ? (
155
+ <span className="shrink-0 text-xs tabular-nums text-muted-foreground">{childFolders.length}</span>
156
+ ) : null}
157
+ </Link>
158
+ </Tip>
159
+ </div>
160
+ {hasSubfolders ? (
161
+ <CollapsibleContent>
162
+ {visibleFolders.map(child => (
163
+ <PanelFolderBranch
164
+ key={child.id}
165
+ folder={child}
166
+ folders={folders}
167
+ query={query}
168
+ depth={depth + 1}
169
+ pathname={pathname}
170
+ nav={nav}
171
+ />
172
+ ))}
173
+ </CollapsibleContent>
174
+ ) : null}
175
+ </Collapsible>
176
+ )
177
+ }
178
+
179
+ export function QuestionBankSecondaryNav({ query }: { query: string }) {
180
+ const pathname = usePathname()
181
+ const searchParams = useSearchParams()
182
+ const searchParamsKey = searchParams.toString()
183
+ const { openPanel } = useSecondaryPanel()
184
+ const nav = React.useMemo(
185
+ () => parseQuestionBankNav(new URLSearchParams(searchParamsKey)),
186
+ [searchParamsKey],
187
+ )
188
+
189
+ const folders = DEFAULT_QUESTION_BANK_FOLDERS
190
+ const q = query.trim().toLowerCase()
191
+
192
+ const roots = React.useMemo(
193
+ () => folders.filter(f => f.parentId === null).sort((a, b) => a.name.localeCompare(b.name)),
194
+ [folders],
195
+ )
196
+
197
+ return (
198
+ <div className="min-h-0 flex-1 overflow-y-auto px-3 pb-4" role="navigation" aria-label="Question bank">
199
+ <ul className="space-y-0.5" role="list">
200
+ <NavRow
201
+ href={questionBankNavHref({ scope: "all" })}
202
+ active={isNavActive(pathname, nav, "all")}
203
+ iconClass="fa-table-list"
204
+ label="All questions"
205
+ onClick={() => openPanel("question-bank")}
206
+ />
207
+ <NavRow
208
+ href={questionBankNavHref({ scope: "my" })}
209
+ active={isNavActive(pathname, nav, "my")}
210
+ iconClass="fa-user"
211
+ label="My questions"
212
+ />
213
+ <li role="presentation" className="select-none">
214
+ <span className="block px-2 pt-3 pb-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground/70">
215
+ Folders
216
+ </span>
217
+ </li>
218
+ {roots.map(folder => (
219
+ <li key={folder.id} className="min-w-0">
220
+ <PanelFolderBranch folder={folder} folders={folders} query={q} depth={0} pathname={pathname} nav={nav} />
221
+ </li>
222
+ ))}
223
+ </ul>
224
+ </div>
225
+ )
226
+ }