@michaelthielemann/kestrel 2.1.0 → 3.0.1

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 (81) hide show
  1. package/README.md +2 -1
  2. package/layers/admin/app/components/AdminAccount.vue +3 -3
  3. package/layers/admin/app/components/AdminNav.vue +1 -1
  4. package/layers/admin/app/components/BlockFields.vue +2 -2
  5. package/layers/admin/app/components/BlockPreview.vue +8 -8
  6. package/layers/admin/app/components/BlockTree.vue +10 -10
  7. package/layers/admin/app/components/BlocksBody.vue +4 -4
  8. package/layers/admin/app/components/CollectionDeleteDialog.vue +7 -7
  9. package/layers/admin/app/components/CollectionEditor.vue +3 -3
  10. package/layers/admin/app/components/CollectionList.vue +91 -792
  11. package/layers/admin/app/components/CollectionListBulkBar.vue +48 -0
  12. package/layers/admin/app/components/CollectionListColumnsPanel.vue +37 -0
  13. package/layers/admin/app/components/CollectionListFilterPanel.vue +145 -0
  14. package/layers/admin/app/components/CollectionListPager.vue +70 -0
  15. package/layers/admin/app/components/CollectionListTable.vue +308 -0
  16. package/layers/admin/app/components/EditorStatus.vue +4 -4
  17. package/layers/admin/app/components/FieldsBody.vue +1 -1
  18. package/layers/admin/app/components/LocaleBar.vue +4 -4
  19. package/layers/admin/app/components/PageFields.vue +12 -12
  20. package/layers/admin/app/components/PageFieldsPane.vue +2 -2
  21. package/layers/admin/app/components/SeoFields.vue +19 -19
  22. package/layers/admin/app/components/SingletonEditor.vue +7 -7
  23. package/layers/admin/app/composables/useListBatchActions.ts +61 -0
  24. package/layers/admin/app/composables/useListFilterDraft.ts +128 -0
  25. package/layers/admin/app/composables/useListRows.ts +70 -0
  26. package/layers/admin/app/composables/useListSelection.ts +28 -0
  27. package/layers/admin/app/composables/useToolbarPanel.ts +38 -0
  28. package/layers/admin/app/layouts/admin.vue +7 -7
  29. package/layers/admin/app/pages/admin/[collection]/[id].nuxt.test.ts +2 -2
  30. package/layers/admin/app/pages/admin/[collection]/[id].vue +11 -11
  31. package/layers/admin/app/pages/admin/[collection]/index.vue +2 -2
  32. package/layers/admin/app/pages/admin/[collection]/publish-preview.nuxt.test.ts +1 -1
  33. package/layers/admin/app/pages/admin/index.vue +3 -3
  34. package/layers/admin/app/pages/admin/login.vue +6 -6
  35. package/layers/admin/app/pages/admin/media.vue +1 -1
  36. package/layers/admin/app/pages/admin/references.vue +3 -3
  37. package/layers/admin/app/utils/list-cell.ts +46 -0
  38. package/layers/admin/nuxt.config.ts +3 -0
  39. package/layers/core/modules/component-namespace/index.ts +20 -0
  40. package/layers/core/modules/component-namespace/shared.ts +24 -0
  41. package/layers/core/nuxt.config.ts +1 -0
  42. package/layers/core/server/utils/defineCollection.ts +0 -1
  43. package/layers/core/server/utils/serialize-collection.ts +0 -2
  44. package/layers/fields/app/utils/field-factories.ts +1 -1
  45. package/layers/media/app/components/MediaConflictDialog.vue +7 -7
  46. package/layers/media/app/components/MediaDeleteDialog.vue +5 -5
  47. package/layers/media/app/components/MediaGrid.vue +3 -3
  48. package/layers/media/app/components/MediaLibrary.vue +20 -20
  49. package/layers/media/app/components/MediaNewFolderDialog.vue +7 -7
  50. package/layers/media/app/components/MediaPathBar.vue +1 -1
  51. package/layers/media/app/components/MediaPicker.vue +3 -3
  52. package/layers/media/app/components/MediaRenameDialog.vue +8 -8
  53. package/layers/media/app/components/MediaTable.vue +2 -2
  54. package/layers/media/app/components/MediaToolbar.vue +4 -4
  55. package/layers/media/app/components/MediaUploadDialog.vue +9 -9
  56. package/layers/media/app/components/MediaViewer.vue +14 -14
  57. package/layers/media/app/components/field/Media.vue +9 -9
  58. package/layers/media/app/plugins/field-media.client.ts +3 -3
  59. package/layers/media/nuxt.config.ts +5 -1
  60. package/layers/public/app/pages/[...slug].vue +2 -2
  61. package/layers/public/app/pages/__kestrel/preview.vue +1 -1
  62. package/layers/public/nuxt.config.ts +3 -0
  63. package/layers/ui/app/components/field/DeadRefNote.vue +1 -1
  64. package/layers/ui/app/components/field/Repeater.vue +5 -5
  65. package/layers/ui/app/components/field/Slug.vue +2 -2
  66. package/layers/ui/app/components/ui/Button.vue +3 -3
  67. package/layers/ui/app/components/ui/ButtonGroup.vue +2 -2
  68. package/layers/ui/app/components/ui/Checkbox.vue +2 -2
  69. package/layers/ui/app/components/ui/Combobox.vue +5 -5
  70. package/layers/ui/app/components/ui/DatePicker.vue +2 -2
  71. package/layers/ui/app/components/ui/DateRangePicker.vue +2 -2
  72. package/layers/ui/app/components/ui/EmptyState.vue +1 -1
  73. package/layers/ui/app/components/ui/NumberInput.vue +4 -4
  74. package/layers/ui/app/components/ui/Select.vue +2 -2
  75. package/layers/ui/app/components/ui/TextInput.vue +5 -5
  76. package/layers/ui/app/components/ui/Textarea.vue +2 -2
  77. package/layers/ui/app/components/ui/TimeInput.vue +2 -2
  78. package/layers/ui/app/components/ui/Toasts.vue +4 -4
  79. package/layers/ui/app/composables/useToast.ts +1 -1
  80. package/layers/ui/nuxt.config.ts +5 -1
  81. package/package.json +1 -1
@@ -27,7 +27,7 @@ function rename(c: UploadItem) {
27
27
  </script>
28
28
 
29
29
  <template>
30
- <UiDialog
30
+ <KestrelUiDialog
31
31
  :open="open"
32
32
  :title="t('media.conflicts.title')"
33
33
  :description="t('media.conflicts.desc', { count: conflicts.length })"
@@ -35,22 +35,22 @@ function rename(c: UploadItem) {
35
35
  >
36
36
  <div class="media-conflicts__all">
37
37
  <span>{{ t('media.conflicts.applyAll') }}</span>
38
- <UiButton @click="emit('resolve-all', 'overwrite')">{{ t('media.conflicts.overwriteAll') }}</UiButton>
39
- <UiButton @click="emit('resolve-all', 'rename')">{{ t('media.conflicts.renameAll') }}</UiButton>
40
- <UiButton @click="emit('resolve-all', 'skip')">{{ t('media.conflicts.skipAll') }}</UiButton>
38
+ <KestrelUiButton @click="emit('resolve-all', 'overwrite')">{{ t('media.conflicts.overwriteAll') }}</KestrelUiButton>
39
+ <KestrelUiButton @click="emit('resolve-all', 'rename')">{{ t('media.conflicts.renameAll') }}</KestrelUiButton>
40
+ <KestrelUiButton @click="emit('resolve-all', 'skip')">{{ t('media.conflicts.skipAll') }}</KestrelUiButton>
41
41
  </div>
42
42
  <ul class="media-conflicts__list">
43
43
  <li v-for="c in conflicts" :key="c.id" class="media-conflicts__row">
44
44
  <span class="media-conflicts__name">{{ c.filename }}</span>
45
- <UiTextInput v-model="names[c.id]" :aria-label="t('media.conflicts.newName', { name: c.filename })" />
45
+ <KestrelUiTextInput v-model="names[c.id]" :aria-label="t('media.conflicts.newName', { name: c.filename })" />
46
46
  <div class="media-conflicts__actions">
47
- <UiButton @click="emit('resolve', c.id, 'overwrite', undefined)">{{ t('media.conflicts.overwrite') }}</UiButton>
48
- <UiButton variant="primary" @click="rename(c)">{{ t('media.conflicts.rename') }}</UiButton>
49
- <UiButton @click="emit('resolve', c.id, 'skip', undefined)">{{ t('media.conflicts.skip') }}</UiButton>
47
+ <KestrelUiButton @click="emit('resolve', c.id, 'overwrite', undefined)">{{ t('media.conflicts.overwrite') }}</KestrelUiButton>
48
+ <KestrelUiButton variant="primary" @click="rename(c)">{{ t('media.conflicts.rename') }}</KestrelUiButton>
49
+ <KestrelUiButton @click="emit('resolve', c.id, 'skip', undefined)">{{ t('media.conflicts.skip') }}</KestrelUiButton>
50
50
  </div>
51
51
  </li>
52
52
  </ul>
53
- </UiDialog>
53
+ </KestrelUiDialog>
54
54
  </template>
55
55
 
56
56
  <style lang="scss" scoped>
@@ -63,7 +63,7 @@ function save() {
63
63
  </script>
64
64
 
65
65
  <template>
66
- <UiDialog :open="open" size="xl" :title="file?.filename ?? ''" @update:open="(v) => emit('update:open', v)">
66
+ <KestrelUiDialog :open="open" size="xl" :title="file?.filename ?? ''" @update:open="(v) => emit('update:open', v)">
67
67
  <div v-if="file" class="media-viewer">
68
68
  <div class="media-viewer__preview">
69
69
  <img v-if="isImage" :src="file.src" :srcset="file.srcset" sizes="(max-width: 60rem) 100vw, 60rem" :alt="file.alt ?? file.filename" />
@@ -77,33 +77,33 @@ function save() {
77
77
  <div><dt>{{ t('mediaViewer.folder') }}</dt><dd>{{ file.folder || '/' }}</dd></div>
78
78
  <div><dt>{{ t('mediaViewer.uploaded') }}</dt><dd>{{ uploaded }}</dd></div>
79
79
  </dl>
80
- <UiField v-if="isImage" :label="t('mediaViewer.alt')" :hint="t('mediaViewer.altHint')">
80
+ <KestrelUiField v-if="isImage" :label="t('mediaViewer.alt')" :hint="t('mediaViewer.altHint')">
81
81
  <template #default="f">
82
- <UiTextInput v-model="alt" v-bind="f" @keydown.enter="save" />
82
+ <KestrelUiTextInput v-model="alt" v-bind="f" @keydown.enter="save" />
83
83
  </template>
84
- </UiField>
84
+ </KestrelUiField>
85
85
  <div v-if="showAi" class="media-viewer__ai">
86
- <UiField :label="t('mediaViewer.aiSourceTypeLabel')" :hint="t('mediaViewer.aiSourceTypeHint')">
86
+ <KestrelUiField :label="t('mediaViewer.aiSourceTypeLabel')" :hint="t('mediaViewer.aiSourceTypeHint')">
87
87
  <template #default="f">
88
- <UiSelect v-model="aiSourceType" :options="aiSourceTypeOptions" v-bind="f" />
88
+ <KestrelUiSelect v-model="aiSourceType" :options="aiSourceTypeOptions" v-bind="f" />
89
89
  </template>
90
- </UiField>
91
- <UiField :label="t('mediaViewer.aiNote')" :hint="t('mediaViewer.aiNoteHint')">
90
+ </KestrelUiField>
91
+ <KestrelUiField :label="t('mediaViewer.aiNote')" :hint="t('mediaViewer.aiNoteHint')">
92
92
  <template #default="f">
93
- <UiTextInput v-model="aiNote" v-bind="f" @keydown.enter="save" />
93
+ <KestrelUiTextInput v-model="aiNote" v-bind="f" @keydown.enter="save" />
94
94
  </template>
95
- </UiField>
95
+ </KestrelUiField>
96
96
  </div>
97
- <UiAlert v-if="error" variant="error">{{ error }}</UiAlert>
97
+ <KestrelUiAlert v-if="error" variant="error">{{ error }}</KestrelUiAlert>
98
98
  <!-- Optional per-file extra panel (e.g. proofing comments). Empty by default. -->
99
99
  <slot name="extra" :file="file" />
100
100
  </aside>
101
101
  </div>
102
102
  <template v-if="file" #footer>
103
- <UiButton :disabled="busy" @click="emit('update:open', false)">{{ t('common.close') }}</UiButton>
104
- <UiButton v-if="isImage" variant="primary" :disabled="busy || !dirty" @click="save">{{ t('common.save') }}</UiButton>
103
+ <KestrelUiButton :disabled="busy" @click="emit('update:open', false)">{{ t('common.close') }}</KestrelUiButton>
104
+ <KestrelUiButton v-if="isImage" variant="primary" :disabled="busy || !dirty" @click="save">{{ t('common.save') }}</KestrelUiButton>
105
105
  </template>
106
- </UiDialog>
106
+ </KestrelUiDialog>
107
107
  </template>
108
108
 
109
109
  <style lang="scss" scoped>
@@ -114,7 +114,7 @@ defineExpose({ onConfirm, removeId, onDragStart, onDrop, moveItem })
114
114
  </script>
115
115
 
116
116
  <template>
117
- <UiField :id="id" :label="name" :error="error" :required="required">
117
+ <KestrelUiField :id="id" :label="name" :error="error" :required="required">
118
118
  <template #default="f">
119
119
  <div class="field-media">
120
120
  <!-- eslint-disable-next-line vuejs-accessibility/no-static-element-interactions -- @dragleave is a mouse-only progressive enhancement; the move-earlier/move-later buttons below give the same reorder fully keyboard access -->
@@ -135,7 +135,7 @@ defineExpose({ onConfirm, removeId, onDragStart, onDrop, moveItem })
135
135
  @drop.prevent="onDrop(i)"
136
136
  @dragend="onDragEnd"
137
137
  >
138
- <MediaThumb v-if="isImage(r)" draggable="false" class="field-media__thumb" :src="r.src" :srcset="srcsetOf(r)" sizes="80px" :alt="r.alt ?? ''" :thumbhash="r.thumbhash" />
138
+ <KestrelMediaThumb v-if="isImage(r)" draggable="false" class="field-media__thumb" :src="r.src" :srcset="srcsetOf(r)" sizes="80px" :alt="r.alt ?? ''" :thumbhash="r.thumbhash" />
139
139
  <a v-else draggable="false" class="field-media__file" :href="r.src" target="_blank" rel="noopener noreferrer" :title="filenameOf(r)">
140
140
  <span class="field-media__badge" aria-hidden="true">{{ fileExt(r) }}</span>
141
141
  <span class="field-media__filename">{{ filenameOf(r) }}</span>
@@ -148,7 +148,7 @@ defineExpose({ onConfirm, removeId, onDragStart, onDrop, moveItem })
148
148
  :aria-label="t('field.media.move_earlier', { name: nameOf(r, i) })"
149
149
  :disabled="i === 0"
150
150
  @click="moveItem(i, -1)"
151
- ><UiIcon name="chevron-left" size="0.875rem" /></button>
151
+ ><KestrelUiIcon name="chevron-left" size="0.875rem" /></button>
152
152
  <button
153
153
  type="button"
154
154
  draggable="false"
@@ -156,13 +156,13 @@ defineExpose({ onConfirm, removeId, onDragStart, onDrop, moveItem })
156
156
  :aria-label="t('field.media.move_later', { name: nameOf(r, i) })"
157
157
  :disabled="i === resolved.length - 1"
158
158
  @click="moveItem(i, 1)"
159
- ><UiIcon name="chevron-right" size="0.875rem" /></button>
159
+ ><KestrelUiIcon name="chevron-right" size="0.875rem" /></button>
160
160
  </template>
161
- <button v-if="!disabled" type="button" draggable="false" class="field-media__remove" :aria-label="t('field.media.remove', { name: nameOf(r, i) })" @click="removeId(r.id)"><UiIcon name="x" size="1rem" /></button>
161
+ <button v-if="!disabled" type="button" draggable="false" class="field-media__remove" :aria-label="t('field.media.remove', { name: nameOf(r, i) })" @click="removeId(r.id)"><KestrelUiIcon name="x" size="1rem" /></button>
162
162
  </li>
163
163
  </ul>
164
164
  <p v-else class="field-media__empty">{{ t('field.media.empty') }}</p>
165
- <UiButton
165
+ <KestrelUiButton
166
166
  v-if="!disabled"
167
167
  :id="f.id"
168
168
  type="button"
@@ -172,12 +172,12 @@ defineExpose({ onConfirm, removeId, onDragStart, onDrop, moveItem })
172
172
  @click="pickerOpen = true"
173
173
  >
174
174
  {{ multiple ? t('field.media.add') : (resolved.length ? t('field.media.replace') : t('field.media.select')) }}
175
- </UiButton>
175
+ </KestrelUiButton>
176
176
  <span class="field-media__live" aria-live="polite">{{ liveMessage }}</span>
177
177
  </div>
178
- <MediaPicker v-model:open="pickerOpen" :multiple="multiple" :accept="accept" :initial-folder="initialFolder" :initial-selected="ids" @confirm="onConfirm" />
178
+ <KestrelMediaPicker v-model:open="pickerOpen" :multiple="multiple" :accept="accept" :initial-folder="initialFolder" :initial-selected="ids" @confirm="onConfirm" />
179
179
  </template>
180
- </UiField>
180
+ </KestrelUiField>
181
181
  </template>
182
182
 
183
183
  <style lang="scss" scoped>
@@ -1,6 +1,6 @@
1
- // components are NOT auto-imported in a plugin's script context — pull FieldMedia from #components
2
- import { FieldMedia } from '#components'
1
+ // components are NOT auto-imported in a plugin's script context — pull KestrelFieldMedia from #components
2
+ import { KestrelFieldMedia } from '#components'
3
3
 
4
4
  export default defineNuxtPlugin(() => {
5
- registerFieldComponent('media', FieldMedia)
5
+ registerFieldComponent('media', KestrelFieldMedia)
6
6
  })
@@ -1,3 +1,7 @@
1
+ import { kestrelComponents } from '../core/modules/component-namespace/shared'
2
+
1
3
  // Marks `media` as a Nuxt layer (so its components/server code are auto-registered). The media config
2
4
  // + the `kestrel` module that resolves it now live in the core layer (single non-auth config source).
3
- export default defineNuxtConfig({})
5
+ export default defineNuxtConfig({
6
+ components: [kestrelComponents(import.meta.url)],
7
+ })
@@ -205,9 +205,9 @@ useSeoMeta({
205
205
  <!-- Editor preview: the bridge swaps in the editor's live (unsaved) tree over postMessage and makes
206
206
  blocks selectable; the saved content renders until the first message. Normal path unchanged. -->
207
207
  <LazyKestrelPreviewBridge v-if="previewActive" :blocks="(page?.content as any[]) ?? []" v-slot="{ blocks }">
208
- <BlockRenderer :blocks="(blocks as any[])" />
208
+ <KestrelBlockRenderer :blocks="(blocks as any[])" />
209
209
  </LazyKestrelPreviewBridge>
210
- <BlockRenderer v-else :blocks="(page?.content as any[]) ?? []" />
210
+ <KestrelBlockRenderer v-else :blocks="(page?.content as any[]) ?? []" />
211
211
  </article>
212
212
  </NuxtLayout>
213
213
  </template>
@@ -41,6 +41,6 @@ useHead({
41
41
 
42
42
  <template>
43
43
  <KestrelPreviewBridge :blocks="ticketBlocks" v-slot="{ blocks }">
44
- <BlockRenderer :blocks="(blocks as any[])" />
44
+ <KestrelBlockRenderer :blocks="(blocks as any[])" />
45
45
  </KestrelPreviewBridge>
46
46
  </template>
@@ -1,6 +1,9 @@
1
1
  import { fileURLToPath } from 'node:url'
2
2
 
3
+ import { kestrelComponents } from '../core/modules/component-namespace/shared'
4
+
3
5
  export default defineNuxtConfig({
6
+ components: [kestrelComponents(import.meta.url)],
4
7
  modules: [
5
8
  fileURLToPath(new URL('./modules/prerender-routes/index.ts', import.meta.url)),
6
9
  fileURLToPath(new URL('./modules/prune-media/index.ts', import.meta.url)),
@@ -9,7 +9,7 @@ const { t } = useT()
9
9
 
10
10
  <template>
11
11
  <p v-if="show" class="field-dead-ref" role="note">
12
- <UiIcon name="triangle-alert" :size="13" class="field-dead-ref__icon" />{{ t('deadRefs.fieldNote') }}
12
+ <KestrelUiIcon name="triangle-alert" :size="13" class="field-dead-ref__icon" />{{ t('deadRefs.fieldNote') }}
13
13
  </p>
14
14
  </template>
15
15
 
@@ -239,7 +239,7 @@ function insertRowAt(at: number) {
239
239
  </template>
240
240
 
241
241
  <style lang="scss">
242
- @use '../../assets/scss/mixins' as *;
242
+ @use '../../assets/scss/mixins';
243
243
 
244
244
  .ui-repeater {
245
245
  display: flex;
@@ -294,7 +294,7 @@ function insertRowAt(at: number) {
294
294
  }
295
295
 
296
296
  &__insert {
297
- @include focus-ring;
297
+ @include mixins.focus-ring;
298
298
  position: relative;
299
299
  z-index: 1;
300
300
  display: flex;
@@ -388,7 +388,7 @@ function insertRowAt(at: number) {
388
388
  &__move,
389
389
  &__duplicate,
390
390
  &__remove {
391
- @include focus-ring;
391
+ @include mixins.focus-ring;
392
392
  display: flex;
393
393
  align-items: center;
394
394
  justify-content: center;
@@ -428,7 +428,7 @@ function insertRowAt(at: number) {
428
428
  }
429
429
 
430
430
  &__add {
431
- @include focus-ring;
431
+ @include mixins.focus-ring;
432
432
  align-self: flex-start;
433
433
  border: 1px solid var(--color-border);
434
434
  background: var(--color-surface);
@@ -450,7 +450,7 @@ function insertRowAt(at: number) {
450
450
  }
451
451
 
452
452
  &__live {
453
- @include sr-only;
453
+ @include mixins.sr-only;
454
454
  }
455
455
 
456
456
  @media (prefers-reduced-motion: reduce) {
@@ -25,7 +25,7 @@ function onBlur() {
25
25
  </script>
26
26
 
27
27
  <template>
28
- <UiField :id="id" :label="name" :error="error ?? undefined">
28
+ <KestrelUiField :id="id" :label="name" :error="error ?? undefined">
29
29
  <template #default="f">
30
30
  <div class="field-slug" :data-prefixed="!!prefix">
31
31
  <span v-if="prefix" class="field-slug__prefix">{{ prefix }}</span>
@@ -43,7 +43,7 @@ function onBlur() {
43
43
  />
44
44
  </div>
45
45
  </template>
46
- </UiField>
46
+ </KestrelUiField>
47
47
  </template>
48
48
 
49
49
  <style lang="scss" scoped>
@@ -23,16 +23,16 @@ withDefaults(
23
23
  :aria-busy="loading || undefined"
24
24
  >
25
25
  <span v-if="loading" class="ui-button__spinner" aria-hidden="true" />
26
- <UiIcon v-else-if="icon" :name="icon" class="ui-button__icon" />
26
+ <KestrelUiIcon v-else-if="icon" :name="icon" class="ui-button__icon" />
27
27
  <span v-if="$slots.default" class="ui-button__label"><slot /></span>
28
28
  </button>
29
29
  </template>
30
30
 
31
31
  <style lang="scss">
32
- @use '../../assets/scss/mixins' as *;
32
+ @use '../../assets/scss/mixins';
33
33
 
34
34
  .ui-button {
35
- @include focus-ring;
35
+ @include mixins.focus-ring;
36
36
  display: inline-flex;
37
37
  align-items: center;
38
38
  justify-content: center;
@@ -49,7 +49,7 @@ function onUpdate(v: unknown) {
49
49
  </template>
50
50
 
51
51
  <style lang="scss">
52
- @use '../../assets/scss/mixins' as *;
52
+ @use '../../assets/scss/mixins';
53
53
 
54
54
  .ui-btngroup {
55
55
  display: inline-flex;
@@ -62,7 +62,7 @@ function onUpdate(v: unknown) {
62
62
  width: fit-content;
63
63
 
64
64
  &__item {
65
- @include focus-ring;
65
+ @include mixins.focus-ring;
66
66
  display: inline-flex;
67
67
  align-items: center;
68
68
  gap: var(--space-1);
@@ -36,10 +36,10 @@ watch(() => props.indeterminate, syncIndeterminate)
36
36
  </template>
37
37
 
38
38
  <style lang="scss">
39
- @use '../../assets/scss/mixins' as *;
39
+ @use '../../assets/scss/mixins';
40
40
 
41
41
  .ui-checkbox {
42
- @include focus-ring;
42
+ @include mixins.focus-ring;
43
43
  width: 1.25rem;
44
44
  height: 1.25rem;
45
45
  accent-color: var(--color-primary);
@@ -197,7 +197,7 @@ function moveChip(from: number, to: number) {
197
197
  </template>
198
198
 
199
199
  <style lang="scss">
200
- @use '../../assets/scss/mixins' as *;
200
+ @use '../../assets/scss/mixins';
201
201
 
202
202
  .ui-combobox {
203
203
  &__anchor {
@@ -232,7 +232,7 @@ function moveChip(from: number, to: number) {
232
232
  font-size: var(--text-sm);
233
233
  }
234
234
  &__chip-remove {
235
- @include focus-ring;
235
+ @include mixins.focus-ring;
236
236
  display: inline-flex;
237
237
  align-items: center;
238
238
  justify-content: center;
@@ -246,7 +246,7 @@ function moveChip(from: number, to: number) {
246
246
  line-height: 1;
247
247
  }
248
248
  &__chip-move {
249
- @include focus-ring;
249
+ @include mixins.focus-ring;
250
250
  display: inline-flex;
251
251
  align-items: center;
252
252
  justify-content: center;
@@ -269,7 +269,7 @@ function moveChip(from: number, to: number) {
269
269
 
270
270
  &:disabled { cursor: default; } // stays collapsed (the reveal selector excludes :disabled)
271
271
  }
272
- &__live { @include sr-only; }
272
+ &__live { @include mixins.sr-only; }
273
273
  &__chip {
274
274
  &:hover,
275
275
  &:focus-within {
@@ -302,7 +302,7 @@ function moveChip(from: number, to: number) {
302
302
  padding: var(--space-1) 0;
303
303
  }
304
304
  &__icon {
305
- @include focus-ring;
305
+ @include mixins.focus-ring;
306
306
  border: 0;
307
307
  background: transparent;
308
308
  cursor: pointer;
@@ -87,6 +87,6 @@ const value = computed<DateValue | undefined>({
87
87
 
88
88
  <style lang="scss">
89
89
  // Shared, global (non-scoped) datepicker chrome — the calendar is teleported to <body>.
90
- @use '../../assets/scss/datepicker' as *;
91
- @include ui-datepicker;
90
+ @use '../../assets/scss/datepicker';
91
+ @include datepicker.ui-datepicker;
92
92
  </style>
@@ -95,6 +95,6 @@ const value = computed<{ start: DateValue | undefined; end: DateValue | undefine
95
95
  // Shares the global (non-scoped) datepicker chrome — the calendar is teleported to <body>, so a
96
96
  // `scoped` block would silently drop these styles. Included explicitly rather than borrowing them
97
97
  // from DatePicker being present on the page.
98
- @use '../../assets/scss/datepicker' as *;
99
- @include ui-datepicker;
98
+ @use '../../assets/scss/datepicker';
99
+ @include datepicker.ui-datepicker;
100
100
  </style>
@@ -6,7 +6,7 @@ defineProps<{ icon?: IconName; title: string; description?: string }>()
6
6
 
7
7
  <template>
8
8
  <div class="ui-empty">
9
- <UiIcon v-if="icon" :name="icon" size="1.5rem" class="ui-empty__icon" />
9
+ <KestrelUiIcon v-if="icon" :name="icon" size="1.5rem" class="ui-empty__icon" />
10
10
  <p class="ui-empty__title">{{ title }}</p>
11
11
  <p v-if="description" class="ui-empty__desc">{{ description }}</p>
12
12
  <div v-if="$slots.action" class="ui-empty__action"><slot name="action" /></div>
@@ -90,17 +90,17 @@ function onKeydown(e: KeyboardEvent) {
90
90
  </template>
91
91
 
92
92
  <style lang="scss">
93
- @use '../../assets/scss/mixins' as *;
93
+ @use '../../assets/scss/mixins';
94
94
 
95
95
  .ui-number {
96
- @include input-base;
96
+ @include mixins.input-base;
97
97
  width: 100%;
98
98
 
99
99
  &::placeholder {
100
100
  color: var(--color-text-muted);
101
101
  }
102
102
  &--slim {
103
- @include input-slim;
103
+ @include mixins.input-slim;
104
104
  }
105
105
  }
106
106
 
@@ -166,7 +166,7 @@ function onKeydown(e: KeyboardEvent) {
166
166
  // separate rule (not nested) so `.ui-number-wrap--slim .ui-number-wrap__input` outranks the base element rule.
167
167
  .ui-number-wrap--slim {
168
168
  .ui-number-wrap__input {
169
- @include input-slim;
169
+ @include mixins.input-slim;
170
170
  }
171
171
  .ui-number-wrap__suffix {
172
172
  padding-inline: var(--space-2);
@@ -27,7 +27,7 @@ const model = defineModel<string | null>()
27
27
  </template>
28
28
 
29
29
  <style lang="scss">
30
- @use '../../assets/scss/mixins' as *;
30
+ @use '../../assets/scss/mixins';
31
31
 
32
32
  .ui-select {
33
33
  position: relative;
@@ -35,7 +35,7 @@ const model = defineModel<string | null>()
35
35
  width: 100%;
36
36
  }
37
37
  .ui-select__field {
38
- @include input-base;
38
+ @include mixins.input-base;
39
39
  width: 100%;
40
40
  appearance: none;
41
41
  // leave room for the overlaid chevron so long values don't run under it
@@ -49,13 +49,13 @@ const inputType = computed(() => (revealable.value && revealed.value ? 'text' :
49
49
  :aria-label="revealed ? t('input.hidePassword') : t('input.showPassword')"
50
50
  @click="revealed = !revealed"
51
51
  >
52
- <UiIcon :name="revealed ? 'eye-off' : 'eye'" size="1.125rem" />
52
+ <KestrelUiIcon :name="revealed ? 'eye-off' : 'eye'" size="1.125rem" />
53
53
  </button>
54
54
  </div>
55
55
  </template>
56
56
 
57
57
  <style lang="scss">
58
- @use '../../assets/scss/mixins' as *;
58
+ @use '../../assets/scss/mixins';
59
59
 
60
60
  .ui-input-wrap {
61
61
  position: relative;
@@ -64,7 +64,7 @@ const inputType = computed(() => (revealable.value && revealed.value ? 'text' :
64
64
  }
65
65
 
66
66
  .ui-input {
67
- @include input-base;
67
+ @include mixins.input-base;
68
68
  width: 100%;
69
69
 
70
70
  &::placeholder {
@@ -72,7 +72,7 @@ const inputType = computed(() => (revealable.value && revealed.value ? 'text' :
72
72
  }
73
73
 
74
74
  &--slim {
75
- @include input-slim;
75
+ @include mixins.input-slim;
76
76
  }
77
77
 
78
78
  // Leave room for the inset reveal button so the value never sits under it. Declared after `--slim` so a
@@ -83,7 +83,7 @@ const inputType = computed(() => (revealable.value && revealed.value ? 'text' :
83
83
  }
84
84
 
85
85
  .ui-input__reveal {
86
- @include focus-ring;
86
+ @include mixins.focus-ring;
87
87
  position: absolute;
88
88
  inset-block: 1px;
89
89
  inset-inline-end: 1px;
@@ -23,10 +23,10 @@ const model = defineModel<string | null>()
23
23
  </template>
24
24
 
25
25
  <style lang="scss">
26
- @use '../../assets/scss/mixins' as *;
26
+ @use '../../assets/scss/mixins';
27
27
 
28
28
  .ui-textarea {
29
- @include input-base;
29
+ @include mixins.input-base;
30
30
  width: 100%;
31
31
  font-family: inherit;
32
32
  line-height: var(--leading-normal);
@@ -10,9 +10,9 @@ const model = defineModel<string | null>()
10
10
  </template>
11
11
 
12
12
  <style lang="scss">
13
- @use '../../assets/scss/mixins' as *;
13
+ @use '../../assets/scss/mixins';
14
14
 
15
15
  .ui-time {
16
- @include input-base;
16
+ @include mixins.input-base;
17
17
  }
18
18
  </style>
@@ -30,7 +30,7 @@ const { t } = useT()
30
30
  >
31
31
  <span class="ui-toast__msg">{{ toast.message }}</span>
32
32
  <button type="button" class="ui-toast__close" :aria-label="t('toast.dismiss')" @click="toasts.dismiss(toast.id)">
33
- <UiIcon name="x" size="0.875rem" />
33
+ <KestrelUiIcon name="x" size="0.875rem" />
34
34
  </button>
35
35
  </div>
36
36
  </TransitionGroup>
@@ -39,9 +39,9 @@ const { t } = useT()
39
39
  </template>
40
40
 
41
41
  <style lang="scss">
42
- @use '../../assets/scss/mixins' as *;
42
+ @use '../../assets/scss/mixins';
43
43
 
44
- .ui-toasts__live { @include sr-only; }
44
+ .ui-toasts__live { @include mixins.sr-only; }
45
45
 
46
46
  .ui-toasts {
47
47
  position: fixed;
@@ -76,7 +76,7 @@ const { t } = useT()
76
76
  &__msg { flex: 1 1 auto; min-width: 0; }
77
77
 
78
78
  &__close {
79
- @include focus-ring;
79
+ @include mixins.focus-ring;
80
80
  flex-shrink: 0;
81
81
  display: inline-flex;
82
82
  padding: var(--space-1);
@@ -6,7 +6,7 @@ export interface Toast { id: number; type: ToastType; message: string }
6
6
  /**
7
7
  * Transient notification queue: `push` returns an id, auto-expires after `timeout` (0 = sticky),
8
8
  * and the queue is capped (oldest dropped). Pure factory so the logic is unit-testable; `useToast`
9
- * shares one instance app-wide. The teleported `<UiToasts>` container renders `items`.
9
+ * shares one instance app-wide. The teleported `<KestrelUiToasts>` container renders `items`.
10
10
  */
11
11
  export function createToastStore(opts: { limit?: number; defaultTimeout?: number } = {}) {
12
12
  const limit = opts.limit ?? 4
@@ -1,5 +1,9 @@
1
+ import { kestrelComponents } from '../core/modules/component-namespace/shared'
2
+
1
3
  // The UI design system (tokens / reset / base in app/assets/scss/main.scss) is intentionally NOT loaded
2
4
  // app-globally — that would leak Kestrel's CSS reset + base styles onto a consumer's public site and fight
3
5
  // their own CSS (e.g. Bootstrap). It is imported by the admin layout (layers/admin/app/layouts/admin.vue)
4
6
  // instead, which scopes it to /admin: every admin route uses that layout, and /admin/** is ssr:false.
5
- export default defineNuxtConfig({})
7
+ export default defineNuxtConfig({
8
+ components: [kestrelComponents(import.meta.url)],
9
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaelthielemann/kestrel",
3
- "version": "2.1.0",
3
+ "version": "3.0.1",
4
4
  "description": "A slim, collection-driven Nuxt 4 CMS meta-layer with a runtime schema engine. Add `extends: ['@michaelthielemann/kestrel']`, define collections, and the database migrates itself.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Michael Thielemann <283621694+MichaelThielemann@users.noreply.github.com>",