@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.
- package/README.md +2 -1
- package/layers/admin/app/components/AdminAccount.vue +3 -3
- package/layers/admin/app/components/AdminNav.vue +1 -1
- package/layers/admin/app/components/BlockFields.vue +2 -2
- package/layers/admin/app/components/BlockPreview.vue +8 -8
- package/layers/admin/app/components/BlockTree.vue +10 -10
- package/layers/admin/app/components/BlocksBody.vue +4 -4
- package/layers/admin/app/components/CollectionDeleteDialog.vue +7 -7
- package/layers/admin/app/components/CollectionEditor.vue +3 -3
- package/layers/admin/app/components/CollectionList.vue +91 -792
- package/layers/admin/app/components/CollectionListBulkBar.vue +48 -0
- package/layers/admin/app/components/CollectionListColumnsPanel.vue +37 -0
- package/layers/admin/app/components/CollectionListFilterPanel.vue +145 -0
- package/layers/admin/app/components/CollectionListPager.vue +70 -0
- package/layers/admin/app/components/CollectionListTable.vue +308 -0
- package/layers/admin/app/components/EditorStatus.vue +4 -4
- package/layers/admin/app/components/FieldsBody.vue +1 -1
- package/layers/admin/app/components/LocaleBar.vue +4 -4
- package/layers/admin/app/components/PageFields.vue +12 -12
- package/layers/admin/app/components/PageFieldsPane.vue +2 -2
- package/layers/admin/app/components/SeoFields.vue +19 -19
- package/layers/admin/app/components/SingletonEditor.vue +7 -7
- package/layers/admin/app/composables/useListBatchActions.ts +61 -0
- package/layers/admin/app/composables/useListFilterDraft.ts +128 -0
- package/layers/admin/app/composables/useListRows.ts +70 -0
- package/layers/admin/app/composables/useListSelection.ts +28 -0
- package/layers/admin/app/composables/useToolbarPanel.ts +38 -0
- package/layers/admin/app/layouts/admin.vue +7 -7
- package/layers/admin/app/pages/admin/[collection]/[id].nuxt.test.ts +2 -2
- package/layers/admin/app/pages/admin/[collection]/[id].vue +11 -11
- package/layers/admin/app/pages/admin/[collection]/index.vue +2 -2
- package/layers/admin/app/pages/admin/[collection]/publish-preview.nuxt.test.ts +1 -1
- package/layers/admin/app/pages/admin/index.vue +3 -3
- package/layers/admin/app/pages/admin/login.vue +6 -6
- package/layers/admin/app/pages/admin/media.vue +1 -1
- package/layers/admin/app/pages/admin/references.vue +3 -3
- package/layers/admin/app/utils/list-cell.ts +46 -0
- package/layers/admin/nuxt.config.ts +3 -0
- package/layers/core/modules/component-namespace/index.ts +20 -0
- package/layers/core/modules/component-namespace/shared.ts +24 -0
- package/layers/core/nuxt.config.ts +1 -0
- package/layers/core/server/utils/defineCollection.ts +0 -1
- package/layers/core/server/utils/serialize-collection.ts +0 -2
- package/layers/fields/app/utils/field-factories.ts +1 -1
- package/layers/media/app/components/MediaConflictDialog.vue +7 -7
- package/layers/media/app/components/MediaDeleteDialog.vue +5 -5
- package/layers/media/app/components/MediaGrid.vue +3 -3
- package/layers/media/app/components/MediaLibrary.vue +20 -20
- package/layers/media/app/components/MediaNewFolderDialog.vue +7 -7
- package/layers/media/app/components/MediaPathBar.vue +1 -1
- package/layers/media/app/components/MediaPicker.vue +3 -3
- package/layers/media/app/components/MediaRenameDialog.vue +8 -8
- package/layers/media/app/components/MediaTable.vue +2 -2
- package/layers/media/app/components/MediaToolbar.vue +4 -4
- package/layers/media/app/components/MediaUploadDialog.vue +9 -9
- package/layers/media/app/components/MediaViewer.vue +14 -14
- package/layers/media/app/components/field/Media.vue +9 -9
- package/layers/media/app/plugins/field-media.client.ts +3 -3
- package/layers/media/nuxt.config.ts +5 -1
- package/layers/public/app/pages/[...slug].vue +2 -2
- package/layers/public/app/pages/__kestrel/preview.vue +1 -1
- package/layers/public/nuxt.config.ts +3 -0
- package/layers/ui/app/components/field/DeadRefNote.vue +1 -1
- package/layers/ui/app/components/field/Repeater.vue +5 -5
- package/layers/ui/app/components/field/Slug.vue +2 -2
- package/layers/ui/app/components/ui/Button.vue +3 -3
- package/layers/ui/app/components/ui/ButtonGroup.vue +2 -2
- package/layers/ui/app/components/ui/Checkbox.vue +2 -2
- package/layers/ui/app/components/ui/Combobox.vue +5 -5
- package/layers/ui/app/components/ui/DatePicker.vue +2 -2
- package/layers/ui/app/components/ui/DateRangePicker.vue +2 -2
- package/layers/ui/app/components/ui/EmptyState.vue +1 -1
- package/layers/ui/app/components/ui/NumberInput.vue +4 -4
- package/layers/ui/app/components/ui/Select.vue +2 -2
- package/layers/ui/app/components/ui/TextInput.vue +5 -5
- package/layers/ui/app/components/ui/Textarea.vue +2 -2
- package/layers/ui/app/components/ui/TimeInput.vue +2 -2
- package/layers/ui/app/components/ui/Toasts.vue +4 -4
- package/layers/ui/app/composables/useToast.ts +1 -1
- package/layers/ui/nuxt.config.ts +5 -1
- package/package.json +1 -1
|
@@ -27,7 +27,7 @@ function rename(c: UploadItem) {
|
|
|
27
27
|
</script>
|
|
28
28
|
|
|
29
29
|
<template>
|
|
30
|
-
<
|
|
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
|
-
<
|
|
39
|
-
<
|
|
40
|
-
<
|
|
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
|
-
<
|
|
45
|
+
<KestrelUiTextInput v-model="names[c.id]" :aria-label="t('media.conflicts.newName', { name: c.filename })" />
|
|
46
46
|
<div class="media-conflicts__actions">
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
<
|
|
80
|
+
<KestrelUiField v-if="isImage" :label="t('mediaViewer.alt')" :hint="t('mediaViewer.altHint')">
|
|
81
81
|
<template #default="f">
|
|
82
|
-
<
|
|
82
|
+
<KestrelUiTextInput v-model="alt" v-bind="f" @keydown.enter="save" />
|
|
83
83
|
</template>
|
|
84
|
-
</
|
|
84
|
+
</KestrelUiField>
|
|
85
85
|
<div v-if="showAi" class="media-viewer__ai">
|
|
86
|
-
<
|
|
86
|
+
<KestrelUiField :label="t('mediaViewer.aiSourceTypeLabel')" :hint="t('mediaViewer.aiSourceTypeHint')">
|
|
87
87
|
<template #default="f">
|
|
88
|
-
<
|
|
88
|
+
<KestrelUiSelect v-model="aiSourceType" :options="aiSourceTypeOptions" v-bind="f" />
|
|
89
89
|
</template>
|
|
90
|
-
</
|
|
91
|
-
<
|
|
90
|
+
</KestrelUiField>
|
|
91
|
+
<KestrelUiField :label="t('mediaViewer.aiNote')" :hint="t('mediaViewer.aiNoteHint')">
|
|
92
92
|
<template #default="f">
|
|
93
|
-
<
|
|
93
|
+
<KestrelUiTextInput v-model="aiNote" v-bind="f" @keydown.enter="save" />
|
|
94
94
|
</template>
|
|
95
|
-
</
|
|
95
|
+
</KestrelUiField>
|
|
96
96
|
</div>
|
|
97
|
-
<
|
|
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
|
-
<
|
|
104
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
><
|
|
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
|
-
><
|
|
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)"><
|
|
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
|
-
<
|
|
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
|
-
</
|
|
175
|
+
</KestrelUiButton>
|
|
176
176
|
<span class="field-media__live" aria-live="polite">{{ liveMessage }}</span>
|
|
177
177
|
</div>
|
|
178
|
-
<
|
|
178
|
+
<KestrelMediaPicker v-model:open="pickerOpen" :multiple="multiple" :accept="accept" :initial-folder="initialFolder" :initial-selected="ids" @confirm="onConfirm" />
|
|
179
179
|
</template>
|
|
180
|
-
</
|
|
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
|
|
2
|
-
import {
|
|
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',
|
|
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
|
-
<
|
|
208
|
+
<KestrelBlockRenderer :blocks="(blocks as any[])" />
|
|
209
209
|
</LazyKestrelPreviewBridge>
|
|
210
|
-
<
|
|
210
|
+
<KestrelBlockRenderer v-else :blocks="(page?.content as any[]) ?? []" />
|
|
211
211
|
</article>
|
|
212
212
|
</NuxtLayout>
|
|
213
213
|
</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
|
-
<
|
|
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'
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
-
<
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
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
|
-
<
|
|
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'
|
|
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'
|
|
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
|
-
<
|
|
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'
|
|
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'
|
|
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);
|
|
@@ -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
|
-
<
|
|
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'
|
|
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 `<
|
|
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
|
package/layers/ui/nuxt.config.ts
CHANGED
|
@@ -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": "
|
|
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>",
|