@nixxie-cms/core 2.1.0 → 2.2.0

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 (126) hide show
  1. package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.cjs.js +7 -7
  2. package/admin-ui/components/dist/nixxie-cms-core-admin-ui-components.esm.js +7 -7
  3. package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.cjs.js +2 -2
  4. package/admin-ui/context/dist/nixxie-cms-core-admin-ui-context.esm.js +2 -2
  5. package/admin-ui/utils/dist/nixxie-cms-core-admin-ui-utils.cjs.js +5 -5
  6. package/admin-ui/utils/dist/nixxie-cms-core-admin-ui-utils.esm.js +3 -3
  7. package/dist/{CreateItemDialog-7008b050.esm.js → CreateItemDialog-66621fe8.esm.js} +3 -3
  8. package/dist/{CreateItemDialog-a0cab315.cjs.js → CreateItemDialog-96b044ce.cjs.js} +4 -4
  9. package/dist/{Field-47f85161.esm.js → Field-1820c4e6.esm.js} +1 -0
  10. package/dist/{Field-ed8d7627.cjs.js → Field-38d3cdf9.cjs.js} +1 -0
  11. package/dist/GraphQLErrorNotice-7594a9f8.esm.js +64 -0
  12. package/dist/GraphQLErrorNotice-c8890f80.cjs.js +66 -0
  13. package/dist/{PageContainer-5ae731cc.esm.js → PageContainer-355cfbfa.esm.js} +362 -156
  14. package/dist/{PageContainer-abd7159f.cjs.js → PageContainer-4095555a.cjs.js} +361 -155
  15. package/dist/{context-af9957ed.esm.js → context-2924eaaa.esm.js} +53 -58
  16. package/dist/{context-b5204629.cjs.js → context-2ce61d0b.cjs.js} +53 -58
  17. package/dist/declarations/src/admin-ui/components/GraphQLErrorNotice.d.ts.map +1 -1
  18. package/dist/declarations/src/admin-ui/components/Navigation.d.ts.map +1 -1
  19. package/dist/declarations/src/admin-ui/components/PageContainer.d.ts.map +1 -1
  20. package/dist/declarations/src/admin-ui/context.d.ts.map +1 -1
  21. package/dist/declarations/src/admin-ui/utils/useCreateItem.d.ts.map +1 -1
  22. package/dist/declarations/src/fields/types/bigInt/views/index.d.ts +2 -1
  23. package/dist/declarations/src/fields/types/bigInt/views/index.d.ts.map +1 -1
  24. package/dist/declarations/src/fields/types/bytes/views/index.d.ts +2 -1
  25. package/dist/declarations/src/fields/types/bytes/views/index.d.ts.map +1 -1
  26. package/dist/declarations/src/fields/types/calendarDay/views/index.d.ts.map +1 -1
  27. package/dist/declarations/src/fields/types/decimal/views/index.d.ts +2 -1
  28. package/dist/declarations/src/fields/types/decimal/views/index.d.ts.map +1 -1
  29. package/dist/declarations/src/fields/types/float/views/index.d.ts +2 -1
  30. package/dist/declarations/src/fields/types/float/views/index.d.ts.map +1 -1
  31. package/dist/declarations/src/fields/types/integer/views/index.d.ts +2 -1
  32. package/dist/declarations/src/fields/types/integer/views/index.d.ts.map +1 -1
  33. package/dist/declarations/src/fields/types/json/views/index.d.ts.map +1 -1
  34. package/dist/declarations/src/fields/types/multiselect/views/index.d.ts.map +1 -1
  35. package/dist/declarations/src/fields/types/relationship/views/index.d.ts.map +1 -1
  36. package/dist/declarations/src/fields/types/select/views/index.d.ts.map +1 -1
  37. package/dist/declarations/src/fields/types/text/views/index.d.ts +2 -1
  38. package/dist/declarations/src/fields/types/text/views/index.d.ts.map +1 -1
  39. package/dist/declarations/src/fields/types/timestamp/views/index.d.ts.map +1 -1
  40. package/dist/declarations/src/fields/types/virtual/views/index.d.ts.map +1 -1
  41. package/dist/declarations/src/internal-unstable/admin-ui/pages/HomePage/index.d.ts.map +1 -1
  42. package/dist/declarations/src/internal-unstable/admin-ui/pages/ItemPage/index.d.ts.map +1 -1
  43. package/dist/pick-4c785a54.esm.js +34 -0
  44. package/dist/pick-906341bb.cjs.js +37 -0
  45. package/dist/{useCreateItem-1f94d252.esm.js → useCreateItem-36a75f1c.esm.js} +26 -26
  46. package/dist/{useCreateItem-1be4987e.cjs.js → useCreateItem-acf06f77.cjs.js} +37 -37
  47. package/dist/{useFilter-acc9d413.cjs.js → useFilter-c29f17a8.cjs.js} +1 -1
  48. package/dist/{useFilter-9b6db1f9.esm.js → useFilter-f79b2abb.esm.js} +1 -1
  49. package/dist/{Fields-956d9a14.esm.js → usePreventNavigation-093389dd.esm.js} +28 -2
  50. package/dist/{Fields-e2c28056.cjs.js → usePreventNavigation-d4f9f4fa.cjs.js} +27 -0
  51. package/fields/types/bigInt/views/dist/nixxie-cms-core-fields-types-bigInt-views.cjs.js +8 -0
  52. package/fields/types/bigInt/views/dist/nixxie-cms-core-fields-types-bigInt-views.esm.js +8 -1
  53. package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.cjs.js +14 -3
  54. package/fields/types/bytes/views/dist/nixxie-cms-core-fields-types-bytes-views.esm.js +15 -5
  55. package/fields/types/calendarDay/views/dist/nixxie-cms-core-fields-types-calendarDay-views.cjs.js +2 -1
  56. package/fields/types/calendarDay/views/dist/nixxie-cms-core-fields-types-calendarDay-views.esm.js +2 -1
  57. package/fields/types/decimal/views/dist/nixxie-cms-core-fields-types-decimal-views.cjs.js +10 -1
  58. package/fields/types/decimal/views/dist/nixxie-cms-core-fields-types-decimal-views.esm.js +10 -2
  59. package/fields/types/file/views/dist/nixxie-cms-core-fields-types-file-views.cjs.js +1 -1
  60. package/fields/types/file/views/dist/nixxie-cms-core-fields-types-file-views.esm.js +2 -2
  61. package/fields/types/float/views/dist/nixxie-cms-core-fields-types-float-views.cjs.js +10 -1
  62. package/fields/types/float/views/dist/nixxie-cms-core-fields-types-float-views.esm.js +10 -2
  63. package/fields/types/image/views/dist/nixxie-cms-core-fields-types-image-views.cjs.js +2 -1
  64. package/fields/types/image/views/dist/nixxie-cms-core-fields-types-image-views.esm.js +2 -1
  65. package/fields/types/integer/views/dist/nixxie-cms-core-fields-types-integer-views.cjs.js +8 -0
  66. package/fields/types/integer/views/dist/nixxie-cms-core-fields-types-integer-views.esm.js +8 -1
  67. package/fields/types/json/views/dist/nixxie-cms-core-fields-types-json-views.cjs.js +19 -4
  68. package/fields/types/json/views/dist/nixxie-cms-core-fields-types-json-views.esm.js +19 -4
  69. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.cjs.js +18 -3
  70. package/fields/types/multiselect/views/dist/nixxie-cms-core-fields-types-multiselect-views.esm.js +18 -3
  71. package/fields/types/password/views/dist/nixxie-cms-core-fields-types-password-views.cjs.js +1 -1
  72. package/fields/types/password/views/dist/nixxie-cms-core-fields-types-password-views.esm.js +1 -1
  73. package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.cjs.js +9 -7
  74. package/fields/types/relationship/views/dist/nixxie-cms-core-fields-types-relationship-views.esm.js +9 -7
  75. package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.cjs.js +3 -2
  76. package/fields/types/select/views/dist/nixxie-cms-core-fields-types-select-views.esm.js +3 -2
  77. package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.cjs.js +14 -3
  78. package/fields/types/text/views/dist/nixxie-cms-core-fields-types-text-views.esm.js +15 -5
  79. package/fields/types/timestamp/views/dist/nixxie-cms-core-fields-types-timestamp-views.cjs.js +2 -1
  80. package/fields/types/timestamp/views/dist/nixxie-cms-core-fields-types-timestamp-views.esm.js +2 -1
  81. package/fields/types/virtual/views/dist/nixxie-cms-core-fields-types-virtual-views.cjs.js +13 -1
  82. package/fields/types/virtual/views/dist/nixxie-cms-core-fields-types-virtual-views.esm.js +14 -2
  83. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.cjs.js +3 -3
  84. package/internal-unstable/admin-ui/pages/App/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-App.esm.js +3 -3
  85. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.cjs.js +7 -7
  86. package/internal-unstable/admin-ui/pages/CreateItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-CreateItemPage.esm.js +6 -6
  87. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.cjs.js +34 -33
  88. package/internal-unstable/admin-ui/pages/HomePage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-HomePage.esm.js +35 -34
  89. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.cjs.js +53 -13
  90. package/internal-unstable/admin-ui/pages/ItemPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ItemPage.esm.js +52 -12
  91. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.cjs.js +36 -25
  92. package/internal-unstable/admin-ui/pages/ListPage/dist/nixxie-cms-core-internal-unstable-admin-ui-pages-ListPage.esm.js +36 -25
  93. package/package.json +2 -2
  94. package/src/admin-ui/components/CommandPalette.tsx +134 -27
  95. package/src/admin-ui/components/CreateButtonLink.tsx +20 -46
  96. package/src/admin-ui/components/GraphQLErrorNotice.tsx +39 -33
  97. package/src/admin-ui/components/Logo.tsx +5 -5
  98. package/src/admin-ui/components/Navigation.tsx +41 -27
  99. package/src/admin-ui/components/PageContainer.tsx +171 -15
  100. package/src/admin-ui/components/WelcomeDialog.tsx +14 -14
  101. package/src/admin-ui/context.tsx +5 -2
  102. package/src/admin-ui/utils/useCreateItem.ts +21 -1
  103. package/src/fields/types/bigInt/views/index.tsx +10 -1
  104. package/src/fields/types/bytes/views/index.tsx +14 -1
  105. package/src/fields/types/calendarDay/views/index.tsx +2 -1
  106. package/src/fields/types/decimal/views/index.tsx +7 -1
  107. package/src/fields/types/file/views/Field.tsx +1 -1
  108. package/src/fields/types/float/views/index.tsx +7 -1
  109. package/src/fields/types/image/views/index.tsx +1 -1
  110. package/src/fields/types/integer/views/index.tsx +5 -0
  111. package/src/fields/types/json/views/index.tsx +20 -2
  112. package/src/fields/types/multiselect/views/index.tsx +7 -3
  113. package/src/fields/types/password/views/index.tsx +1 -1
  114. package/src/fields/types/relationship/views/index.tsx +1 -0
  115. package/src/fields/types/select/views/index.tsx +2 -1
  116. package/src/fields/types/text/views/index.tsx +14 -1
  117. package/src/fields/types/timestamp/views/index.tsx +2 -1
  118. package/src/fields/types/virtual/views/index.tsx +17 -2
  119. package/src/internal-unstable/admin-ui/pages/HomePage/index.tsx +40 -31
  120. package/src/internal-unstable/admin-ui/pages/ItemPage/index.tsx +36 -3
  121. package/src/internal-unstable/admin-ui/pages/ListPage/PaginationControls.tsx +20 -33
  122. package/src/internal-unstable/admin-ui/pages/ListPage/index.tsx +24 -16
  123. package/dist/GraphQLErrorNotice-cd74180d.cjs.js +0 -57
  124. package/dist/GraphQLErrorNotice-d9f0931b.esm.js +0 -55
  125. package/dist/pick-5fe45878.cjs.js +0 -71
  126. package/dist/pick-b7ef3115.esm.js +0 -68
@@ -8,7 +8,8 @@ import {
8
8
  useMemo,
9
9
  } from 'react'
10
10
 
11
- import { css } from '@keystar/ui/style'
11
+ import { css, tokenSchema } from '@keystar/ui/style'
12
+ import { useId } from '@react-aria/utils'
12
13
 
13
14
  import { useNixxie } from '../context'
14
15
  import { getHrefFromList } from './Navigation'
@@ -76,6 +77,8 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
76
77
  const [activeIndex, setActiveIndex] = useState(0)
77
78
  const inputRef = useRef<HTMLInputElement>(null)
78
79
  const listRef = useRef<HTMLUListElement>(null)
80
+ const panelRef = useRef<HTMLDivElement>(null)
81
+ const listboxId = useId()
79
82
  const commands = useCommands()
80
83
 
81
84
  const filtered = useMemo(() => {
@@ -107,8 +110,27 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
107
110
  if (!isOpen) return
108
111
  setQuery('')
109
112
  setActiveIndex(0)
113
+
114
+ // FOCUS RESTORE: remember what was focused before the palette opened.
115
+ const previouslyFocused = document.activeElement as HTMLElement | null
116
+
117
+ // SCROLL LOCK: prevent the page behind the modal from scrolling.
118
+ const prevOverflow = document.body.style.overflow
119
+ document.body.style.overflow = 'hidden'
120
+
110
121
  const t = setTimeout(() => inputRef.current?.focus(), 10)
111
- return () => clearTimeout(t)
122
+
123
+ return () => {
124
+ clearTimeout(t)
125
+ // Restore scroll position behaviour.
126
+ document.body.style.overflow = prevOverflow
127
+ // Restore focus to whatever held it before we opened.
128
+ try {
129
+ previouslyFocused?.focus?.()
130
+ } catch {
131
+ /* element may be gone from the DOM — ignore */
132
+ }
133
+ }
112
134
  }, [isOpen])
113
135
 
114
136
  useEffect(() => {
@@ -130,6 +152,49 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
130
152
  }
131
153
  }
132
154
 
155
+ // FOCUS TRAP + panel-level ESCAPE. Runs for any child of the panel so the
156
+ // modal closes regardless of which element currently holds focus, and Tab /
157
+ // Shift+Tab stay contained within the panel's focusable descendants.
158
+ function onPanelKeyDown(e: KeyboardEvent) {
159
+ if (e.key === 'Escape') {
160
+ e.preventDefault()
161
+ onClose()
162
+ return
163
+ }
164
+ if (e.key !== 'Tab') return
165
+
166
+ const panel = panelRef.current
167
+ if (!panel) return
168
+
169
+ let focusables: HTMLElement[]
170
+ try {
171
+ focusables = Array.from(
172
+ panel.querySelectorAll<HTMLElement>(
173
+ 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
174
+ )
175
+ ).filter(el => el.offsetParent !== null || el === document.activeElement)
176
+ } catch {
177
+ return
178
+ }
179
+ if (focusables.length === 0) return
180
+
181
+ const first = focusables[0]
182
+ const last = focusables[focusables.length - 1]
183
+ const current = document.activeElement as HTMLElement | null
184
+
185
+ if (e.shiftKey) {
186
+ if (current === first || !panel.contains(current)) {
187
+ e.preventDefault()
188
+ last.focus()
189
+ }
190
+ } else {
191
+ if (current === last || !panel.contains(current)) {
192
+ e.preventDefault()
193
+ first.focus()
194
+ }
195
+ }
196
+ }
197
+
133
198
  function onKeyDown(e: KeyboardEvent) {
134
199
  if (e.key === 'ArrowDown') {
135
200
  e.preventDefault()
@@ -154,6 +219,12 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
154
219
  return map
155
220
  }, [flatList])
156
221
 
222
+ // Stable per-option id derived from the listbox id, used for both the rendered
223
+ // <li role="option"> ids and the input's aria-activedescendant.
224
+ const optionId = (cmdId: string) => `${listboxId}-opt-${cmdId}`
225
+ const activeCommand = flatList[activeIndex]
226
+ const activeOptionId = activeCommand ? optionId(activeCommand.id) : undefined
227
+
157
228
  if (!isOpen) return null
158
229
 
159
230
  return (
@@ -164,7 +235,7 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
164
235
  className={css({
165
236
  position: 'fixed',
166
237
  inset: 0,
167
- backgroundColor: 'rgba(0,0,0,0.45)',
238
+ backgroundColor: tokenSchema.color.alias.blanket,
168
239
  zIndex: 200,
169
240
  backdropFilter: 'blur(3px)',
170
241
  })}
@@ -172,9 +243,11 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
172
243
 
173
244
  {/* Panel */}
174
245
  <div
246
+ ref={panelRef}
175
247
  role="dialog"
176
248
  aria-modal="true"
177
249
  aria-label="Command palette"
250
+ onKeyDown={onPanelKeyDown}
178
251
  className={css({
179
252
  position: 'fixed',
180
253
  top: '14vh',
@@ -185,7 +258,7 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
185
258
  maxHeight: '62vh',
186
259
  display: 'flex',
187
260
  flexDirection: 'column',
188
- backgroundColor: '#ffffff',
261
+ backgroundColor: tokenSchema.color.background.canvas,
189
262
  borderRadius: 10,
190
263
  boxShadow: '0 24px 64px rgba(0,0,0,0.22), 0 0 0 1px rgba(0,0,0,0.07)',
191
264
  zIndex: 201,
@@ -199,7 +272,7 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
199
272
  alignItems: 'center',
200
273
  gap: 10,
201
274
  padding: '12px 16px',
202
- borderBottom: '1px solid #f0f0f0',
275
+ borderBottom: `1px solid ${tokenSchema.color.border.muted}`,
203
276
  })}
204
277
  >
205
278
  <svg
@@ -207,7 +280,8 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
207
280
  height="15"
208
281
  viewBox="0 0 15 15"
209
282
  fill="none"
210
- style={{ flexShrink: 0, color: '#a3a3a3' }}
283
+ aria-hidden="true"
284
+ style={{ flexShrink: 0, color: tokenSchema.color.foreground.neutralSecondary }}
211
285
  >
212
286
  <circle cx="6.5" cy="6.5" r="4.5" stroke="currentColor" strokeWidth="1.4" />
213
287
  <line
@@ -226,15 +300,21 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
226
300
  onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
227
301
  onKeyDown={onKeyDown}
228
302
  placeholder="Search commands, lists, actions…"
303
+ role="combobox"
304
+ aria-label="Search commands"
305
+ aria-expanded={flatList.length > 0}
306
+ aria-controls={listboxId}
307
+ aria-autocomplete="list"
308
+ aria-activedescendant={activeOptionId}
229
309
  className={css({
230
310
  flex: 1,
231
311
  border: 'none',
232
312
  outline: 'none',
233
313
  fontSize: 14.5,
234
- color: '#0a0a0a',
314
+ color: tokenSchema.color.foreground.neutralEmphasis,
235
315
  background: 'transparent',
236
316
  fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
237
- '&::placeholder': { color: '#b8b8b8' },
317
+ '&::placeholder': { color: tokenSchema.color.foreground.neutralSecondary },
238
318
  })}
239
319
  />
240
320
  <kbd
@@ -243,11 +323,11 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
243
323
  alignItems: 'center',
244
324
  gap: 2,
245
325
  padding: '2px 7px',
246
- backgroundColor: '#f5f5f5',
247
- border: '1px solid #e8e8e8',
326
+ backgroundColor: tokenSchema.color.background.surfaceSecondary,
327
+ border: `1px solid ${tokenSchema.color.border.muted}`,
248
328
  borderRadius: 5,
249
329
  fontSize: 11,
250
- color: '#a3a3a3',
330
+ color: tokenSchema.color.foreground.neutralSecondary,
251
331
  fontFamily: 'inherit',
252
332
  flexShrink: 0,
253
333
  })}
@@ -259,7 +339,9 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
259
339
  {/* Results */}
260
340
  <ul
261
341
  ref={listRef}
342
+ id={listboxId}
262
343
  role="listbox"
344
+ aria-label="Commands"
263
345
  className={css({
264
346
  flex: 1,
265
347
  overflowY: 'auto',
@@ -267,7 +349,10 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
267
349
  margin: 0,
268
350
  listStyle: 'none',
269
351
  '&::-webkit-scrollbar': { width: 4 },
270
- '&::-webkit-scrollbar-thumb': { background: '#e8e8e8', borderRadius: 4 },
352
+ '&::-webkit-scrollbar-thumb': {
353
+ background: tokenSchema.color.border.muted,
354
+ borderRadius: 4,
355
+ },
271
356
  })}
272
357
  >
273
358
  {grouped.size === 0 ? (
@@ -275,16 +360,20 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
275
360
  className={css({
276
361
  padding: '28px 16px',
277
362
  textAlign: 'center',
278
- color: '#a3a3a3',
363
+ color: tokenSchema.color.foreground.neutralSecondary,
279
364
  fontSize: 13.5,
280
365
  })}
281
366
  >
282
367
  No results for &ldquo;{query}&rdquo;
283
368
  </li>
284
369
  ) : (
285
- Array.from(grouped.entries()).map(([category, items]) => (
286
- <li key={category} role="none">
370
+ Array.from(grouped.entries()).map(([category, items], groupIdx) => {
371
+ const headerId = `${listboxId}-cat-${groupIdx}`
372
+ return (
373
+ <li key={category} role="presentation">
287
374
  <p
375
+ id={headerId}
376
+ role="presentation"
288
377
  className={css({
289
378
  margin: 0,
290
379
  padding: '8px 16px 3px',
@@ -292,18 +381,23 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
292
381
  fontWeight: 600,
293
382
  letterSpacing: '0.10em',
294
383
  textTransform: 'uppercase',
295
- color: '#b8b8b8',
384
+ color: tokenSchema.color.foreground.neutralSecondary,
296
385
  })}
297
386
  >
298
387
  {category}
299
388
  </p>
300
- <ul role="group" className={css({ listStyle: 'none', margin: 0, padding: 0 })}>
389
+ <ul
390
+ role="group"
391
+ aria-labelledby={headerId}
392
+ className={css({ listStyle: 'none', margin: 0, padding: 0 })}
393
+ >
301
394
  {items.map(cmd => {
302
395
  const idx = cmdFlatIndex.get(cmd.id) ?? 0
303
396
  const isActive = idx === activeIndex
304
397
  return (
305
398
  <li
306
399
  key={cmd.id}
400
+ id={optionId(cmd.id)}
307
401
  role="option"
308
402
  aria-selected={isActive}
309
403
  data-active={isActive}
@@ -315,10 +409,14 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
315
409
  gap: 10,
316
410
  padding: '7px 16px',
317
411
  cursor: 'pointer',
318
- backgroundColor: isActive ? '#0a0a0a' : 'transparent',
412
+ backgroundColor: isActive
413
+ ? tokenSchema.color.scale.black
414
+ : 'transparent',
319
415
  transition: 'background 100ms',
320
416
  '&:hover': {
321
- backgroundColor: isActive ? '#0a0a0a' : '#f5f5f5',
417
+ backgroundColor: isActive
418
+ ? tokenSchema.color.scale.black
419
+ : tokenSchema.color.background.surfaceSecondary,
322
420
  },
323
421
  })}
324
422
  >
@@ -331,10 +429,14 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
331
429
  width: 28,
332
430
  height: 28,
333
431
  borderRadius: 6,
334
- backgroundColor: isActive ? '#2a2a2a' : '#f0f0f0',
432
+ backgroundColor: isActive
433
+ ? '#2a2a2a'
434
+ : tokenSchema.color.background.surfaceSecondary,
335
435
  fontSize: 12,
336
436
  fontWeight: 500,
337
- color: isActive ? '#ffffff' : '#636363',
437
+ color: isActive
438
+ ? tokenSchema.color.foreground.onEmphasis
439
+ : tokenSchema.color.foreground.neutralSecondary,
338
440
  flexShrink: 0,
339
441
  transition: 'background 100ms, color 100ms',
340
442
  fontFamily: 'monospace',
@@ -350,7 +452,9 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
350
452
  display: 'block',
351
453
  fontSize: 13.5,
352
454
  fontWeight: 500,
353
- color: isActive ? '#ffffff' : '#0a0a0a',
455
+ color: isActive
456
+ ? tokenSchema.color.foreground.onEmphasis
457
+ : tokenSchema.color.foreground.neutralEmphasis,
354
458
  whiteSpace: 'nowrap',
355
459
  overflow: 'hidden',
356
460
  textOverflow: 'ellipsis',
@@ -364,7 +468,9 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
364
468
  className={css({
365
469
  display: 'block',
366
470
  fontSize: 12,
367
- color: isActive ? 'rgba(255,255,255,0.5)' : '#a3a3a3',
471
+ color: isActive
472
+ ? tokenSchema.color.foreground.inverseSecondary
473
+ : tokenSchema.color.foreground.neutralSecondary,
368
474
  whiteSpace: 'nowrap',
369
475
  overflow: 'hidden',
370
476
  textOverflow: 'ellipsis',
@@ -381,7 +487,7 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
381
487
  <kbd
382
488
  className={css({
383
489
  fontSize: 11,
384
- color: 'rgba(255,255,255,0.6)',
490
+ color: tokenSchema.color.foreground.inverseSecondary,
385
491
  background: '#2a2a2a',
386
492
  border: '1px solid #3a3a3a',
387
493
  borderRadius: 4,
@@ -398,7 +504,8 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
398
504
  })}
399
505
  </ul>
400
506
  </li>
401
- ))
507
+ )
508
+ })
402
509
  )}
403
510
  </ul>
404
511
 
@@ -409,9 +516,9 @@ export function CommandPalette({ isOpen, onClose }: CommandPaletteProps) {
409
516
  alignItems: 'center',
410
517
  gap: 14,
411
518
  padding: '8px 16px',
412
- borderTop: '1px solid #f0f0f0',
519
+ borderTop: `1px solid ${tokenSchema.color.border.muted}`,
413
520
  fontSize: 11,
414
- color: '#b8b8b8',
521
+ color: tokenSchema.color.foreground.neutralSecondary,
415
522
  })}
416
523
  >
417
524
  <span>
@@ -1,46 +1,20 @@
1
- import { css } from '@keystar/ui/style'
2
-
3
- import type { CollectionMeta } from '../../types'
4
-
5
- export function CreateButtonLink(props: { children?: string; list: CollectionMeta }) {
6
- const { list, children = `New ${list.singular}` } = props
7
- return (
8
- <a
9
- href={`/${list.path}/create`}
10
- aria-label={`New ${list.singular}`}
11
- className={css({
12
- display: 'inline-flex',
13
- alignItems: 'center',
14
- gap: 6,
15
- paddingInline: '13px',
16
- paddingBlock: '7px',
17
- borderRadius: 6,
18
- border: '1px solid transparent',
19
- backgroundColor: '#111827',
20
- color: '#ffffff',
21
- fontSize: 13,
22
- fontWeight: 500,
23
- fontFamily: 'inherit',
24
- textDecoration: 'none',
25
- cursor: 'pointer',
26
- whiteSpace: 'nowrap',
27
- flexShrink: 0,
28
- letterSpacing: '-0.01em',
29
- transition: 'background 130ms',
30
-
31
- '&:hover': { backgroundColor: '#1f2937' },
32
- '&:active': { backgroundColor: '#374151' },
33
-
34
- '&:focus-visible': {
35
- outline: '2px solid #111827',
36
- outlineOffset: 2,
37
- },
38
- })}
39
- >
40
- <svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden>
41
- <path d="M6 1.5v9M1.5 6h9" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
42
- </svg>
43
- {children}
44
- </a>
45
- )
46
- }
1
+ import { Button } from '@keystar/ui/button'
2
+ import { Icon } from '@keystar/ui/icon'
3
+ import { plusIcon } from '@keystar/ui/icon/icons/plusIcon'
4
+ import { Text } from '@keystar/ui/typography'
5
+
6
+ import type { CollectionMeta } from '../../types'
7
+
8
+ export function CreateButtonLink(props: { children?: string; list: CollectionMeta }) {
9
+ const { list, children = `New ${list.singular}` } = props
10
+ return (
11
+ <Button
12
+ href={`/${list.path}/create`}
13
+ aria-label={`New ${list.singular}`}
14
+ prominence="high"
15
+ >
16
+ <Icon src={plusIcon} />
17
+ <Text>{children}</Text>
18
+ </Button>
19
+ )
20
+ }
@@ -34,45 +34,51 @@ export function GraphQLErrorNotice({
34
34
  })
35
35
  if (!errors.length) return null
36
36
 
37
+ // The Notice already sets role="alert" for the critical tone, but with
38
+ // aria-live="polite". Wrap it in an assertive live region (without a second
39
+ // role, to avoid a double role) so screen readers announce the failure
40
+ // immediately (WCAG 4.1.3 Status Messages).
37
41
  return (
38
- <Notice tone="critical">
39
- <Heading>Errors</Heading>
40
- <Content>
41
- <VStack elementType="ul" gap="large">
42
- {[
43
- ...(function* () {
44
- let i = 0
45
- for (const error of errors) {
46
- const lines = error.message.split('\n')
47
- for (const line of lines) {
48
- yield (
49
- <Text key={i++} elementType="li">
50
- {line}
51
- </Text>
52
- )
53
- }
54
-
55
- if (
56
- 'result' in error &&
57
- typeof error.result === 'object' &&
58
- error.result !== null &&
59
- 'errors' in error.result &&
60
- Array.isArray(error.result.errors)
61
- ) {
62
- for (const { message } of error.result.errors) {
63
- if (typeof message !== 'string') continue
42
+ <div aria-live="assertive" aria-atomic="true">
43
+ <Notice tone="critical">
44
+ <Heading>Errors</Heading>
45
+ <Content>
46
+ <VStack elementType="ul" gap="large">
47
+ {[
48
+ ...(function* () {
49
+ let i = 0
50
+ for (const error of errors) {
51
+ const lines = error.message.split('\n')
52
+ for (const line of lines) {
64
53
  yield (
65
54
  <Text key={i++} elementType="li">
66
- {message}
55
+ {line}
67
56
  </Text>
68
57
  )
69
58
  }
59
+
60
+ if (
61
+ 'result' in error &&
62
+ typeof error.result === 'object' &&
63
+ error.result !== null &&
64
+ 'errors' in error.result &&
65
+ Array.isArray(error.result.errors)
66
+ ) {
67
+ for (const { message } of error.result.errors) {
68
+ if (typeof message !== 'string') continue
69
+ yield (
70
+ <Text key={i++} elementType="li">
71
+ {message}
72
+ </Text>
73
+ )
74
+ }
75
+ }
70
76
  }
71
- }
72
- })(),
73
- ]}
74
- </VStack>
75
- </Content>
76
- </Notice>
77
+ })(),
78
+ ]}
79
+ </VStack>
80
+ </Content>
81
+ </Notice>
82
+ </div>
77
83
  )
78
84
  }
@@ -1,6 +1,6 @@
1
1
  import Link from 'next/link'
2
2
 
3
- import { css } from '@keystar/ui/style'
3
+ import { css, tokenSchema } from '@keystar/ui/style'
4
4
 
5
5
  import { useNixxie } from '../context'
6
6
 
@@ -22,7 +22,7 @@ function DefaultLogo() {
22
22
  outline: 0,
23
23
  flexShrink: 0,
24
24
  '&:focus-visible': {
25
- outline: '2px solid #000',
25
+ outline: `2px solid ${tokenSchema.color.scale.black}`,
26
26
  outlineOffset: 3,
27
27
  borderRadius: 4,
28
28
  },
@@ -37,14 +37,14 @@ function DefaultLogo() {
37
37
  width: 28,
38
38
  height: 28,
39
39
  borderRadius: 6,
40
- backgroundColor: '#000000',
40
+ backgroundColor: tokenSchema.color.scale.black,
41
41
  flexShrink: 0,
42
42
  })}
43
43
  >
44
44
  <svg width="13" height="13" viewBox="0 0 14 14" fill="none">
45
45
  <path
46
46
  d="M2.5 11V3L11.5 11V3"
47
- stroke="white"
47
+ stroke={tokenSchema.color.scale.white}
48
48
  strokeWidth="1.8"
49
49
  strokeLinecap="round"
50
50
  strokeLinejoin="round"
@@ -58,7 +58,7 @@ function DefaultLogo() {
58
58
  fontSize: 14,
59
59
  fontWeight: 600,
60
60
  letterSpacing: '-0.03em',
61
- color: '#0a0a0a',
61
+ color: tokenSchema.color.foreground.neutralEmphasis,
62
62
  lineHeight: 1,
63
63
  fontFamily: "'Inter', -apple-system, BlinkMacSystemFont, sans-serif",
64
64
  })}