@nixxie-cms/core 1.0.0 → 1.0.2

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 (187) hide show
  1. package/README.md +2 -2
  2. package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.cjs.js +4 -4
  3. package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.esm.js +4 -4
  4. package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.cjs.js +2 -2
  5. package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.esm.js +2 -2
  6. package/context/dist/nixxie-cms-core-context.cjs.js +2 -2
  7. package/context/dist/nixxie-cms-core-context.esm.js +2 -2
  8. package/dist/{CreateItemDialog-33335548.esm.js → CreateItemDialog-7008b050.esm.js} +1 -1
  9. package/dist/{CreateItemDialog-56cf59b7.cjs.js → CreateItemDialog-a0cab315.cjs.js} +1 -1
  10. package/dist/{PageContainer-7db73317.esm.js → PageContainer-5ae731cc.esm.js} +25 -18
  11. package/dist/{PageContainer-27c27f10.cjs.js → PageContainer-abd7159f.cjs.js} +25 -18
  12. package/dist/{admin-meta-graphql-6f7f5331.esm.js → admin-meta-graphql-0e6e606e.esm.js} +1 -1
  13. package/dist/{admin-meta-graphql-c8f926e9.cjs.js → admin-meta-graphql-306c224a.cjs.js} +1 -1
  14. package/dist/{context-3132c3ed.esm.js → context-af9957ed.esm.js} +2 -2
  15. package/dist/{context-e7a45152.cjs.js → context-b5204629.cjs.js} +2 -2
  16. package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
  17. package/dist/declarations/src/admin-ui/components/PageContainer.d.ts.map +1 -1
  18. package/dist/declarations/src/helpers.d.ts.map +1 -1
  19. package/dist/declarations/src/index.d.ts +1 -0
  20. package/dist/declarations/src/index.d.ts.map +1 -1
  21. package/dist/declarations/src/internal-unstable/admin-ui/id-field-view.d.ts.map +1 -0
  22. package/dist/declarations/src/internal-unstable/admin-ui/pages/App/index.d.ts.map +1 -0
  23. package/dist/declarations/src/internal-unstable/admin-ui/pages/CreateItemPage/index.d.ts.map +1 -0
  24. package/dist/declarations/src/internal-unstable/admin-ui/pages/HomePage/index.d.ts.map +1 -0
  25. package/dist/declarations/src/internal-unstable/admin-ui/pages/ItemPage/index.d.ts.map +1 -0
  26. package/dist/declarations/src/internal-unstable/admin-ui/pages/ListPage/index.d.ts.map +1 -0
  27. package/dist/declarations/src/internal-unstable/admin-ui/pages/NoAccessPage/index.d.ts.map +1 -0
  28. package/dist/declarations/src/internal-unstable/artifacts.d.ts.map +1 -0
  29. package/dist/declarations/src/lib/core/initialise-lists.d.ts +1 -1
  30. package/dist/declarations/src/schema.d.ts.map +1 -1
  31. package/dist/declarations/src/types/config/index.d.ts +60 -1
  32. package/dist/declarations/src/types/config/index.d.ts.map +1 -1
  33. package/dist/declarations/src/types/config/lists.d.ts +4 -4
  34. package/dist/declarations/src/types/context.d.ts +150 -0
  35. package/dist/declarations/src/types/context.d.ts.map +1 -1
  36. package/dist/declarations/src/types/next-fields.d.ts +1 -1
  37. package/dist/{express-e9ed9a7d.cjs.js → express-455ae20c.cjs.js} +1 -1
  38. package/dist/{express-6743b918.esm.js → express-7559ca2d.esm.js} +1 -1
  39. package/dist/{index-ac01583b.cjs.js → index-89635494.cjs.js} +4 -4
  40. package/dist/{index-24b78415.esm.js → index-baa799e0.esm.js} +4 -4
  41. package/dist/nixxie-cms-core.cjs.js +104 -77
  42. package/dist/nixxie-cms-core.esm.js +104 -77
  43. package/dist/{non-null-graphql-5315718c.esm.js → non-null-graphql-a84ed64d.esm.js} +1 -1
  44. package/dist/{non-null-graphql-17b83ddc.cjs.js → non-null-graphql-add6bb3d.cjs.js} +1 -1
  45. package/dist/{resolve-hooks-66fe8a8e.cjs.js → resolve-hooks-165a9ce2.cjs.js} +1 -1
  46. package/dist/{resolve-hooks-17aafd37.esm.js → resolve-hooks-6813a045.esm.js} +2 -2
  47. package/dist/{system-dfec2f0a.esm.js → system-03e49e4f.esm.js} +8 -4
  48. package/dist/{system-48c5f6df.cjs.js → system-a321642d.cjs.js} +8 -4
  49. package/dist/{useFilter-0b5a1ee6.esm.js → useFilter-9b6db1f9.esm.js} +1 -1
  50. package/dist/{useFilter-1a4e6900.cjs.js → useFilter-acc9d413.cjs.js} +1 -1
  51. package/fields/dist/nixxie-cms-core-fields.cjs.js +16 -16
  52. package/fields/dist/nixxie-cms-core-fields.esm.js +17 -17
  53. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.cjs.js +3 -3
  54. package/fields/types/bytes/dist/nixxie-cms-core-fields-types-bytes.esm.js +3 -3
  55. package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.cjs.js +1 -1
  56. package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.esm.js +1 -1
  57. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.cjs.js +3 -3
  58. package/fields/types/password/dist/nixxie-cms-core-fields-types-password.esm.js +3 -3
  59. package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.cjs.js +4 -4
  60. package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.esm.js +4 -4
  61. package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.cjs.js +1 -1
  62. package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.esm.js +1 -1
  63. package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.cjs.js +1 -1
  64. package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.esm.js +1 -1
  65. package/internal-unstable/admin-ui/id-field-view/dist/nixxie-cms-core-internal-unstable-admin-ui-id-field-view.cjs.d.ts +2 -0
  66. package/internal-unstable/admin-ui/id-field-view/dist/nixxie-cms-core-internal-unstable-admin-ui-id-field-view.cjs.js +244 -0
  67. package/internal-unstable/admin-ui/id-field-view/dist/nixxie-cms-core-internal-unstable-admin-ui-id-field-view.esm.js +235 -0
  68. package/internal-unstable/admin-ui/id-field-view/package.json +4 -0
  69. package/internal-unstable/admin-ui/next-config/package.json +4 -0
  70. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.cjs.d.ts +2 -0
  71. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.cjs.js +59 -0
  72. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.esm.js +55 -0
  73. package/internal-unstable/admin-ui/pages/App/package.json +4 -0
  74. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.cjs.d.ts +2 -0
  75. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.cjs.js +116 -0
  76. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.esm.js +112 -0
  77. package/internal-unstable/admin-ui/pages/CreateItemPage/package.json +4 -0
  78. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.cjs.d.ts +2 -0
  79. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.cjs.js +336 -0
  80. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.esm.js +332 -0
  81. package/internal-unstable/admin-ui/pages/HomePage/package.json +4 -0
  82. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.cjs.d.ts +2 -0
  83. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.cjs.js +463 -0
  84. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.esm.js +455 -0
  85. package/internal-unstable/admin-ui/pages/ItemPage/package.json +4 -0
  86. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.cjs.d.ts +2 -0
  87. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.cjs.js +1195 -0
  88. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.esm.js +1187 -0
  89. package/internal-unstable/admin-ui/pages/ListPage/package.json +4 -0
  90. package/internal-unstable/admin-ui/pages/NoAccessPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-NoAccessPage.cjs.d.ts +2 -0
  91. package/internal-unstable/admin-ui/pages/NoAccessPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-NoAccessPage.cjs.js +40 -0
  92. package/internal-unstable/admin-ui/pages/NoAccessPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-NoAccessPage.esm.js +35 -0
  93. package/internal-unstable/admin-ui/pages/NoAccessPage/package.json +4 -0
  94. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.d.ts +2 -0
  95. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.cjs.js +51 -0
  96. package/internal-unstable/artifacts/dist/nixxie-cms-core-internal-unstable-artifacts.esm.js +38 -0
  97. package/internal-unstable/artifacts/package.json +4 -0
  98. package/package.json +44 -44
  99. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.cjs.js +44 -15
  100. package/scripts/cli/dist/nixxie-cms-core-scripts-cli.esm.js +44 -15
  101. package/scripts/dist/nixxie-cms-core-scripts.cjs.js +3 -3
  102. package/scripts/dist/nixxie-cms-core-scripts.esm.js +3 -3
  103. package/src/admin-ui/admin-meta-graphql.ts +168 -168
  104. package/src/admin-ui/components/CommandPalette.tsx +433 -431
  105. package/src/admin-ui/components/Navigation.tsx +389 -385
  106. package/src/admin-ui/components/PageContainer.tsx +311 -310
  107. package/src/admin-ui/components/WelcomeDialog.tsx +1 -1
  108. package/src/admin-ui/context.tsx +338 -338
  109. package/src/admin-ui/templates/app.ts +60 -60
  110. package/src/admin-ui/templates/create-item.ts +5 -5
  111. package/src/admin-ui/templates/home.ts +2 -2
  112. package/src/admin-ui/templates/item.tsx +5 -5
  113. package/src/admin-ui/templates/list.tsx +5 -5
  114. package/src/admin-ui/templates/next-config.ts +29 -0
  115. package/src/admin-ui/templates/no-access.ts +7 -7
  116. package/src/fields/types/bigInt/index.ts +181 -181
  117. package/src/fields/types/bytes/index.ts +275 -275
  118. package/src/fields/types/calendarDay/index.ts +194 -194
  119. package/src/fields/types/checkbox/index.ts +76 -76
  120. package/src/fields/types/decimal/index.ts +182 -182
  121. package/src/fields/types/file/index.ts +168 -168
  122. package/src/fields/types/float/index.ts +133 -133
  123. package/src/fields/types/image/index.ts +244 -244
  124. package/src/fields/types/integer/index.ts +156 -156
  125. package/src/fields/types/json/index.ts +77 -77
  126. package/src/fields/types/multiselect/index.ts +212 -212
  127. package/src/fields/types/password/index.ts +241 -241
  128. package/src/fields/types/relationship/index.ts +381 -381
  129. package/src/fields/types/relationship/views/RelationshipTable.tsx +190 -190
  130. package/src/fields/types/select/index.ts +226 -226
  131. package/src/fields/types/text/index.ts +207 -207
  132. package/src/fields/types/timestamp/index.ts +116 -116
  133. package/src/fields/types/virtual/index.ts +108 -108
  134. package/src/helpers.ts +342 -316
  135. package/src/index.ts +4 -0
  136. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/id-field-view.tsx +167 -167
  137. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/App/index.tsx +22 -22
  138. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/CreateItemPage/index.tsx +71 -71
  139. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/HomePage/index.tsx +333 -333
  140. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/common.tsx +358 -358
  141. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/index.tsx +483 -483
  142. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/FilterAdd.tsx +221 -221
  143. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/PaginationControls.tsx +170 -170
  144. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/Tag.tsx +72 -72
  145. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/index.tsx +1006 -1006
  146. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/NoAccessPage/index.tsx +24 -24
  147. package/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/artifacts.ts +5 -5
  148. package/src/lib/context/createContext.ts +165 -161
  149. package/src/lib/core/initialise-lists.ts +1097 -1097
  150. package/src/lib/id-field.ts +214 -214
  151. package/src/lib/telemetry.ts +342 -342
  152. package/src/schema.ts +237 -233
  153. package/src/scripts/telemetry.ts +1 -1
  154. package/src/types/config/index.ts +400 -333
  155. package/src/types/config/lists.ts +4 -4
  156. package/src/types/context.ts +700 -530
  157. package/src/types/next-fields.ts +499 -499
  158. package/src/types/telemetry.ts +51 -51
  159. package/tests/telemetry.test.ts +361 -361
  160. package/CHANGELOG.md +0 -3158
  161. package/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view/package.json +0 -4
  162. package/___internal-do-not-use-will-break-in-patch/admin-ui/next-config/package.json +0 -4
  163. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/package.json +0 -4
  164. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/package.json +0 -4
  165. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage/package.json +0 -4
  166. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/package.json +0 -4
  167. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/package.json +0 -4
  168. package/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage/package.json +0 -4
  169. package/___internal-do-not-use-will-break-in-patch/artifacts/package.json +0 -4
  170. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/id-field-view.d.ts.map +0 -1
  171. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/App/index.d.ts.map +0 -1
  172. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/CreateItemPage/index.d.ts.map +0 -1
  173. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/HomePage/index.d.ts.map +0 -1
  174. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ItemPage/index.d.ts.map +0 -1
  175. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/ListPage/index.d.ts.map +0 -1
  176. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/admin-ui/pages/NoAccessPage/index.d.ts.map +0 -1
  177. package/dist/declarations/src/___internal-do-not-use-will-break-in-patch/artifacts.d.ts.map +0 -1
  178. /package/dist/{common-1a350e11.cjs.js → common-5933f758.cjs.js} +0 -0
  179. /package/dist/{common-29fc82e6.esm.js → common-ea5c441a.esm.js} +0 -0
  180. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/id-field-view.d.ts +0 -0
  181. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/App/index.d.ts +0 -0
  182. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/CreateItemPage/index.d.ts +0 -0
  183. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/HomePage/index.d.ts +0 -0
  184. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ItemPage/index.d.ts +0 -0
  185. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/ListPage/index.d.ts +0 -0
  186. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/admin-ui/pages/NoAccessPage/index.d.ts +0 -0
  187. /package/dist/declarations/src/{___internal-do-not-use-will-break-in-patch → internal-unstable}/artifacts.d.ts +0 -0
@@ -1,431 +1,433 @@
1
- import { useRouter } from 'next/router'
2
- import {
3
- type KeyboardEvent,
4
- type ChangeEvent,
5
- useEffect,
6
- useRef,
7
- useState,
8
- useMemo,
9
- } from 'react'
10
-
11
- import { css } from '@keystar/ui/style'
12
-
13
- import { useNixxie } from '../context'
14
- import { getHrefFromList } from './Navigation'
15
-
16
- type Command = {
17
- id: string
18
- label: string
19
- description?: string
20
- href?: string
21
- action?: () => void
22
- icon: string
23
- category: string
24
- }
25
-
26
- function useCommands(): Command[] {
27
- const { lists } = useNixxie()
28
-
29
- return useMemo(() => {
30
- const cmds: Command[] = [
31
- {
32
- id: 'nav-dashboard',
33
- label: 'Go to Dashboard',
34
- category: 'Navigate',
35
- icon: '◼',
36
- href: '/',
37
- },
38
- ]
39
-
40
- for (const list of Object.values(lists)) {
41
- if (list.hideNavigation) continue
42
-
43
- cmds.push({
44
- id: `nav-${list.key}`,
45
- label: list.label,
46
- description: `Open ${list.label} list`,
47
- category: 'Navigate',
48
- icon: '≡',
49
- href: getHrefFromList(list),
50
- })
51
-
52
- if (!list.hideCreate && !list.isSingleton) {
53
- cmds.push({
54
- id: `create-${list.key}`,
55
- label: `New ${list.singular}`,
56
- description: `Create a new ${list.singular.toLowerCase()}`,
57
- category: 'Create',
58
- icon: '+',
59
- href: `/${list.path}/create`,
60
- })
61
- }
62
- }
63
-
64
- return cmds
65
- }, [lists])
66
- }
67
-
68
- type CommandPaletteProps = {
69
- isOpen: boolean
70
- onClose: () => void
71
- }
72
-
73
- export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
74
- const router = useRouter()
75
- const [query, setQuery] = useState('')
76
- const [activeIndex, setActiveIndex] = useState(0)
77
- const inputRef = useRef<HTMLInputElement>(null)
78
- const listRef = useRef<HTMLUListElement>(null)
79
- const commands = useCommands()
80
-
81
- const filtered = useMemo(() => {
82
- if (!query.trim()) return commands
83
- const q = query.toLowerCase()
84
- return commands.filter(
85
- c =>
86
- c.label.toLowerCase().includes(q) ||
87
- c.description?.toLowerCase().includes(q) ||
88
- c.category.toLowerCase().includes(q)
89
- )
90
- }, [commands, query])
91
-
92
- // Group by category
93
- const grouped = useMemo(() => {
94
- const map = new Map<string, Command[]>()
95
- for (const cmd of filtered) {
96
- if (!map.has(cmd.category)) map.set(cmd.category, [])
97
- map.get(cmd.category)!.push(cmd)
98
- }
99
- return map
100
- }, [filtered])
101
-
102
- // Flat list for keyboard navigation
103
- const flatList = useMemo(() => filtered, [filtered])
104
-
105
- useEffect(() => {
106
- if (isOpen) {
107
- setQuery('')
108
- setActiveIndex(0)
109
- setTimeout(() => inputRef.current?.focus(), 10)
110
- }
111
- }, [isOpen])
112
-
113
- useEffect(() => {
114
- setActiveIndex(0)
115
- }, [query])
116
-
117
- // Scroll active item into view
118
- useEffect(() => {
119
- const el = listRef.current?.querySelector(`[data-active="true"]`) as HTMLElement | null
120
- el?.scrollIntoView({ block: 'nearest' })
121
- }, [activeIndex])
122
-
123
- function runCommand(cmd: Command) {
124
- onClose()
125
- if (cmd.href) {
126
- router.push(cmd.href)
127
- } else if (cmd.action) {
128
- cmd.action()
129
- }
130
- }
131
-
132
- function onKeyDown(e: KeyboardEvent) {
133
- if (e.key === 'ArrowDown') {
134
- e.preventDefault()
135
- setActiveIndex(i => Math.min(i + 1, flatList.length - 1))
136
- } else if (e.key === 'ArrowUp') {
137
- e.preventDefault()
138
- setActiveIndex(i => Math.max(i - 1, 0))
139
- } else if (e.key === 'Enter') {
140
- e.preventDefault()
141
- const cmd = flatList[activeIndex]
142
- if (cmd) runCommand(cmd)
143
- } else if (e.key === 'Escape') {
144
- onClose()
145
- }
146
- }
147
-
148
- // Pre-compute flat index for each command ID
149
- const cmdFlatIndex = useMemo(() => {
150
- const map = new Map<string, number>()
151
- filtered.forEach((cmd, i) => map.set(cmd.id, i))
152
- return map
153
- }, [filtered])
154
-
155
- if (!isOpen) return null
156
-
157
- return (
158
- <>
159
- {/* Backdrop */}
160
- <div
161
- onClick={onClose}
162
- className={css({
163
- position: 'fixed',
164
- inset: 0,
165
- backgroundColor: 'rgba(0,0,0,0.45)',
166
- zIndex: 200,
167
- backdropFilter: 'blur(3px)',
168
- })}
169
- />
170
-
171
- {/* Panel */}
172
- <div
173
- role="dialog"
174
- aria-modal="true"
175
- aria-label="Command palette"
176
- className={css({
177
- position: 'fixed',
178
- top: '14vh',
179
- left: '50%',
180
- transform: 'translateX(-50%)',
181
- width: '90vw',
182
- maxWidth: 560,
183
- maxHeight: '62vh',
184
- display: 'flex',
185
- flexDirection: 'column',
186
- backgroundColor: '#ffffff',
187
- borderRadius: 10,
188
- boxShadow: '0 24px 64px rgba(0,0,0,0.22), 0 0 0 1px rgba(0,0,0,0.07)',
189
- zIndex: 201,
190
- overflow: 'hidden',
191
- })}
192
- >
193
- {/* Search input */}
194
- <div
195
- className={css({
196
- display: 'flex',
197
- alignItems: 'center',
198
- gap: 10,
199
- padding: '12px 16px',
200
- borderBottom: '1px solid #f0f0f0',
201
- })}
202
- >
203
- <svg
204
- width="15"
205
- height="15"
206
- viewBox="0 0 15 15"
207
- fill="none"
208
- style={{ flexShrink: 0, color: '#a3a3a3' }}
209
- >
210
- <circle cx="6.5" cy="6.5" r="4.5" stroke="currentColor" strokeWidth="1.4" />
211
- <line
212
- x1="9.9"
213
- y1="9.9"
214
- x2="13.2"
215
- y2="13.2"
216
- stroke="currentColor"
217
- strokeWidth="1.4"
218
- strokeLinecap="round"
219
- />
220
- </svg>
221
- <input
222
- ref={inputRef}
223
- value={query}
224
- onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
225
- onKeyDown={onKeyDown}
226
- placeholder="Search commands, lists, actions…"
227
- className={css({
228
- flex: 1,
229
- border: 'none',
230
- outline: 'none',
231
- fontSize: 14.5,
232
- color: '#0a0a0a',
233
- background: 'transparent',
234
- fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
235
- '&::placeholder': { color: '#b8b8b8' },
236
- })}
237
- />
238
- <kbd
239
- className={css({
240
- display: 'inline-flex',
241
- alignItems: 'center',
242
- gap: 2,
243
- padding: '2px 7px',
244
- backgroundColor: '#f5f5f5',
245
- border: '1px solid #e8e8e8',
246
- borderRadius: 5,
247
- fontSize: 11,
248
- color: '#a3a3a3',
249
- fontFamily: 'inherit',
250
- flexShrink: 0,
251
- })}
252
- >
253
- Esc
254
- </kbd>
255
- </div>
256
-
257
- {/* Results */}
258
- <ul
259
- ref={listRef}
260
- role="listbox"
261
- className={css({
262
- flex: 1,
263
- overflowY: 'auto',
264
- padding: '6px 0',
265
- margin: 0,
266
- listStyle: 'none',
267
- '&::-webkit-scrollbar': { width: 4 },
268
- '&::-webkit-scrollbar-thumb': { background: '#e8e8e8', borderRadius: 4 },
269
- })}
270
- >
271
- {grouped.size === 0 ? (
272
- <li
273
- className={css({
274
- padding: '28px 16px',
275
- textAlign: 'center',
276
- color: '#a3a3a3',
277
- fontSize: 13.5,
278
- })}
279
- >
280
- No results for &ldquo;{query}&rdquo;
281
- </li>
282
- ) : (
283
- Array.from(grouped.entries()).map(([category, items]) => (
284
- <li key={category} role="none">
285
- <p
286
- className={css({
287
- margin: 0,
288
- padding: '8px 16px 3px',
289
- fontSize: 10,
290
- fontWeight: 600,
291
- letterSpacing: '0.10em',
292
- textTransform: 'uppercase',
293
- color: '#b8b8b8',
294
- })}
295
- >
296
- {category}
297
- </p>
298
- <ul role="group" className={css({ listStyle: 'none', margin: 0, padding: 0 })}>
299
- {items.map(cmd => {
300
- const idx = cmdFlatIndex.get(cmd.id) ?? 0
301
- const isActive = idx === activeIndex
302
- return (
303
- <li
304
- key={cmd.id}
305
- role="option"
306
- aria-selected={isActive}
307
- data-active={isActive}
308
- onClick={() => runCommand(cmd)}
309
- onMouseEnter={() => setActiveIndex(idx)}
310
- className={css({
311
- display: 'flex',
312
- alignItems: 'center',
313
- gap: 10,
314
- padding: '7px 16px',
315
- cursor: 'pointer',
316
- backgroundColor: isActive ? '#0a0a0a' : 'transparent',
317
- transition: 'background 100ms',
318
- '&:hover': {
319
- backgroundColor: isActive ? '#0a0a0a' : '#f5f5f5',
320
- },
321
- })}
322
- >
323
- {/* Icon badge */}
324
- <span
325
- className={css({
326
- display: 'inline-flex',
327
- alignItems: 'center',
328
- justifyContent: 'center',
329
- width: 28,
330
- height: 28,
331
- borderRadius: 6,
332
- backgroundColor: isActive ? '#2a2a2a' : '#f0f0f0',
333
- fontSize: 12,
334
- fontWeight: 500,
335
- color: isActive ? '#ffffff' : '#636363',
336
- flexShrink: 0,
337
- transition: 'background 100ms, color 100ms',
338
- fontFamily: 'monospace',
339
- })}
340
- >
341
- {cmd.icon}
342
- </span>
343
-
344
- {/* Text */}
345
- <span className={css({ flex: 1, minWidth: 0 })}>
346
- <span
347
- className={css({
348
- display: 'block',
349
- fontSize: 13.5,
350
- fontWeight: 500,
351
- color: isActive ? '#ffffff' : '#0a0a0a',
352
- whiteSpace: 'nowrap',
353
- overflow: 'hidden',
354
- textOverflow: 'ellipsis',
355
- transition: 'color 100ms',
356
- })}
357
- >
358
- {cmd.label}
359
- </span>
360
- {cmd.description && (
361
- <span
362
- className={css({
363
- display: 'block',
364
- fontSize: 12,
365
- color: isActive ? 'rgba(255,255,255,0.5)' : '#a3a3a3',
366
- whiteSpace: 'nowrap',
367
- overflow: 'hidden',
368
- textOverflow: 'ellipsis',
369
- transition: 'color 100ms',
370
- })}
371
- >
372
- {cmd.description}
373
- </span>
374
- )}
375
- </span>
376
-
377
- {/* Enter hint */}
378
- {isActive && (
379
- <kbd
380
- className={css({
381
- fontSize: 11,
382
- color: 'rgba(255,255,255,0.6)',
383
- background: '#2a2a2a',
384
- border: '1px solid #3a3a3a',
385
- borderRadius: 4,
386
- padding: '2px 6px',
387
- fontFamily: 'inherit',
388
- flexShrink: 0,
389
- })}
390
- >
391
-
392
- </kbd>
393
- )}
394
- </li>
395
- )
396
- })}
397
- </ul>
398
- </li>
399
- ))
400
- )}
401
- </ul>
402
-
403
- {/* Footer shortcuts */}
404
- <div
405
- className={css({
406
- display: 'flex',
407
- alignItems: 'center',
408
- gap: 14,
409
- padding: '8px 16px',
410
- borderTop: '1px solid #f0f0f0',
411
- fontSize: 11,
412
- color: '#b8b8b8',
413
- })}
414
- >
415
- <span>
416
- <kbd className={css({ fontFamily: 'inherit' })}>↑↓</kbd> navigate
417
- </span>
418
- <span>
419
- <kbd className={css({ fontFamily: 'inherit' })}>↵</kbd> open
420
- </span>
421
- <span>
422
- <kbd className={css({ fontFamily: 'inherit' })}>Esc</kbd> close
423
- </span>
424
- <span style={{ marginInlineStart: 'auto' }}>
425
- <kbd className={css({ fontFamily: 'inherit' })}>⌘K</kbd> toggle
426
- </span>
427
- </div>
428
- </div>
429
- </>
430
- )
431
- }
1
+ import { useRouter } from 'next/router'
2
+ import {
3
+ type KeyboardEvent,
4
+ type ChangeEvent,
5
+ useEffect,
6
+ useRef,
7
+ useState,
8
+ useMemo,
9
+ } from 'react'
10
+
11
+ import { css } from '@keystar/ui/style'
12
+
13
+ import { useNixxie } from '../context'
14
+ import { getHrefFromList } from './Navigation'
15
+
16
+ type Command = {
17
+ id: string
18
+ label: string
19
+ description?: string
20
+ href?: string
21
+ action?: () => void
22
+ icon: string
23
+ category: string
24
+ }
25
+
26
+ function useCommands(): Command[] {
27
+ const { lists } = useNixxie()
28
+
29
+ return useMemo(() => {
30
+ const cmds: Command[] = [
31
+ {
32
+ id: 'nav-dashboard',
33
+ label: 'Go to Dashboard',
34
+ category: 'Navigate',
35
+ icon: '◼',
36
+ href: '/',
37
+ },
38
+ ]
39
+
40
+ for (const list of Object.values(lists)) {
41
+ if (list.hideNavigation) continue
42
+
43
+ cmds.push({
44
+ id: `nav-${list.key}`,
45
+ label: list.label,
46
+ description: `Open ${list.label} list`,
47
+ category: 'Navigate',
48
+ icon: '≡',
49
+ href: getHrefFromList(list),
50
+ })
51
+
52
+ if (!list.hideCreate && !list.isSingleton) {
53
+ cmds.push({
54
+ id: `create-${list.key}`,
55
+ label: `New ${list.singular}`,
56
+ description: `Create a new ${list.singular.toLowerCase()}`,
57
+ category: 'Create',
58
+ icon: '+',
59
+ href: `/${list.path}/create`,
60
+ })
61
+ }
62
+ }
63
+
64
+ return cmds
65
+ }, [lists])
66
+ }
67
+
68
+ type CommandPaletteProps = {
69
+ isOpen: boolean
70
+ onClose: () => void
71
+ }
72
+
73
+ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
74
+ const router = useRouter()
75
+ const [query, setQuery] = useState('')
76
+ const [activeIndex, setActiveIndex] = useState(0)
77
+ const inputRef = useRef<HTMLInputElement>(null)
78
+ const listRef = useRef<HTMLUListElement>(null)
79
+ const commands = useCommands()
80
+
81
+ const filtered = useMemo(() => {
82
+ if (!query.trim()) return commands
83
+ const q = query.toLowerCase()
84
+ return commands.filter(
85
+ c =>
86
+ c.label.toLowerCase().includes(q) ||
87
+ c.description?.toLowerCase().includes(q) ||
88
+ c.category.toLowerCase().includes(q)
89
+ )
90
+ }, [commands, query])
91
+
92
+ // Group by category
93
+ const grouped = useMemo(() => {
94
+ const map = new Map<string, Command[]>()
95
+ for (const cmd of filtered) {
96
+ if (!map.has(cmd.category)) map.set(cmd.category, [])
97
+ map.get(cmd.category)!.push(cmd)
98
+ }
99
+ return map
100
+ }, [filtered])
101
+
102
+ // Flat list for keyboard navigation — built from the *grouped* (rendered) order so ArrowUp/Down
103
+ // walk items in the same top-to-bottom order the user sees, not the interleaved insertion order.
104
+ const flatList = useMemo(() => Array.from(grouped.values()).flat(), [grouped])
105
+
106
+ useEffect(() => {
107
+ if (!isOpen) return
108
+ setQuery('')
109
+ setActiveIndex(0)
110
+ const t = setTimeout(() => inputRef.current?.focus(), 10)
111
+ return () => clearTimeout(t)
112
+ }, [isOpen])
113
+
114
+ useEffect(() => {
115
+ setActiveIndex(0)
116
+ }, [query])
117
+
118
+ // Scroll active item into view
119
+ useEffect(() => {
120
+ const el = listRef.current?.querySelector(`[data-active="true"]`) as HTMLElement | null
121
+ el?.scrollIntoView({ block: 'nearest' })
122
+ }, [activeIndex])
123
+
124
+ function runCommand(cmd: Command) {
125
+ onClose()
126
+ if (cmd.href) {
127
+ router.push(cmd.href)
128
+ } else if (cmd.action) {
129
+ cmd.action()
130
+ }
131
+ }
132
+
133
+ function onKeyDown(e: KeyboardEvent) {
134
+ if (e.key === 'ArrowDown') {
135
+ e.preventDefault()
136
+ setActiveIndex(i => Math.max(0, Math.min(i + 1, flatList.length - 1)))
137
+ } else if (e.key === 'ArrowUp') {
138
+ e.preventDefault()
139
+ setActiveIndex(i => Math.max(i - 1, 0))
140
+ } else if (e.key === 'Enter') {
141
+ e.preventDefault()
142
+ const cmd = flatList[activeIndex]
143
+ if (cmd) runCommand(cmd)
144
+ } else if (e.key === 'Escape') {
145
+ onClose()
146
+ }
147
+ }
148
+
149
+ // Pre-compute flat index for each command ID, using the same grouped order as `flatList` so the
150
+ // rendered highlight (`idx === activeIndex`) tracks keyboard navigation correctly.
151
+ const cmdFlatIndex = useMemo(() => {
152
+ const map = new Map<string, number>()
153
+ flatList.forEach((cmd, i) => map.set(cmd.id, i))
154
+ return map
155
+ }, [flatList])
156
+
157
+ if (!isOpen) return null
158
+
159
+ return (
160
+ <>
161
+ {/* Backdrop */}
162
+ <div
163
+ onClick={onClose}
164
+ className={css({
165
+ position: 'fixed',
166
+ inset: 0,
167
+ backgroundColor: 'rgba(0,0,0,0.45)',
168
+ zIndex: 200,
169
+ backdropFilter: 'blur(3px)',
170
+ })}
171
+ />
172
+
173
+ {/* Panel */}
174
+ <div
175
+ role="dialog"
176
+ aria-modal="true"
177
+ aria-label="Command palette"
178
+ className={css({
179
+ position: 'fixed',
180
+ top: '14vh',
181
+ left: '50%',
182
+ transform: 'translateX(-50%)',
183
+ width: '90vw',
184
+ maxWidth: 560,
185
+ maxHeight: '62vh',
186
+ display: 'flex',
187
+ flexDirection: 'column',
188
+ backgroundColor: '#ffffff',
189
+ borderRadius: 10,
190
+ boxShadow: '0 24px 64px rgba(0,0,0,0.22), 0 0 0 1px rgba(0,0,0,0.07)',
191
+ zIndex: 201,
192
+ overflow: 'hidden',
193
+ })}
194
+ >
195
+ {/* Search input */}
196
+ <div
197
+ className={css({
198
+ display: 'flex',
199
+ alignItems: 'center',
200
+ gap: 10,
201
+ padding: '12px 16px',
202
+ borderBottom: '1px solid #f0f0f0',
203
+ })}
204
+ >
205
+ <svg
206
+ width="15"
207
+ height="15"
208
+ viewBox="0 0 15 15"
209
+ fill="none"
210
+ style={{ flexShrink: 0, color: '#a3a3a3' }}
211
+ >
212
+ <circle cx="6.5" cy="6.5" r="4.5" stroke="currentColor" strokeWidth="1.4" />
213
+ <line
214
+ x1="9.9"
215
+ y1="9.9"
216
+ x2="13.2"
217
+ y2="13.2"
218
+ stroke="currentColor"
219
+ strokeWidth="1.4"
220
+ strokeLinecap="round"
221
+ />
222
+ </svg>
223
+ <input
224
+ ref={inputRef}
225
+ value={query}
226
+ onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
227
+ onKeyDown={onKeyDown}
228
+ placeholder="Search commands, lists, actions…"
229
+ className={css({
230
+ flex: 1,
231
+ border: 'none',
232
+ outline: 'none',
233
+ fontSize: 14.5,
234
+ color: '#0a0a0a',
235
+ background: 'transparent',
236
+ fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
237
+ '&::placeholder': { color: '#b8b8b8' },
238
+ })}
239
+ />
240
+ <kbd
241
+ className={css({
242
+ display: 'inline-flex',
243
+ alignItems: 'center',
244
+ gap: 2,
245
+ padding: '2px 7px',
246
+ backgroundColor: '#f5f5f5',
247
+ border: '1px solid #e8e8e8',
248
+ borderRadius: 5,
249
+ fontSize: 11,
250
+ color: '#a3a3a3',
251
+ fontFamily: 'inherit',
252
+ flexShrink: 0,
253
+ })}
254
+ >
255
+ Esc
256
+ </kbd>
257
+ </div>
258
+
259
+ {/* Results */}
260
+ <ul
261
+ ref={listRef}
262
+ role="listbox"
263
+ className={css({
264
+ flex: 1,
265
+ overflowY: 'auto',
266
+ padding: '6px 0',
267
+ margin: 0,
268
+ listStyle: 'none',
269
+ '&::-webkit-scrollbar': { width: 4 },
270
+ '&::-webkit-scrollbar-thumb': { background: '#e8e8e8', borderRadius: 4 },
271
+ })}
272
+ >
273
+ {grouped.size === 0 ? (
274
+ <li
275
+ className={css({
276
+ padding: '28px 16px',
277
+ textAlign: 'center',
278
+ color: '#a3a3a3',
279
+ fontSize: 13.5,
280
+ })}
281
+ >
282
+ No results for &ldquo;{query}&rdquo;
283
+ </li>
284
+ ) : (
285
+ Array.from(grouped.entries()).map(([category, items]) => (
286
+ <li key={category} role="none">
287
+ <p
288
+ className={css({
289
+ margin: 0,
290
+ padding: '8px 16px 3px',
291
+ fontSize: 10,
292
+ fontWeight: 600,
293
+ letterSpacing: '0.10em',
294
+ textTransform: 'uppercase',
295
+ color: '#b8b8b8',
296
+ })}
297
+ >
298
+ {category}
299
+ </p>
300
+ <ul role="group" className={css({ listStyle: 'none', margin: 0, padding: 0 })}>
301
+ {items.map(cmd => {
302
+ const idx = cmdFlatIndex.get(cmd.id) ?? 0
303
+ const isActive = idx === activeIndex
304
+ return (
305
+ <li
306
+ key={cmd.id}
307
+ role="option"
308
+ aria-selected={isActive}
309
+ data-active={isActive}
310
+ onClick={() => runCommand(cmd)}
311
+ onMouseEnter={() => setActiveIndex(idx)}
312
+ className={css({
313
+ display: 'flex',
314
+ alignItems: 'center',
315
+ gap: 10,
316
+ padding: '7px 16px',
317
+ cursor: 'pointer',
318
+ backgroundColor: isActive ? '#0a0a0a' : 'transparent',
319
+ transition: 'background 100ms',
320
+ '&:hover': {
321
+ backgroundColor: isActive ? '#0a0a0a' : '#f5f5f5',
322
+ },
323
+ })}
324
+ >
325
+ {/* Icon badge */}
326
+ <span
327
+ className={css({
328
+ display: 'inline-flex',
329
+ alignItems: 'center',
330
+ justifyContent: 'center',
331
+ width: 28,
332
+ height: 28,
333
+ borderRadius: 6,
334
+ backgroundColor: isActive ? '#2a2a2a' : '#f0f0f0',
335
+ fontSize: 12,
336
+ fontWeight: 500,
337
+ color: isActive ? '#ffffff' : '#636363',
338
+ flexShrink: 0,
339
+ transition: 'background 100ms, color 100ms',
340
+ fontFamily: 'monospace',
341
+ })}
342
+ >
343
+ {cmd.icon}
344
+ </span>
345
+
346
+ {/* Text */}
347
+ <span className={css({ flex: 1, minWidth: 0 })}>
348
+ <span
349
+ className={css({
350
+ display: 'block',
351
+ fontSize: 13.5,
352
+ fontWeight: 500,
353
+ color: isActive ? '#ffffff' : '#0a0a0a',
354
+ whiteSpace: 'nowrap',
355
+ overflow: 'hidden',
356
+ textOverflow: 'ellipsis',
357
+ transition: 'color 100ms',
358
+ })}
359
+ >
360
+ {cmd.label}
361
+ </span>
362
+ {cmd.description && (
363
+ <span
364
+ className={css({
365
+ display: 'block',
366
+ fontSize: 12,
367
+ color: isActive ? 'rgba(255,255,255,0.5)' : '#a3a3a3',
368
+ whiteSpace: 'nowrap',
369
+ overflow: 'hidden',
370
+ textOverflow: 'ellipsis',
371
+ transition: 'color 100ms',
372
+ })}
373
+ >
374
+ {cmd.description}
375
+ </span>
376
+ )}
377
+ </span>
378
+
379
+ {/* Enter hint */}
380
+ {isActive && (
381
+ <kbd
382
+ className={css({
383
+ fontSize: 11,
384
+ color: 'rgba(255,255,255,0.6)',
385
+ background: '#2a2a2a',
386
+ border: '1px solid #3a3a3a',
387
+ borderRadius: 4,
388
+ padding: '2px 6px',
389
+ fontFamily: 'inherit',
390
+ flexShrink: 0,
391
+ })}
392
+ >
393
+
394
+ </kbd>
395
+ )}
396
+ </li>
397
+ )
398
+ })}
399
+ </ul>
400
+ </li>
401
+ ))
402
+ )}
403
+ </ul>
404
+
405
+ {/* Footer shortcuts */}
406
+ <div
407
+ className={css({
408
+ display: 'flex',
409
+ alignItems: 'center',
410
+ gap: 14,
411
+ padding: '8px 16px',
412
+ borderTop: '1px solid #f0f0f0',
413
+ fontSize: 11,
414
+ color: '#b8b8b8',
415
+ })}
416
+ >
417
+ <span>
418
+ <kbd className={css({ fontFamily: 'inherit' })}>↑↓</kbd> navigate
419
+ </span>
420
+ <span>
421
+ <kbd className={css({ fontFamily: 'inherit' })}>↵</kbd> open
422
+ </span>
423
+ <span>
424
+ <kbd className={css({ fontFamily: 'inherit' })}>Esc</kbd> close
425
+ </span>
426
+ <span style={{ marginInlineStart: 'auto' }}>
427
+ <kbd className={css({ fontFamily: 'inherit' })}>⌘K</kbd> toggle
428
+ </span>
429
+ </div>
430
+ </div>
431
+ </>
432
+ )
433
+ }