@octabits-io/nuxt-ui-kit 0.2.0 → 0.3.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.
- package/dist/api/index.d.ts +65 -0
- package/dist/api/index.js +51 -0
- package/dist/auth/index.d.ts +240 -0
- package/dist/auth/index.js +273 -0
- package/dist/i18n/index.d.ts +49 -0
- package/dist/i18n/index.js +80 -0
- package/dist/index.d.ts +65 -310
- package/dist/index.js +70 -325
- package/dist/locale/index.d.ts +140 -0
- package/dist/locale/index.js +140 -0
- package/dist/zod/index.d.ts +23 -1
- package/dist/zod/index.js +44 -1
- package/package.json +33 -2
- package/src/components/LocaleInput.vue +132 -0
- package/src/components/LocaleTab.vue +44 -0
- package/src/components/LocaleTextarea.vue +131 -0
- package/src/components/PageAction.vue +88 -0
- package/src/components/PageActionMenu.vue +34 -0
- package/src/components/PageHeader.vue +110 -0
- package/src/components/PageUtilityActions.vue +31 -0
- package/src/components/SubSidebar.vue +10 -1
- package/src/components/TranslationBadge.vue +65 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shipped as source: the consumer's Vite compiles this SFC. All imports are
|
|
3
|
+
// explicit — no reliance on the consumer's auto-import configuration.
|
|
4
|
+
import { computed, useSlots } from 'vue'
|
|
5
|
+
import type { RouteLocationRaw } from 'vue-router'
|
|
6
|
+
import UTooltip from '@nuxt/ui/components/Tooltip.vue'
|
|
7
|
+
import UButton from '@nuxt/ui/components/Button.vue'
|
|
8
|
+
import type { ButtonProps } from '@nuxt/ui'
|
|
9
|
+
|
|
10
|
+
type Tone = 'primary' | 'neutral' | 'destructive'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Normalized page-header action button: icon-only renders with a tooltip and
|
|
14
|
+
* aria-label from the required `label`; a default slot or `showLabel` renders
|
|
15
|
+
* the label text inline. `destructive` is only valid inside `PageActionMenu`.
|
|
16
|
+
*/
|
|
17
|
+
const props = withDefaults(defineProps<{
|
|
18
|
+
/** Icon name (required). */
|
|
19
|
+
icon: string
|
|
20
|
+
/** Required. Used as tooltip text for icon-only buttons and aria-label otherwise. */
|
|
21
|
+
label: string
|
|
22
|
+
/** Visual tone. `destructive` is only valid inside PageActionMenu. */
|
|
23
|
+
tone?: Tone
|
|
24
|
+
loading?: boolean
|
|
25
|
+
disabled?: boolean
|
|
26
|
+
to?: RouteLocationRaw
|
|
27
|
+
/** Link target, e.g. '_blank' for external links (only meaningful with `to`). */
|
|
28
|
+
target?: string
|
|
29
|
+
/** Force the button to render label text. If absent, a default slot determines it. */
|
|
30
|
+
showLabel?: boolean
|
|
31
|
+
/** Whether the tooltip should respect global disabled state. */
|
|
32
|
+
tooltipDisabled?: boolean
|
|
33
|
+
}>(), {
|
|
34
|
+
tone: 'neutral',
|
|
35
|
+
loading: false,
|
|
36
|
+
disabled: false,
|
|
37
|
+
showLabel: false,
|
|
38
|
+
tooltipDisabled: false,
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const slots = useSlots()
|
|
42
|
+
|
|
43
|
+
const hasLabelSlot = computed(() => Boolean(slots.default))
|
|
44
|
+
const isIconOnly = computed(() => !hasLabelSlot.value && !props.showLabel)
|
|
45
|
+
|
|
46
|
+
const buttonProps = computed<Partial<ButtonProps>>(() => {
|
|
47
|
+
const base: Partial<ButtonProps> = {
|
|
48
|
+
icon: props.icon,
|
|
49
|
+
size: 'sm',
|
|
50
|
+
loading: props.loading,
|
|
51
|
+
disabled: props.disabled || props.loading,
|
|
52
|
+
to: props.to,
|
|
53
|
+
target: props.target,
|
|
54
|
+
}
|
|
55
|
+
switch (props.tone) {
|
|
56
|
+
case 'primary':
|
|
57
|
+
return { ...base, color: 'primary', variant: 'solid' }
|
|
58
|
+
case 'destructive':
|
|
59
|
+
if (import.meta.dev) {
|
|
60
|
+
console.warn('[PageAction] tone="destructive" should be used inside PageActionMenu, not inline.')
|
|
61
|
+
}
|
|
62
|
+
return { ...base, color: 'error', variant: 'ghost' }
|
|
63
|
+
case 'neutral':
|
|
64
|
+
default:
|
|
65
|
+
return { ...base, color: 'neutral', variant: 'ghost' }
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
if (import.meta.dev && !props.label) {
|
|
70
|
+
console.warn('[PageAction] `label` is required (used as tooltip text for icon-only buttons).')
|
|
71
|
+
}
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<template>
|
|
75
|
+
<UTooltip v-if="isIconOnly" :text="label" :disabled="tooltipDisabled">
|
|
76
|
+
<UButton
|
|
77
|
+
v-bind="buttonProps"
|
|
78
|
+
:aria-label="label"
|
|
79
|
+
/>
|
|
80
|
+
</UTooltip>
|
|
81
|
+
<UButton
|
|
82
|
+
v-else
|
|
83
|
+
v-bind="buttonProps"
|
|
84
|
+
:label="showLabel ? label : undefined"
|
|
85
|
+
>
|
|
86
|
+
<slot />
|
|
87
|
+
</UButton>
|
|
88
|
+
</template>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shipped as source: the consumer's Vite compiles this SFC. All imports are
|
|
3
|
+
// explicit — no reliance on the consumer's auto-import configuration.
|
|
4
|
+
// i18n key contract: pageChrome.moreActions.
|
|
5
|
+
import { useI18n } from 'vue-i18n'
|
|
6
|
+
import UDropdownMenu from '@nuxt/ui/components/DropdownMenu.vue'
|
|
7
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
8
|
+
import PageAction from './PageAction.vue'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Overflow/dropdown menu trigger that matches PageAction sizing. Accepts the
|
|
12
|
+
* grouped `DropdownMenuItem[][]` shape. Destructive actions should set
|
|
13
|
+
* `color: 'error'` on the item.
|
|
14
|
+
*/
|
|
15
|
+
withDefaults(defineProps<{
|
|
16
|
+
items: DropdownMenuItem[][]
|
|
17
|
+
icon?: string
|
|
18
|
+
label?: string
|
|
19
|
+
}>(), {
|
|
20
|
+
icon: 'i-lucide-ellipsis-vertical',
|
|
21
|
+
label: undefined,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const { t } = useI18n()
|
|
25
|
+
</script>
|
|
26
|
+
|
|
27
|
+
<template>
|
|
28
|
+
<UDropdownMenu v-if="items.length" :items="items">
|
|
29
|
+
<PageAction
|
|
30
|
+
:icon="icon"
|
|
31
|
+
:label="label ?? t('pageChrome.moreActions')"
|
|
32
|
+
/>
|
|
33
|
+
</UDropdownMenu>
|
|
34
|
+
</template>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shipped as source: the consumer's Vite compiles this SFC. All imports are
|
|
3
|
+
// explicit — no reliance on the consumer's auto-import configuration.
|
|
4
|
+
// i18n key contract: pageChrome.back (+ PageActionMenu/PageUtilityActions keys).
|
|
5
|
+
import { computed } from 'vue'
|
|
6
|
+
import { useI18n } from 'vue-i18n'
|
|
7
|
+
import { useRouter, type RouteLocationRaw } from 'vue-router'
|
|
8
|
+
import UButton from '@nuxt/ui/components/Button.vue'
|
|
9
|
+
import USkeleton from '@nuxt/ui/components/Skeleton.vue'
|
|
10
|
+
import type { DropdownMenuItem } from '@nuxt/ui'
|
|
11
|
+
import PageActionMenu from './PageActionMenu.vue'
|
|
12
|
+
import PageUtilityActions from './PageUtilityActions.vue'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Standard page header.
|
|
16
|
+
*
|
|
17
|
+
* Conventions enforced by this component and its siblings:
|
|
18
|
+
* - Max 3 neutral icon buttons inline in `#actions`; more go in `overflowItems`.
|
|
19
|
+
* - Destructive actions are ALWAYS placed inside the overflow menu, never inline.
|
|
20
|
+
* - Header height, spacing, and tooltip behavior are normalized via `PageAction`.
|
|
21
|
+
* - Utility triggers (Help, AI history, …) live in `#utility` (default =
|
|
22
|
+
* `PageUtilityActions`) and render as LABELED buttons, not icon-only.
|
|
23
|
+
*/
|
|
24
|
+
const props = withDefaults(defineProps<{
|
|
25
|
+
title?: string
|
|
26
|
+
subtitle?: string
|
|
27
|
+
/** Show a leading back button. `true` uses router.back(); pass `{ to }` to navigate. */
|
|
28
|
+
back?: boolean | { to: RouteLocationRaw }
|
|
29
|
+
/** Show skeleton title while loading. */
|
|
30
|
+
loading?: boolean
|
|
31
|
+
/**
|
|
32
|
+
* `default` = full-width top-of-page header with padding.
|
|
33
|
+
* `compact` = sits inside a detail panel / sidebar; smaller title + thinner divider.
|
|
34
|
+
* `flush` = no padding/border (caller wraps it).
|
|
35
|
+
*/
|
|
36
|
+
density?: 'default' | 'compact' | 'flush'
|
|
37
|
+
/** When false, the default utility cluster is hidden. */
|
|
38
|
+
utility?: boolean
|
|
39
|
+
/** Optional grouped overflow items. Alternative to using the #overflow slot. */
|
|
40
|
+
overflowItems?: DropdownMenuItem[][]
|
|
41
|
+
}>(), {
|
|
42
|
+
back: false,
|
|
43
|
+
loading: false,
|
|
44
|
+
density: 'default',
|
|
45
|
+
utility: true,
|
|
46
|
+
overflowItems: () => [],
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const { t } = useI18n()
|
|
50
|
+
const router = useRouter()
|
|
51
|
+
|
|
52
|
+
function onBack() {
|
|
53
|
+
if (typeof props.back === 'object' && props.back && 'to' in props.back) {
|
|
54
|
+
router.push(props.back.to)
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
router.back()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const wrapperClass = computed(() => {
|
|
61
|
+
switch (props.density) {
|
|
62
|
+
case 'compact':
|
|
63
|
+
return 'flex items-center gap-2 flex-wrap border-b border-default px-6 py-4'
|
|
64
|
+
case 'flush':
|
|
65
|
+
return 'flex items-center gap-2 flex-wrap'
|
|
66
|
+
case 'default':
|
|
67
|
+
default:
|
|
68
|
+
return 'flex items-center gap-2 flex-wrap'
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const titleClass = computed(() => props.density === 'compact' ? 'font-display text-lg font-semibold tracking-tight' : 'font-display text-2xl font-semibold tracking-tight')
|
|
73
|
+
</script>
|
|
74
|
+
|
|
75
|
+
<template>
|
|
76
|
+
<div :class="wrapperClass">
|
|
77
|
+
<UButton
|
|
78
|
+
v-if="back"
|
|
79
|
+
icon="i-lucide-arrow-left"
|
|
80
|
+
color="neutral"
|
|
81
|
+
variant="ghost"
|
|
82
|
+
size="sm"
|
|
83
|
+
:aria-label="t('pageChrome.back')"
|
|
84
|
+
@click="onBack"
|
|
85
|
+
/>
|
|
86
|
+
|
|
87
|
+
<div class="min-w-0">
|
|
88
|
+
<slot name="title">
|
|
89
|
+
<USkeleton v-if="loading" class="h-7 w-40" />
|
|
90
|
+
<h1 v-else-if="title" :class="titleClass">{{ title }}</h1>
|
|
91
|
+
</slot>
|
|
92
|
+
<p v-if="subtitle && !loading" class="text-sm text-muted mt-1">{{ subtitle }}</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
<div v-if="$slots.badges" class="flex items-center gap-2">
|
|
96
|
+
<slot name="badges" />
|
|
97
|
+
</div>
|
|
98
|
+
|
|
99
|
+
<div class="ml-auto flex items-center gap-1">
|
|
100
|
+
<slot name="actions" />
|
|
101
|
+
<PageActionMenu
|
|
102
|
+
v-if="overflowItems.length"
|
|
103
|
+
:items="overflowItems"
|
|
104
|
+
/>
|
|
105
|
+
<slot name="utility">
|
|
106
|
+
<PageUtilityActions v-if="utility" />
|
|
107
|
+
</slot>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shipped as source: the consumer's Vite compiles this SFC. All imports are
|
|
3
|
+
// explicit — no reliance on the consumer's auto-import configuration.
|
|
4
|
+
// i18n key contract: pageChrome.help.
|
|
5
|
+
// Utility triggers stay LABELED buttons (not icon-only) by convention.
|
|
6
|
+
import { computed, inject } from 'vue'
|
|
7
|
+
import { useI18n } from 'vue-i18n'
|
|
8
|
+
import USeparator from '@nuxt/ui/components/Separator.vue'
|
|
9
|
+
import UButton from '@nuxt/ui/components/Button.vue'
|
|
10
|
+
import { HELP_PANEL_KEY } from '@octabits-io/nuxt-ui-kit'
|
|
11
|
+
|
|
12
|
+
const { t } = useI18n()
|
|
13
|
+
const helpPanel = inject(HELP_PANEL_KEY, null)
|
|
14
|
+
|
|
15
|
+
const showHelp = computed(() => Boolean(helpPanel?.hasActions.value))
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<template>
|
|
19
|
+
<div v-if="showHelp" class="flex items-center gap-1">
|
|
20
|
+
<USeparator orientation="vertical" class="h-5 mx-1" />
|
|
21
|
+
<UButton
|
|
22
|
+
v-if="helpPanel"
|
|
23
|
+
icon="i-lucide-circle-help"
|
|
24
|
+
:label="t('pageChrome.help')"
|
|
25
|
+
size="sm"
|
|
26
|
+
color="neutral"
|
|
27
|
+
:variant="helpPanel.isOpen.value ? 'soft' : 'ghost'"
|
|
28
|
+
@click="helpPanel.toggle()"
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
</template>
|
|
@@ -28,6 +28,15 @@ const route = useRoute()
|
|
|
28
28
|
|
|
29
29
|
const buttonLabel = computed(() => props.toggleLabel ?? props.title)
|
|
30
30
|
|
|
31
|
+
// A named handler rather than an inline `@click="open = true"`: Vue compiles an
|
|
32
|
+
// inline assignment to `$event => (open = true)`, whose return type is
|
|
33
|
+
// `boolean`. UButton types `onClick` as `(event) => void | Promise<void>` — a
|
|
34
|
+
// *union*, so TS's "a function returning a value is assignable to a void-returning
|
|
35
|
+
// signature" rule does not apply and the assignment errors (TS2322).
|
|
36
|
+
function openSidebar() {
|
|
37
|
+
open.value = true
|
|
38
|
+
}
|
|
39
|
+
|
|
31
40
|
watch(
|
|
32
41
|
() => [route.path, route.query[props.selectionQueryKey]],
|
|
33
42
|
() => { open.value = false },
|
|
@@ -73,7 +82,7 @@ watch(
|
|
|
73
82
|
color="neutral"
|
|
74
83
|
variant="outline"
|
|
75
84
|
size="sm"
|
|
76
|
-
@click="
|
|
85
|
+
@click="openSidebar"
|
|
77
86
|
/>
|
|
78
87
|
</div>
|
|
79
88
|
<div class="min-w-0 flex-1 overflow-y-auto">
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Shipped as source: the consumer's Vite compiles this SFC. All imports are
|
|
3
|
+
// explicit — no reliance on the consumer's auto-import configuration.
|
|
4
|
+
// i18n key contract: localeField.translationStatus.complete / .missing.
|
|
5
|
+
// Requires provideLocaleFieldContext() near the app root.
|
|
6
|
+
import { computed, toValue } from 'vue'
|
|
7
|
+
import { useI18n } from 'vue-i18n'
|
|
8
|
+
import UTooltip from '@nuxt/ui/components/Tooltip.vue'
|
|
9
|
+
import UBadge from '@nuxt/ui/components/Badge.vue'
|
|
10
|
+
import UIcon from '@nuxt/ui/components/Icon.vue'
|
|
11
|
+
import {
|
|
12
|
+
useLocaleFieldContext,
|
|
13
|
+
type TranslationStatus,
|
|
14
|
+
} from '@octabits-io/nuxt-ui-kit/locale'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Translation-completeness badge for list rows and detail headers.
|
|
18
|
+
*
|
|
19
|
+
* - Hidden for single-locale content (nothing to translate into).
|
|
20
|
+
* - Green check when every in-use translatable leaf covers all supported
|
|
21
|
+
* locales (register variants inherit their base).
|
|
22
|
+
* - Orange with the total missing-leaf count otherwise; the tooltip breaks
|
|
23
|
+
* the count down per locale.
|
|
24
|
+
*/
|
|
25
|
+
const props = defineProps<{
|
|
26
|
+
status: TranslationStatus | undefined
|
|
27
|
+
}>()
|
|
28
|
+
|
|
29
|
+
const { t } = useI18n()
|
|
30
|
+
const { locales } = useLocaleFieldContext().useSource()
|
|
31
|
+
|
|
32
|
+
const visible = computed(() => !!props.status && toValue(locales).length > 1)
|
|
33
|
+
|
|
34
|
+
const totalMissing = computed(() =>
|
|
35
|
+
Object.values(props.status?.missing ?? {}).reduce((sum, n) => sum + n, 0),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const tooltip = computed(() => {
|
|
39
|
+
if (!props.status) return ''
|
|
40
|
+
if (props.status.complete) return t('localeField.translationStatus.complete')
|
|
41
|
+
const perLocale = Object.entries(props.status.missing)
|
|
42
|
+
.map(([locale, n]) => `${locale.toUpperCase()}: ${n}`)
|
|
43
|
+
.join(', ')
|
|
44
|
+
return t('localeField.translationStatus.missing', { details: perLocale })
|
|
45
|
+
})
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<template>
|
|
49
|
+
<UTooltip v-if="visible" :text="tooltip">
|
|
50
|
+
<UBadge
|
|
51
|
+
v-if="!status!.complete"
|
|
52
|
+
color="warning"
|
|
53
|
+
variant="subtle"
|
|
54
|
+
size="xs"
|
|
55
|
+
icon="i-lucide-languages"
|
|
56
|
+
:label="String(totalMissing)"
|
|
57
|
+
/>
|
|
58
|
+
<UIcon
|
|
59
|
+
v-else
|
|
60
|
+
name="i-lucide-check-circle-2"
|
|
61
|
+
class="size-4 text-success"
|
|
62
|
+
:aria-label="t('localeField.translationStatus.complete')"
|
|
63
|
+
/>
|
|
64
|
+
</UTooltip>
|
|
65
|
+
</template>
|