create-einja-app 0.2.17 → 0.2.19

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 (275) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +685 -1715
  3. package/dist/cli.js.map +1 -1
  4. package/package.json +2 -2
  5. package/templates/default/.claude/hooks/einja/playwright-resize.sh +12 -2
  6. package/templates/default/.claude/settings.json +16 -0
  7. package/templates/default/.cursor/commands/task-vibe-kanban-loop.md +107 -42
  8. package/templates/default/.env.develop +0 -4
  9. package/templates/default/.env.example +1 -0
  10. package/templates/default/.env.preview +0 -4
  11. package/templates/default/.env.staging +19 -0
  12. package/templates/default/.github/actions/ci/action.yml +39 -0
  13. package/templates/default/.github/actions/migrate/action.yml +39 -0
  14. package/templates/default/.github/actions/neon-export-env/action.yml +28 -0
  15. package/templates/default/.github/actions/setup/action.yml +20 -0
  16. package/templates/default/.github/workflows/claude.yml +1 -0
  17. package/templates/default/.github/workflows/{cleanup-neon-branches.yml → cleanup-pr-preview-db.yml} +28 -24
  18. package/templates/default/.github/workflows/cleanup-pr-preview-on-close.yml +50 -0
  19. package/templates/default/.github/workflows/deploy-pr-preview.yml +398 -0
  20. package/templates/default/.github/workflows/deploy-stable-branches.yml +259 -0
  21. package/templates/default/.github/workflows/release-create-einja-app.yml +95 -0
  22. package/templates/default/.mcp.json +29 -11
  23. package/templates/default/.serena/project.yml +4 -0
  24. package/templates/default/.vscode/settings.json +18 -0
  25. package/templates/default/CLAUDE.md +129 -353
  26. package/templates/default/README.md +5 -14
  27. package/templates/default/apps/admin/next.config.ts +11 -0
  28. package/templates/default/apps/admin/package.json +55 -0
  29. package/templates/default/apps/admin/postcss.config.cjs +5 -0
  30. package/templates/default/apps/admin/src/app/(auth)/forgot-password/page.tsx +97 -0
  31. package/templates/default/apps/admin/src/app/(auth)/layout.tsx +18 -0
  32. package/templates/default/apps/admin/src/app/(auth)/otp/page.tsx +121 -0
  33. package/templates/default/apps/admin/src/app/(auth)/sign-in/page.tsx +145 -0
  34. package/templates/default/apps/admin/src/app/(auth)/sign-up/page.tsx +199 -0
  35. package/templates/default/apps/admin/src/app/(errors)/401/page.tsx +27 -0
  36. package/templates/default/apps/admin/src/app/(errors)/403/page.tsx +28 -0
  37. package/templates/default/apps/admin/src/app/(errors)/500/page.tsx +29 -0
  38. package/templates/default/apps/admin/src/app/(errors)/layout.tsx +7 -0
  39. package/templates/default/apps/admin/src/app/(errors)/maintenance/page.tsx +25 -0
  40. package/templates/default/apps/admin/src/app/dashboard/_components/analytics-chart.tsx +68 -0
  41. package/templates/default/apps/admin/src/app/dashboard/_components/analytics.tsx +182 -0
  42. package/templates/default/apps/admin/src/app/dashboard/_components/dashboard-page.tsx +74 -0
  43. package/templates/default/apps/admin/src/app/dashboard/_components/metric-cards.tsx +49 -0
  44. package/templates/default/apps/admin/src/app/dashboard/_components/overview-chart.tsx +73 -0
  45. package/templates/default/apps/admin/src/app/dashboard/_components/recent-sales.tsx +75 -0
  46. package/templates/default/apps/admin/src/app/dashboard/apps/_components/apps-page.tsx +135 -0
  47. package/templates/default/apps/admin/src/app/dashboard/apps/page.tsx +10 -0
  48. package/templates/default/apps/admin/src/app/dashboard/chats/_components/chat-list.tsx +82 -0
  49. package/templates/default/apps/admin/src/app/dashboard/chats/_components/chat-messages.tsx +194 -0
  50. package/templates/default/apps/admin/src/app/dashboard/chats/_components/chats-page.tsx +99 -0
  51. package/templates/default/apps/admin/src/app/dashboard/chats/_components/new-chat.tsx +118 -0
  52. package/templates/default/apps/admin/src/app/dashboard/chats/page.tsx +10 -0
  53. package/templates/default/apps/admin/src/app/dashboard/layout.tsx +9 -0
  54. package/templates/default/apps/admin/src/app/dashboard/not-found.tsx +14 -0
  55. package/templates/default/apps/admin/src/app/dashboard/page.tsx +10 -0
  56. package/templates/default/apps/admin/src/app/dashboard/settings/_components/content-section.tsx +20 -0
  57. package/templates/default/apps/admin/src/app/dashboard/settings/_components/sidebar-nav.tsx +66 -0
  58. package/templates/default/apps/admin/src/app/dashboard/settings/account/page.tsx +173 -0
  59. package/templates/default/apps/admin/src/app/dashboard/settings/appearance/page.tsx +156 -0
  60. package/templates/default/apps/admin/src/app/dashboard/settings/display/page.tsx +125 -0
  61. package/templates/default/apps/admin/src/app/dashboard/settings/layout.tsx +30 -0
  62. package/templates/default/apps/admin/src/app/dashboard/settings/notifications/page.tsx +196 -0
  63. package/templates/default/apps/admin/src/app/dashboard/settings/page.tsx +5 -0
  64. package/templates/default/apps/admin/src/app/dashboard/settings/profile/page.tsx +176 -0
  65. package/templates/default/apps/admin/src/app/dashboard/tasks/_components/data-table-bulk-actions.tsx +183 -0
  66. package/templates/default/apps/admin/src/app/dashboard/tasks/_components/data-table-row-actions.tsx +79 -0
  67. package/templates/default/apps/admin/src/app/dashboard/tasks/_components/tasks-columns.tsx +107 -0
  68. package/templates/default/apps/admin/src/app/dashboard/tasks/_components/tasks-dialogs.tsx +71 -0
  69. package/templates/default/apps/admin/src/app/dashboard/tasks/_components/tasks-import-dialog.tsx +106 -0
  70. package/templates/default/apps/admin/src/app/dashboard/tasks/_components/tasks-multi-delete-dialog.tsx +90 -0
  71. package/templates/default/apps/admin/src/app/dashboard/tasks/_components/tasks-mutate-drawer.tsx +207 -0
  72. package/templates/default/apps/admin/src/app/dashboard/tasks/_components/tasks-page.tsx +31 -0
  73. package/templates/default/apps/admin/src/app/dashboard/tasks/_components/tasks-primary-buttons.tsx +19 -0
  74. package/templates/default/apps/admin/src/app/dashboard/tasks/_components/tasks-provider.tsx +37 -0
  75. package/templates/default/apps/admin/src/app/dashboard/tasks/_components/tasks-table.tsx +155 -0
  76. package/templates/default/apps/admin/src/app/dashboard/tasks/page.tsx +14 -0
  77. package/templates/default/apps/admin/src/app/dashboard/users/_components/data-table-bulk-actions.tsx +136 -0
  78. package/templates/default/apps/admin/src/app/dashboard/users/_components/data-table-row-actions.tsx +62 -0
  79. package/templates/default/apps/admin/src/app/dashboard/users/_components/users-action-dialog.tsx +297 -0
  80. package/templates/default/apps/admin/src/app/dashboard/users/_components/users-columns.tsx +121 -0
  81. package/templates/default/apps/admin/src/app/dashboard/users/_components/users-delete-dialog.tsx +72 -0
  82. package/templates/default/apps/admin/src/app/dashboard/users/_components/users-dialogs.tsx +49 -0
  83. package/templates/default/apps/admin/src/app/dashboard/users/_components/users-invite-dialog.tsx +139 -0
  84. package/templates/default/apps/admin/src/app/dashboard/users/_components/users-multi-delete-dialog.tsx +89 -0
  85. package/templates/default/apps/admin/src/app/dashboard/users/_components/users-page.tsx +30 -0
  86. package/templates/default/apps/admin/src/app/dashboard/users/_components/users-primary-buttons.tsx +19 -0
  87. package/templates/default/apps/admin/src/app/dashboard/users/_components/users-provider.tsx +35 -0
  88. package/templates/default/apps/admin/src/app/dashboard/users/_components/users-table.tsx +157 -0
  89. package/templates/default/apps/admin/src/app/dashboard/users/page.tsx +10 -0
  90. package/templates/default/apps/admin/src/app/globals.css +109 -0
  91. package/templates/default/apps/admin/src/app/layout.tsx +32 -0
  92. package/templates/default/apps/admin/src/app/not-found.tsx +14 -0
  93. package/templates/default/apps/admin/src/app/page.tsx +5 -0
  94. package/templates/default/apps/admin/src/components/layout/admin-layout.tsx +16 -0
  95. package/templates/default/apps/admin/src/components/layout/app-sidebar.tsx +52 -0
  96. package/templates/default/apps/admin/src/components/layout/nav-config.ts +131 -0
  97. package/templates/default/apps/admin/src/components/providers/theme-provider.tsx +10 -0
  98. package/templates/default/apps/admin/src/components/shared/long-text.tsx +78 -0
  99. package/templates/default/apps/admin/src/components/shared/search-input.tsx +16 -0
  100. package/templates/default/apps/admin/src/components/shared/select-dropdown.tsx +64 -0
  101. package/templates/default/apps/admin/src/data/apps.tsx +116 -0
  102. package/templates/default/apps/admin/src/data/chats.ts +114 -0
  103. package/templates/default/apps/admin/src/data/tasks.ts +114 -0
  104. package/templates/default/apps/admin/src/data/users.ts +90 -0
  105. package/templates/default/apps/admin/src/hooks/use-dialog-state.ts +17 -0
  106. package/templates/default/apps/admin/src/hooks/use-table-url-state.ts +243 -0
  107. package/templates/default/apps/admin/src/lib/show-submitted-data.tsx +12 -0
  108. package/templates/default/apps/admin/src/types/table.d.ts +9 -0
  109. package/templates/default/apps/admin/tsconfig.json +32 -0
  110. package/templates/default/apps/web/next.config.ts +1 -0
  111. package/templates/default/apps/web/package.json +0 -22
  112. package/templates/default/apps/web/postcss.config.cjs +0 -1
  113. package/templates/default/apps/web/src/app/(authenticated)/dashboard/page.tsx +4 -20
  114. package/templates/default/apps/web/src/app/(authenticated)/data/_components/UserTable.tsx +4 -4
  115. package/templates/default/apps/web/src/app/(authenticated)/data/page.tsx +1 -1
  116. package/templates/default/apps/web/src/app/(authenticated)/profile/page.tsx +1 -1
  117. package/templates/default/apps/web/src/app/error.tsx +8 -70
  118. package/templates/default/apps/web/src/app/global-error.tsx +8 -70
  119. package/templates/default/apps/web/src/app/globals.css +20 -0
  120. package/templates/default/apps/web/src/app/not-found.tsx +5 -39
  121. package/templates/default/apps/web/src/app/page.tsx +27 -203
  122. package/templates/default/apps/web/src/app/signin/page.tsx +27 -191
  123. package/templates/default/apps/web/src/app/signup/page.tsx +33 -240
  124. package/templates/default/apps/web/src/components/dashboard/dashboard-stats.tsx +11 -75
  125. package/templates/default/apps/web/src/components/shared/Sidebar.tsx +3 -3
  126. package/templates/default/apps/web/src/components/shared/header.tsx +17 -112
  127. package/templates/default/apps/web/tsconfig.json +0 -6
  128. package/templates/default/biome.json +1 -2
  129. package/templates/default/components.json +2 -2
  130. package/templates/default/docker-compose.yml +1 -1
  131. package/templates/default/gitignore +4 -0
  132. package/templates/default/package.json +1 -0
  133. package/templates/default/packages/admin-ui/catalog/catalog.css +54 -0
  134. package/templates/default/packages/admin-ui/catalog/catalog.tsx +401 -0
  135. package/templates/default/packages/admin-ui/catalog/index.html +12 -0
  136. package/templates/default/packages/admin-ui/catalog/main.tsx +9 -0
  137. package/templates/default/packages/admin-ui/components.json +21 -0
  138. package/templates/default/packages/admin-ui/package.json +105 -0
  139. package/templates/default/packages/admin-ui/src/command-menu/index.tsx +174 -0
  140. package/templates/default/packages/admin-ui/src/data-table/bulk-actions.tsx +215 -0
  141. package/templates/default/packages/admin-ui/src/data-table/column-header.tsx +73 -0
  142. package/templates/default/packages/admin-ui/src/data-table/data-table.tsx +127 -0
  143. package/templates/default/packages/admin-ui/src/data-table/faceted-filter.tsx +148 -0
  144. package/templates/default/packages/admin-ui/src/data-table/index.tsx +9 -0
  145. package/templates/default/packages/admin-ui/src/data-table/pagination.tsx +101 -0
  146. package/templates/default/packages/admin-ui/src/data-table/toolbar.tsx +87 -0
  147. package/templates/default/packages/admin-ui/src/data-table/view-options.tsx +57 -0
  148. package/templates/default/packages/admin-ui/src/hooks/use-mobile.tsx +23 -0
  149. package/templates/default/packages/admin-ui/src/layout/header.tsx +55 -0
  150. package/templates/default/packages/admin-ui/src/layout/index.ts +10 -0
  151. package/templates/default/packages/admin-ui/src/layout/main.tsx +23 -0
  152. package/templates/default/packages/admin-ui/src/layout/nav-group.tsx +111 -0
  153. package/templates/default/packages/admin-ui/src/layout/nav-user.tsx +114 -0
  154. package/templates/default/packages/admin-ui/src/layout/theme-switch.tsx +40 -0
  155. package/templates/default/packages/admin-ui/src/layout/types.ts +21 -0
  156. package/templates/default/packages/admin-ui/src/lib/utils.ts +6 -0
  157. package/templates/default/packages/admin-ui/src/styles/base.css +65 -0
  158. package/templates/default/packages/admin-ui/src/styles/tokens.css +91 -0
  159. package/templates/default/packages/admin-ui/src/tanstack-table.d.ts +10 -0
  160. package/templates/default/packages/admin-ui/src/ui/alert-dialog.tsx +157 -0
  161. package/templates/default/packages/admin-ui/src/ui/alert.tsx +66 -0
  162. package/templates/default/packages/admin-ui/src/ui/avatar.tsx +53 -0
  163. package/templates/default/packages/admin-ui/src/ui/badge.tsx +46 -0
  164. package/templates/default/packages/admin-ui/src/ui/breadcrumb.tsx +108 -0
  165. package/templates/default/packages/admin-ui/src/ui/button.tsx +59 -0
  166. package/templates/default/packages/admin-ui/src/ui/calendar.tsx +69 -0
  167. package/templates/default/packages/admin-ui/src/ui/card.tsx +92 -0
  168. package/templates/default/packages/admin-ui/src/ui/chart.tsx +345 -0
  169. package/templates/default/packages/admin-ui/src/ui/checkbox.tsx +32 -0
  170. package/templates/default/packages/admin-ui/src/ui/collapsible.tsx +27 -0
  171. package/templates/default/packages/admin-ui/src/ui/command.tsx +161 -0
  172. package/templates/default/packages/admin-ui/src/ui/confirm-dialog.tsx +72 -0
  173. package/templates/default/packages/admin-ui/src/ui/date-picker.tsx +53 -0
  174. package/templates/default/packages/admin-ui/src/ui/dialog.tsx +143 -0
  175. package/templates/default/packages/admin-ui/src/ui/dropdown-menu.tsx +257 -0
  176. package/templates/default/packages/admin-ui/src/ui/form.tsx +168 -0
  177. package/templates/default/packages/admin-ui/src/ui/input-otp.tsx +84 -0
  178. package/templates/default/packages/admin-ui/src/ui/input.tsx +21 -0
  179. package/templates/default/packages/admin-ui/src/ui/label.tsx +24 -0
  180. package/templates/default/packages/admin-ui/src/ui/pagination.tsx +126 -0
  181. package/templates/default/packages/admin-ui/src/ui/password-input.tsx +46 -0
  182. package/templates/default/packages/admin-ui/src/ui/popover.tsx +48 -0
  183. package/templates/default/packages/admin-ui/src/ui/progress.tsx +31 -0
  184. package/templates/default/packages/admin-ui/src/ui/radio-group.tsx +45 -0
  185. package/templates/default/packages/admin-ui/src/ui/scroll-area.tsx +52 -0
  186. package/templates/default/packages/admin-ui/src/ui/select.tsx +185 -0
  187. package/templates/default/packages/admin-ui/src/ui/separator.tsx +28 -0
  188. package/templates/default/packages/admin-ui/src/ui/sheet.tsx +149 -0
  189. package/templates/default/packages/admin-ui/src/ui/sidebar.tsx +728 -0
  190. package/templates/default/packages/admin-ui/src/ui/skeleton.tsx +13 -0
  191. package/templates/default/packages/admin-ui/src/ui/sonner.tsx +25 -0
  192. package/templates/default/packages/admin-ui/src/ui/switch.tsx +31 -0
  193. package/templates/default/packages/admin-ui/src/ui/table.tsx +116 -0
  194. package/templates/default/packages/admin-ui/src/ui/tabs.tsx +66 -0
  195. package/templates/default/packages/admin-ui/src/ui/textarea.tsx +18 -0
  196. package/templates/default/packages/admin-ui/src/ui/toggle-group.tsx +60 -0
  197. package/templates/default/packages/admin-ui/src/ui/toggle.tsx +44 -0
  198. package/templates/default/packages/admin-ui/src/ui/tooltip.tsx +61 -0
  199. package/templates/default/packages/admin-ui/tsconfig.json +8 -0
  200. package/templates/default/packages/admin-ui/vite.config.ts +11 -0
  201. package/templates/default/packages/config/package.json +0 -2
  202. package/templates/default/packages/server-core/package.json +1 -0
  203. package/templates/default/packages/ui/components.json +21 -0
  204. package/templates/default/packages/ui/package.json +42 -5
  205. package/templates/default/packages/ui/src/accordion.tsx +1 -1
  206. package/templates/default/packages/ui/src/alert-dialog.tsx +4 -4
  207. package/templates/default/packages/ui/src/alert.tsx +1 -1
  208. package/templates/default/packages/ui/src/avatar.tsx +1 -1
  209. package/templates/default/packages/ui/src/badge.tsx +1 -1
  210. package/templates/default/packages/ui/src/breadcrumb.tsx +1 -1
  211. package/templates/default/packages/ui/src/button.tsx +1 -1
  212. package/templates/default/packages/ui/src/card.tsx +1 -1
  213. package/templates/default/packages/ui/src/checkbox.tsx +1 -1
  214. package/templates/default/packages/ui/src/dialog.tsx +3 -3
  215. package/templates/default/packages/ui/src/drawer.tsx +3 -3
  216. package/templates/default/packages/ui/src/dropdown-menu.tsx +3 -3
  217. package/templates/default/packages/ui/src/form.tsx +2 -2
  218. package/templates/default/packages/ui/src/hover-card.tsx +2 -2
  219. package/templates/default/packages/ui/src/input.tsx +1 -1
  220. package/templates/default/packages/ui/src/label.tsx +1 -1
  221. package/templates/default/packages/ui/src/pagination.tsx +2 -2
  222. package/templates/default/packages/ui/src/popover.tsx +2 -2
  223. package/templates/default/packages/ui/src/progress.tsx +1 -1
  224. package/templates/default/packages/ui/src/select.tsx +2 -2
  225. package/templates/default/packages/ui/src/separator.tsx +1 -1
  226. package/templates/default/packages/ui/src/skeleton.tsx +1 -1
  227. package/templates/default/packages/ui/src/table.tsx +1 -1
  228. package/templates/default/packages/ui/src/tabs.tsx +1 -1
  229. package/templates/default/packages/ui/src/textarea.tsx +1 -1
  230. package/templates/default/packages/ui/src/tooltip.tsx +3 -3
  231. package/templates/default/packages/ui/src/typography.tsx +1 -1
  232. package/templates/default/packages/ui/tsconfig.json +1 -6
  233. package/templates/default/pnpm-lock.yaml +1319 -936
  234. package/templates/default/postcss.config.cjs +0 -1
  235. package/templates/default/turbo.json +11 -5
  236. package/templates/default/worktree.config.json +5 -0
  237. package/templates/default/.env.ci +0 -32
  238. package/templates/default/.github/workflows/ci.yml +0 -96
  239. package/templates/default/.github/workflows/preview-db.yml +0 -134
  240. package/templates/default/.playwright-mcp/dashboard.png +0 -0
  241. package/templates/default/.playwright-mcp/web-home.png +0 -0
  242. package/templates/default/apps/web/panda.config.ts +0 -114
  243. package/templates/default/apps/web/src/components/ui/accordion.tsx +0 -64
  244. package/templates/default/apps/web/src/components/ui/alert-dialog.tsx +0 -135
  245. package/templates/default/apps/web/src/components/ui/alert.tsx +0 -60
  246. package/templates/default/apps/web/src/components/ui/aspect-ratio.tsx +0 -9
  247. package/templates/default/apps/web/src/components/ui/avatar.tsx +0 -41
  248. package/templates/default/apps/web/src/components/ui/badge.tsx +0 -39
  249. package/templates/default/apps/web/src/components/ui/breadcrumb.tsx +0 -101
  250. package/templates/default/apps/web/src/components/ui/button.tsx +0 -56
  251. package/templates/default/apps/web/src/components/ui/card.tsx +0 -75
  252. package/templates/default/apps/web/src/components/ui/checkbox.tsx +0 -29
  253. package/templates/default/apps/web/src/components/ui/data-table.tsx +0 -189
  254. package/templates/default/apps/web/src/components/ui/dialog-hook.tsx +0 -210
  255. package/templates/default/apps/web/src/components/ui/dialog.tsx +0 -129
  256. package/templates/default/apps/web/src/components/ui/drawer.tsx +0 -124
  257. package/templates/default/apps/web/src/components/ui/dropdown-menu.tsx +0 -228
  258. package/templates/default/apps/web/src/components/ui/form.tsx +0 -152
  259. package/templates/default/apps/web/src/components/ui/hover-card.tsx +0 -38
  260. package/templates/default/apps/web/src/components/ui/input.tsx +0 -21
  261. package/templates/default/apps/web/src/components/ui/label.tsx +0 -21
  262. package/templates/default/apps/web/src/components/ui/pagination.tsx +0 -105
  263. package/templates/default/apps/web/src/components/ui/popover.tsx +0 -42
  264. package/templates/default/apps/web/src/components/ui/progress.tsx +0 -28
  265. package/templates/default/apps/web/src/components/ui/select.tsx +0 -170
  266. package/templates/default/apps/web/src/components/ui/separator.tsx +0 -28
  267. package/templates/default/apps/web/src/components/ui/skeleton.tsx +0 -13
  268. package/templates/default/apps/web/src/components/ui/sonner.tsx +0 -25
  269. package/templates/default/apps/web/src/components/ui/table.tsx +0 -92
  270. package/templates/default/apps/web/src/components/ui/tabs.tsx +0 -54
  271. package/templates/default/apps/web/src/components/ui/textarea.tsx +0 -18
  272. package/templates/default/apps/web/src/components/ui/tooltip.tsx +0 -57
  273. package/templates/default/apps/web/src/components/ui/typography.tsx +0 -158
  274. package/templates/default/packages/config/panda.config.ts +0 -114
  275. package/templates/default/panda.config.ts +0 -114
@@ -0,0 +1,174 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { useTheme } from "next-themes";
6
+ import { Monitor, Moon, Sun } from "lucide-react";
7
+ import type { NavGroup } from "../layout/types";
8
+ import {
9
+ CommandDialog,
10
+ CommandEmpty,
11
+ CommandGroup,
12
+ CommandInput,
13
+ CommandItem,
14
+ CommandList,
15
+ CommandSeparator,
16
+ } from "../ui/command";
17
+
18
+ interface CommandMenuProps {
19
+ navGroups: NavGroup[];
20
+ open?: boolean;
21
+ onOpenChange?: (open: boolean) => void;
22
+ }
23
+
24
+ interface FlatNavItem {
25
+ title: string;
26
+ url: string;
27
+ icon?: React.ComponentType<{ className?: string }>;
28
+ group: string;
29
+ parentTitle?: string;
30
+ }
31
+
32
+ function CommandMenu({ navGroups, open: controlledOpen, onOpenChange }: CommandMenuProps) {
33
+ const [internalOpen, setInternalOpen] = React.useState(false);
34
+ const router = useRouter();
35
+ const { setTheme } = useTheme();
36
+
37
+ // 制御されている場合は親の状態、そうでない場合は内部状態を使用
38
+ const open = controlledOpen !== undefined ? controlledOpen : internalOpen;
39
+ const setOpen = onOpenChange || setInternalOpen;
40
+
41
+ React.useEffect(() => {
42
+ const down = (e: KeyboardEvent) => {
43
+ if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
44
+ e.preventDefault();
45
+ if (onOpenChange) {
46
+ onOpenChange(!open);
47
+ } else {
48
+ setInternalOpen((prev) => !prev);
49
+ }
50
+ }
51
+ };
52
+
53
+ document.addEventListener("keydown", down);
54
+ return () => document.removeEventListener("keydown", down);
55
+ }, [open, onOpenChange]);
56
+
57
+ const handleSelect = React.useCallback(
58
+ (url: string) => {
59
+ setOpen(false);
60
+ router.push(url);
61
+ },
62
+ [router]
63
+ );
64
+
65
+ const handleThemeChange = React.useCallback(
66
+ (theme: string) => {
67
+ setOpen(false);
68
+ setTheme(theme);
69
+ },
70
+ [setTheme]
71
+ );
72
+
73
+ // navGroupsをフラット化して全てのページをリストアップ
74
+ const flatItems = React.useMemo(() => {
75
+ const items: FlatNavItem[] = [];
76
+
77
+ navGroups.forEach((group) => {
78
+ group.items.forEach((item) => {
79
+ if (item.url) {
80
+ // 直接URLを持つアイテム
81
+ items.push({
82
+ title: item.title,
83
+ url: item.url,
84
+ icon: item.icon,
85
+ group: group.title,
86
+ });
87
+ }
88
+ // サブアイテムがある場合
89
+ if (item.items && item.items.length > 0) {
90
+ item.items.forEach((subItem) => {
91
+ items.push({
92
+ title: subItem.title,
93
+ url: subItem.url,
94
+ icon: item.icon,
95
+ group: group.title,
96
+ parentTitle: item.title,
97
+ });
98
+ });
99
+ }
100
+ });
101
+ });
102
+
103
+ return items;
104
+ }, [navGroups]);
105
+
106
+ // グループごとに整理
107
+ const groupedItems = React.useMemo(() => {
108
+ const groups = new Map<string, FlatNavItem[]>();
109
+
110
+ flatItems.forEach((item) => {
111
+ const key = item.group;
112
+ if (!groups.has(key)) {
113
+ groups.set(key, []);
114
+ }
115
+ groups.get(key)?.push(item);
116
+ });
117
+
118
+ return Array.from(groups.entries());
119
+ }, [flatItems]);
120
+
121
+ return (
122
+ <CommandDialog open={open} onOpenChange={setOpen}>
123
+ <CommandInput placeholder="Type a command or search..." />
124
+ <CommandList>
125
+ <CommandEmpty>No results found.</CommandEmpty>
126
+
127
+ {/* ページナビゲーション */}
128
+ {groupedItems.map(([groupTitle, items], index) => (
129
+ <React.Fragment key={groupTitle}>
130
+ {index > 0 && <CommandSeparator />}
131
+ <CommandGroup heading={groupTitle}>
132
+ {items.map((item) => {
133
+ const displayTitle = item.parentTitle
134
+ ? `${item.parentTitle} > ${item.title}`
135
+ : item.title;
136
+
137
+ return (
138
+ <CommandItem
139
+ key={item.url}
140
+ value={`${groupTitle} ${displayTitle}`}
141
+ onSelect={() => handleSelect(item.url)}
142
+ >
143
+ {item.icon && <item.icon className="mr-2 h-4 w-4" />}
144
+ <span>{displayTitle}</span>
145
+ </CommandItem>
146
+ );
147
+ })}
148
+ </CommandGroup>
149
+ </React.Fragment>
150
+ ))}
151
+
152
+ {/* テーマ切り替え */}
153
+ <CommandSeparator />
154
+ <CommandGroup heading="Theme">
155
+ <CommandItem value="theme light" onSelect={() => handleThemeChange("light")}>
156
+ <Sun className="mr-2 h-4 w-4" />
157
+ <span>Light</span>
158
+ </CommandItem>
159
+ <CommandItem value="theme dark" onSelect={() => handleThemeChange("dark")}>
160
+ <Moon className="mr-2 h-4 w-4" />
161
+ <span>Dark</span>
162
+ </CommandItem>
163
+ <CommandItem value="theme system" onSelect={() => handleThemeChange("system")}>
164
+ <Monitor className="mr-2 h-4 w-4" />
165
+ <span>System</span>
166
+ </CommandItem>
167
+ </CommandGroup>
168
+ </CommandList>
169
+ </CommandDialog>
170
+ );
171
+ }
172
+
173
+ export { CommandMenu };
174
+ export type { CommandMenuProps };
@@ -0,0 +1,215 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect, useRef } from "react";
4
+ import type { Table } from "@tanstack/react-table";
5
+ import { X } from "lucide-react";
6
+ import { cn } from "../lib/utils";
7
+ import { Badge } from "../ui/badge";
8
+ import { Button } from "../ui/button";
9
+ import { Separator } from "../ui/separator";
10
+ import {
11
+ Tooltip,
12
+ TooltipContent,
13
+ TooltipTrigger,
14
+ } from "../ui/tooltip";
15
+
16
+ type DataTableBulkActionsProps<TData> = {
17
+ table: Table<TData>;
18
+ entityName: string;
19
+ children: React.ReactNode;
20
+ };
21
+
22
+ /**
23
+ * A modular toolbar for displaying bulk actions when table rows are selected.
24
+ *
25
+ * @template TData The type of data in the table.
26
+ * @param {object} props The component props.
27
+ * @param {Table<TData>} props.table The react-table instance.
28
+ * @param {string} props.entityName The name of the entity being acted upon (e.g., "task", "user").
29
+ * @param {React.ReactNode} props.children The action buttons to be rendered inside the toolbar.
30
+ * @returns {React.ReactNode | null} The rendered component or null if no rows are selected.
31
+ */
32
+ export function DataTableBulkActions<TData>({
33
+ table,
34
+ entityName,
35
+ children,
36
+ }: DataTableBulkActionsProps<TData>): React.ReactNode | null {
37
+ const selectedRows = table.getFilteredSelectedRowModel().rows;
38
+ const selectedCount = selectedRows.length;
39
+ const toolbarRef = useRef<HTMLDivElement>(null);
40
+ const [announcement, setAnnouncement] = useState("");
41
+
42
+ // Announce selection changes to screen readers
43
+ useEffect(() => {
44
+ if (selectedCount > 0) {
45
+ const message = `${selectedCount} ${entityName}${selectedCount > 1 ? "s" : ""} selected. Bulk actions toolbar is available.`;
46
+
47
+ // Use queueMicrotask to defer state update and avoid cascading renders
48
+ queueMicrotask(() => {
49
+ setAnnouncement(message);
50
+ });
51
+
52
+ // Clear announcement after a delay
53
+ const timer = setTimeout(() => setAnnouncement(""), 3000);
54
+ return () => clearTimeout(timer);
55
+ }
56
+ }, [selectedCount, entityName]);
57
+
58
+ const handleClearSelection = () => {
59
+ table.resetRowSelection();
60
+ };
61
+
62
+ const handleKeyDown = (event: React.KeyboardEvent) => {
63
+ const buttons = toolbarRef.current?.querySelectorAll("button");
64
+ if (!buttons) return;
65
+
66
+ const currentIndex = Array.from(buttons).findIndex(
67
+ (button) => button === document.activeElement
68
+ );
69
+
70
+ switch (event.key) {
71
+ case "ArrowRight": {
72
+ event.preventDefault();
73
+ const nextIndex = (currentIndex + 1) % buttons.length;
74
+ buttons[nextIndex]?.focus();
75
+ break;
76
+ }
77
+ case "ArrowLeft": {
78
+ event.preventDefault();
79
+ const prevIndex =
80
+ currentIndex === 0 ? buttons.length - 1 : currentIndex - 1;
81
+ buttons[prevIndex]?.focus();
82
+ break;
83
+ }
84
+ case "Home":
85
+ event.preventDefault();
86
+ buttons[0]?.focus();
87
+ break;
88
+ case "End":
89
+ event.preventDefault();
90
+ buttons[buttons.length - 1]?.focus();
91
+ break;
92
+ case "Escape": {
93
+ // Check if the Escape key came from a dropdown trigger or content
94
+ // We can't check dropdown state because Radix UI closes it before our handler runs
95
+ const target = event.target as HTMLElement;
96
+ const activeElement = document.activeElement as HTMLElement;
97
+
98
+ // Check if the event target or currently focused element is a dropdown trigger
99
+ const isFromDropdownTrigger =
100
+ target?.getAttribute("data-slot") === "dropdown-menu-trigger" ||
101
+ activeElement?.getAttribute("data-slot") ===
102
+ "dropdown-menu-trigger" ||
103
+ target?.closest('[data-slot="dropdown-menu-trigger"]') ||
104
+ activeElement?.closest('[data-slot="dropdown-menu-trigger"]');
105
+
106
+ // Check if the focused element is inside dropdown content (which is portaled)
107
+ const isFromDropdownContent =
108
+ activeElement?.closest('[data-slot="dropdown-menu-content"]') ||
109
+ target?.closest('[data-slot="dropdown-menu-content"]');
110
+
111
+ if (isFromDropdownTrigger || isFromDropdownContent) {
112
+ // Escape was meant for the dropdown - don't clear selection
113
+ return;
114
+ }
115
+
116
+ // Escape was meant for the toolbar - clear selection
117
+ event.preventDefault();
118
+ handleClearSelection();
119
+ break;
120
+ }
121
+ }
122
+ };
123
+
124
+ if (selectedCount === 0) {
125
+ return null;
126
+ }
127
+
128
+ return (
129
+ <>
130
+ {/* Live region for screen reader announcements */}
131
+ <div
132
+ aria-live="polite"
133
+ aria-atomic="true"
134
+ className="sr-only"
135
+ role="status"
136
+ >
137
+ {announcement}
138
+ </div>
139
+
140
+ <div
141
+ ref={toolbarRef}
142
+ role="toolbar"
143
+ aria-label={`Bulk actions for ${selectedCount} selected ${entityName}${selectedCount > 1 ? "s" : ""}`}
144
+ aria-describedby="bulk-actions-description"
145
+ tabIndex={-1}
146
+ onKeyDown={handleKeyDown}
147
+ className={cn(
148
+ "fixed bottom-6 left-1/2 z-[var(--z-header)] -translate-x-1/2 rounded-xl",
149
+ "transition-all delay-100 duration-300 ease-out hover:scale-105",
150
+ "focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:outline-none"
151
+ )}
152
+ >
153
+ <div
154
+ className={cn(
155
+ "p-2 shadow-xl",
156
+ "rounded-xl border",
157
+ "bg-background/95 backdrop-blur-lg supports-backdrop-filter:bg-background/60",
158
+ "flex items-center gap-x-2"
159
+ )}
160
+ >
161
+ <Tooltip>
162
+ <TooltipTrigger asChild>
163
+ <Button
164
+ variant="outline"
165
+ size="icon"
166
+ onClick={handleClearSelection}
167
+ className="size-6 rounded-full"
168
+ aria-label="Clear selection"
169
+ title="Clear selection (Escape)"
170
+ >
171
+ <X />
172
+ <span className="sr-only">Clear selection</span>
173
+ </Button>
174
+ </TooltipTrigger>
175
+ <TooltipContent>
176
+ <p>Clear selection (Escape)</p>
177
+ </TooltipContent>
178
+ </Tooltip>
179
+
180
+ <Separator
181
+ className="h-5"
182
+ orientation="vertical"
183
+ aria-hidden="true"
184
+ />
185
+
186
+ <div
187
+ className="flex items-center gap-x-1 text-sm"
188
+ id="bulk-actions-description"
189
+ >
190
+ <Badge
191
+ variant="default"
192
+ className="min-w-8 rounded-lg"
193
+ aria-label={`${selectedCount} selected`}
194
+ >
195
+ {selectedCount}
196
+ </Badge>{" "}
197
+ <span className="hidden sm:inline">
198
+ {entityName}
199
+ {selectedCount > 1 ? "s" : ""}
200
+ </span>{" "}
201
+ selected
202
+ </div>
203
+
204
+ <Separator
205
+ className="h-5"
206
+ orientation="vertical"
207
+ aria-hidden="true"
208
+ />
209
+
210
+ {children}
211
+ </div>
212
+ </div>
213
+ </>
214
+ );
215
+ }
@@ -0,0 +1,73 @@
1
+ import {
2
+ ArrowDownIcon,
3
+ ArrowUpIcon,
4
+ ChevronsUpDownIcon,
5
+ EyeOffIcon,
6
+ } from "lucide-react";
7
+ import type { Column } from "@tanstack/react-table";
8
+
9
+ import { cn } from "../lib/utils";
10
+ import { Button } from "../ui/button";
11
+ import {
12
+ DropdownMenu,
13
+ DropdownMenuContent,
14
+ DropdownMenuItem,
15
+ DropdownMenuSeparator,
16
+ DropdownMenuTrigger,
17
+ } from "../ui/dropdown-menu";
18
+
19
+ interface DataTableColumnHeaderProps<TData, TValue>
20
+ extends React.HTMLAttributes<HTMLDivElement> {
21
+ column: Column<TData, TValue>;
22
+ title: string;
23
+ }
24
+
25
+ function DataTableColumnHeader<TData, TValue>({
26
+ column,
27
+ title,
28
+ className,
29
+ }: DataTableColumnHeaderProps<TData, TValue>) {
30
+ if (!column.getCanSort()) {
31
+ return <div className={cn(className)}>{title}</div>;
32
+ }
33
+
34
+ return (
35
+ <div className={cn("flex items-center space-x-2", className)}>
36
+ <DropdownMenu>
37
+ <DropdownMenuTrigger asChild>
38
+ <Button
39
+ variant="ghost"
40
+ size="sm"
41
+ className="-ml-3 h-8 data-[state=open]:bg-accent"
42
+ >
43
+ <span>{title}</span>
44
+ {column.getIsSorted() === "desc" ? (
45
+ <ArrowDownIcon className="ml-2 h-4 w-4" />
46
+ ) : column.getIsSorted() === "asc" ? (
47
+ <ArrowUpIcon className="ml-2 h-4 w-4" />
48
+ ) : (
49
+ <ChevronsUpDownIcon className="ml-2 h-4 w-4" />
50
+ )}
51
+ </Button>
52
+ </DropdownMenuTrigger>
53
+ <DropdownMenuContent align="start">
54
+ <DropdownMenuItem onClick={() => column.toggleSorting(false)}>
55
+ <ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
56
+ Asc
57
+ </DropdownMenuItem>
58
+ <DropdownMenuItem onClick={() => column.toggleSorting(true)}>
59
+ <ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
60
+ Desc
61
+ </DropdownMenuItem>
62
+ <DropdownMenuSeparator />
63
+ <DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
64
+ <EyeOffIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
65
+ Hide
66
+ </DropdownMenuItem>
67
+ </DropdownMenuContent>
68
+ </DropdownMenu>
69
+ </div>
70
+ );
71
+ }
72
+
73
+ export { DataTableColumnHeader };
@@ -0,0 +1,127 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ColumnDef,
5
+ type ColumnFiltersState,
6
+ type SortingState,
7
+ type VisibilityState,
8
+ flexRender,
9
+ getCoreRowModel,
10
+ getFacetedRowModel,
11
+ getFacetedUniqueValues,
12
+ getFilteredRowModel,
13
+ getPaginationRowModel,
14
+ getSortedRowModel,
15
+ useReactTable,
16
+ } from "@tanstack/react-table";
17
+ import * as React from "react";
18
+
19
+ import {
20
+ Table,
21
+ TableBody,
22
+ TableCell,
23
+ TableHead,
24
+ TableHeader,
25
+ TableRow,
26
+ } from "../ui/table";
27
+ import { DataTablePagination } from "./pagination";
28
+
29
+ interface DataTableProps<TData, TValue> {
30
+ columns: ColumnDef<TData, TValue>[];
31
+ data: TData[];
32
+ toolbar?: React.ReactNode;
33
+ }
34
+
35
+ function DataTable<TData, TValue>({
36
+ columns,
37
+ data,
38
+ toolbar,
39
+ }: DataTableProps<TData, TValue>) {
40
+ const [rowSelection, setRowSelection] = React.useState({});
41
+ const [columnVisibility, setColumnVisibility] =
42
+ React.useState<VisibilityState>({});
43
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(
44
+ []
45
+ );
46
+ const [sorting, setSorting] = React.useState<SortingState>([]);
47
+
48
+ const table = useReactTable({
49
+ data,
50
+ columns,
51
+ state: {
52
+ sorting,
53
+ columnVisibility,
54
+ rowSelection,
55
+ columnFilters,
56
+ },
57
+ enableRowSelection: true,
58
+ onRowSelectionChange: setRowSelection,
59
+ onSortingChange: setSorting,
60
+ onColumnFiltersChange: setColumnFilters,
61
+ onColumnVisibilityChange: setColumnVisibility,
62
+ getCoreRowModel: getCoreRowModel(),
63
+ getFilteredRowModel: getFilteredRowModel(),
64
+ getPaginationRowModel: getPaginationRowModel(),
65
+ getSortedRowModel: getSortedRowModel(),
66
+ getFacetedRowModel: getFacetedRowModel(),
67
+ getFacetedUniqueValues: getFacetedUniqueValues(),
68
+ });
69
+
70
+ return (
71
+ <div className="space-y-4">
72
+ {toolbar}
73
+ <div className="rounded-md border">
74
+ <Table>
75
+ <TableHeader>
76
+ {table.getHeaderGroups().map((headerGroup) => (
77
+ <TableRow key={headerGroup.id}>
78
+ {headerGroup.headers.map((header) => (
79
+ <TableHead key={header.id} colSpan={header.colSpan}>
80
+ {header.isPlaceholder
81
+ ? null
82
+ : flexRender(
83
+ header.column.columnDef.header,
84
+ header.getContext()
85
+ )}
86
+ </TableHead>
87
+ ))}
88
+ </TableRow>
89
+ ))}
90
+ </TableHeader>
91
+ <TableBody>
92
+ {table.getRowModel().rows?.length ? (
93
+ table.getRowModel().rows.map((row) => (
94
+ <TableRow
95
+ key={row.id}
96
+ data-state={row.getIsSelected() && "selected"}
97
+ >
98
+ {row.getVisibleCells().map((cell) => (
99
+ <TableCell key={cell.id}>
100
+ {flexRender(
101
+ cell.column.columnDef.cell,
102
+ cell.getContext()
103
+ )}
104
+ </TableCell>
105
+ ))}
106
+ </TableRow>
107
+ ))
108
+ ) : (
109
+ <TableRow>
110
+ <TableCell
111
+ colSpan={columns.length}
112
+ className="h-24 text-center"
113
+ >
114
+ No results.
115
+ </TableCell>
116
+ </TableRow>
117
+ )}
118
+ </TableBody>
119
+ </Table>
120
+ </div>
121
+ <DataTablePagination table={table} />
122
+ </div>
123
+ );
124
+ }
125
+
126
+ export { DataTable };
127
+ export type { DataTableProps };