@octabits-io/nuxt-ui-kit 0.2.1 → 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 -297
- package/dist/index.js +54 -321
- 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/TranslationBadge.vue +65 -0
package/dist/zod/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Ref } from "vue";
|
|
1
2
|
import * as z from "zod";
|
|
2
3
|
//#region src/zod/index.d.ts
|
|
3
4
|
type ZodLocaleFactory = () => Parameters<typeof z.config>[0];
|
|
@@ -20,5 +21,26 @@ interface ZodLocaleSyncOptions {
|
|
|
20
21
|
* change. Call once from an app plugin.
|
|
21
22
|
*/
|
|
22
23
|
declare function setupZodLocaleSync(options: ZodLocaleSyncOptions): void;
|
|
24
|
+
interface UseWizardStepValidationOptions<TState> {
|
|
25
|
+
form: Ref<any>;
|
|
26
|
+
stepper: Ref<any>;
|
|
27
|
+
activeStep: Ref<number>;
|
|
28
|
+
state: TState;
|
|
29
|
+
schema: z.ZodObject<z.ZodRawShape> | Ref<z.ZodObject<z.ZodRawShape>>;
|
|
30
|
+
stepFields: Partial<Record<number, readonly (keyof TState)[]>>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Gates a stepper + form multi-step wizard by validating only the current
|
|
34
|
+
* step's fields via `schema.pick(...)`: `currentStepValid` drives the Next
|
|
35
|
+
* button's enabled state reactively, `goNext` runs the form-level validation
|
|
36
|
+
* for the step's fields (surfacing messages) before advancing, `goPrev` just
|
|
37
|
+
* steps back. Works with any form/stepper exposing the structural `validate`
|
|
38
|
+
* / `next` / `prev` surface (e.g. Nuxt UI's UForm + UStepper).
|
|
39
|
+
*/
|
|
40
|
+
declare function useWizardStepValidation<TState extends object>(options: UseWizardStepValidationOptions<TState>): {
|
|
41
|
+
currentStepValid: import("vue").ComputedRef<boolean>;
|
|
42
|
+
goNext: () => Promise<void>;
|
|
43
|
+
goPrev: () => void;
|
|
44
|
+
};
|
|
23
45
|
//#endregion
|
|
24
|
-
export { ZodLocaleSyncOptions, setupZodLocaleSync };
|
|
46
|
+
export { UseWizardStepValidationOptions, ZodLocaleSyncOptions, setupZodLocaleSync, useWizardStepValidation };
|
package/dist/zod/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { computed, isRef } from "vue";
|
|
1
2
|
import * as z from "zod";
|
|
2
3
|
//#region src/zod/index.ts
|
|
3
4
|
/**
|
|
@@ -13,5 +14,47 @@ function setupZodLocaleSync(options) {
|
|
|
13
14
|
apply(options.getLocale());
|
|
14
15
|
options.onLocaleChange(apply);
|
|
15
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Gates a stepper + form multi-step wizard by validating only the current
|
|
19
|
+
* step's fields via `schema.pick(...)`: `currentStepValid` drives the Next
|
|
20
|
+
* button's enabled state reactively, `goNext` runs the form-level validation
|
|
21
|
+
* for the step's fields (surfacing messages) before advancing, `goPrev` just
|
|
22
|
+
* steps back. Works with any form/stepper exposing the structural `validate`
|
|
23
|
+
* / `next` / `prev` surface (e.g. Nuxt UI's UForm + UStepper).
|
|
24
|
+
*/
|
|
25
|
+
function useWizardStepValidation(options) {
|
|
26
|
+
const { form, stepper, activeStep, state, schema, stepFields } = options;
|
|
27
|
+
function getSchema() {
|
|
28
|
+
return isRef(schema) ? schema.value : schema;
|
|
29
|
+
}
|
|
30
|
+
const currentStepValid = computed(() => {
|
|
31
|
+
const fields = stepFields[activeStep.value];
|
|
32
|
+
if (!fields || fields.length === 0) return true;
|
|
33
|
+
const partial = {};
|
|
34
|
+
const pickShape = {};
|
|
35
|
+
for (const f of fields) {
|
|
36
|
+
partial[f] = state[f];
|
|
37
|
+
pickShape[f] = true;
|
|
38
|
+
}
|
|
39
|
+
return getSchema().pick(pickShape).safeParse(partial).success;
|
|
40
|
+
});
|
|
41
|
+
async function goNext() {
|
|
42
|
+
const fields = stepFields[activeStep.value];
|
|
43
|
+
if (fields && fields.length > 0) try {
|
|
44
|
+
await form.value?.validate({ name: fields.map(String) });
|
|
45
|
+
} catch {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
stepper.value?.next();
|
|
49
|
+
}
|
|
50
|
+
function goPrev() {
|
|
51
|
+
stepper.value?.prev();
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
currentStepValid,
|
|
55
|
+
goNext,
|
|
56
|
+
goPrev
|
|
57
|
+
};
|
|
58
|
+
}
|
|
16
59
|
//#endregion
|
|
17
|
-
export { setupZodLocaleSync };
|
|
60
|
+
export { setupZodLocaleSync, useWizardStepValidation };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octabits-io/nuxt-ui-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Frontend kit for Nuxt/Vue admin SPAs: OIDC session harness (oidc-client-ts), Eden Treaty client factory, auth/org store cores, and a route-guard builder — factory-style seams the app wires into its own plugins, stores, and middleware",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,26 @@
|
|
|
10
10
|
"import": "./dist/index.js",
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./auth": {
|
|
14
|
+
"types": "./dist/auth/index.d.ts",
|
|
15
|
+
"import": "./dist/auth/index.js",
|
|
16
|
+
"default": "./dist/auth/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./api": {
|
|
19
|
+
"types": "./dist/api/index.d.ts",
|
|
20
|
+
"import": "./dist/api/index.js",
|
|
21
|
+
"default": "./dist/api/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./i18n": {
|
|
24
|
+
"types": "./dist/i18n/index.d.ts",
|
|
25
|
+
"import": "./dist/i18n/index.js",
|
|
26
|
+
"default": "./dist/i18n/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./locale": {
|
|
29
|
+
"types": "./dist/locale/index.d.ts",
|
|
30
|
+
"import": "./dist/locale/index.js",
|
|
31
|
+
"default": "./dist/locale/index.js"
|
|
32
|
+
},
|
|
13
33
|
"./zod": {
|
|
14
34
|
"types": "./dist/zod/index.d.ts",
|
|
15
35
|
"import": "./dist/zod/index.js",
|
|
@@ -52,10 +72,12 @@
|
|
|
52
72
|
"oidc-client-ts": "^3.5.0",
|
|
53
73
|
"vitest": "^4.1.10",
|
|
54
74
|
"vue": "^3.5.39",
|
|
55
|
-
"zod": "^4.4.3"
|
|
75
|
+
"zod": "^4.4.3",
|
|
76
|
+
"@octabits-io/framework": "^0.3.0"
|
|
56
77
|
},
|
|
57
78
|
"peerDependencies": {
|
|
58
79
|
"@elysiajs/eden": "^1.4.0",
|
|
80
|
+
"@octabits-io/framework": "^0.3.0",
|
|
59
81
|
"@internationalized/date": "^3",
|
|
60
82
|
"@nuxt/ui": "^4",
|
|
61
83
|
"date-fns": "^3 || ^4",
|
|
@@ -68,6 +90,12 @@
|
|
|
68
90
|
"zod": "^4"
|
|
69
91
|
},
|
|
70
92
|
"peerDependenciesMeta": {
|
|
93
|
+
"@elysiajs/eden": {
|
|
94
|
+
"optional": true
|
|
95
|
+
},
|
|
96
|
+
"@octabits-io/framework": {
|
|
97
|
+
"optional": true
|
|
98
|
+
},
|
|
71
99
|
"@internationalized/date": {
|
|
72
100
|
"optional": true
|
|
73
101
|
},
|
|
@@ -80,6 +108,9 @@
|
|
|
80
108
|
"elysia": {
|
|
81
109
|
"optional": true
|
|
82
110
|
},
|
|
111
|
+
"oidc-client-ts": {
|
|
112
|
+
"optional": true
|
|
113
|
+
},
|
|
83
114
|
"typescript": {
|
|
84
115
|
"optional": true
|
|
85
116
|
},
|
|
@@ -0,0 +1,132 @@
|
|
|
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.translate (+ LocaleTab's key).
|
|
5
|
+
// Requires provideLocaleFieldContext() near the app root.
|
|
6
|
+
import { computed } from 'vue'
|
|
7
|
+
import { useI18n } from 'vue-i18n'
|
|
8
|
+
import type { LocaleMap } from '@octabits-io/framework/utils'
|
|
9
|
+
import UFormField from '@nuxt/ui/components/FormField.vue'
|
|
10
|
+
import UTabs from '@nuxt/ui/components/Tabs.vue'
|
|
11
|
+
import UInput from '@nuxt/ui/components/Input.vue'
|
|
12
|
+
import UTooltip from '@nuxt/ui/components/Tooltip.vue'
|
|
13
|
+
import UButton from '@nuxt/ui/components/Button.vue'
|
|
14
|
+
import {
|
|
15
|
+
useLocaleField,
|
|
16
|
+
useLocaleFieldContext,
|
|
17
|
+
type LocaleFieldTranslateScope,
|
|
18
|
+
} from '@octabits-io/nuxt-ui-kit/locale'
|
|
19
|
+
import LocaleTab from './LocaleTab.vue'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Edits a `LocaleMap<string>` field (one value per content locale) as a single
|
|
23
|
+
* input with a per-locale tab bar. The tab for a locale missing a value shows a
|
|
24
|
+
* dot — orange for a normal locale, red for the default locale (which should
|
|
25
|
+
* always be filled). The full map is the single source of truth, so switching
|
|
26
|
+
* tabs never loses an in-progress edit.
|
|
27
|
+
*
|
|
28
|
+
* Register-variant locales (e.g. `de-formal`) are hidden by default — a label is
|
|
29
|
+
* register-invariant ("Hotel" is the same formal or informal). Set
|
|
30
|
+
* `register-override` for reader-addressing prose where formal/informal differs;
|
|
31
|
+
* see `LocaleTextarea`, which is the usual home for that.
|
|
32
|
+
*/
|
|
33
|
+
const model = defineModel<LocaleMap<string>>({ default: () => ({}) })
|
|
34
|
+
|
|
35
|
+
const props = defineProps<{
|
|
36
|
+
label?: string
|
|
37
|
+
description?: string
|
|
38
|
+
help?: string
|
|
39
|
+
placeholder?: string
|
|
40
|
+
/** Form field path — wires validation errors to this field. */
|
|
41
|
+
name?: string
|
|
42
|
+
/** Marks the default-locale value as required (visual only). */
|
|
43
|
+
required?: boolean
|
|
44
|
+
maxlength?: number
|
|
45
|
+
/** Surface register-variant locales (e.g. `de-formal`) as optional override tabs. */
|
|
46
|
+
registerOverride?: boolean
|
|
47
|
+
/** AI-translate context describing what the field holds. Defaults to `label`. */
|
|
48
|
+
translateContext?: string
|
|
49
|
+
/** Hide the AI-translate button (slugs, codes — values that must not be translated). */
|
|
50
|
+
noTranslate?: boolean
|
|
51
|
+
}>()
|
|
52
|
+
|
|
53
|
+
defineSlots<{
|
|
54
|
+
/**
|
|
55
|
+
* Replaces the default AI-translate button with custom field-level AI
|
|
56
|
+
* actions (e.g. a menu merging generate + translate). The scope carries the
|
|
57
|
+
* field's translate machinery so the override can still offer it.
|
|
58
|
+
*/
|
|
59
|
+
ai?: (scope: LocaleFieldTranslateScope) => unknown
|
|
60
|
+
}>()
|
|
61
|
+
|
|
62
|
+
const { t } = useI18n()
|
|
63
|
+
const { useSource, useTranslate } = useLocaleFieldContext()
|
|
64
|
+
|
|
65
|
+
const { items, active, activeValue, indicatorOf, translateSource, translateTargets } = useLocaleField(
|
|
66
|
+
model,
|
|
67
|
+
useSource(),
|
|
68
|
+
() => props.registerOverride ?? false,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// The translate provider is optional app wiring — without it the sparkle
|
|
72
|
+
// button never renders and the #ai slot scope reports unavailable.
|
|
73
|
+
const translator = useTranslate?.({
|
|
74
|
+
model,
|
|
75
|
+
context: () => props.translateContext ?? props.label,
|
|
76
|
+
source: translateSource,
|
|
77
|
+
targetLocales: translateTargets,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const aiScope = computed<LocaleFieldTranslateScope>(() => ({
|
|
81
|
+
available: !!translator && items.value.length > 1 && !props.noTranslate,
|
|
82
|
+
canTranslate: translator?.canTranslate.value ?? false,
|
|
83
|
+
translating: translator?.translating.value ?? false,
|
|
84
|
+
translate: () => translator?.translate(),
|
|
85
|
+
}))
|
|
86
|
+
</script>
|
|
87
|
+
|
|
88
|
+
<template>
|
|
89
|
+
<UFormField :label="label" :description="description" :help="help" :name="name" :required="required">
|
|
90
|
+
<div class="flex flex-col gap-2">
|
|
91
|
+
<!-- A single effective locale needs no tab chrome — degrade to a plain input
|
|
92
|
+
(the row still renders when a page slots in field-level AI actions). -->
|
|
93
|
+
<div v-if="items.length > 1 || !!$slots.ai" class="flex items-center justify-between gap-2">
|
|
94
|
+
<UTabs
|
|
95
|
+
v-if="items.length > 1"
|
|
96
|
+
v-model="active"
|
|
97
|
+
:items="items"
|
|
98
|
+
:content="false"
|
|
99
|
+
size="sm"
|
|
100
|
+
color="neutral"
|
|
101
|
+
variant="link"
|
|
102
|
+
:ui="{ list: 'gap-2' }"
|
|
103
|
+
>
|
|
104
|
+
<template #default="{ item }">
|
|
105
|
+
<LocaleTab :label="item.label as string" :indicator="indicatorOf(item.value as string)" />
|
|
106
|
+
</template>
|
|
107
|
+
</UTabs>
|
|
108
|
+
<span v-else />
|
|
109
|
+
<slot name="ai" v-bind="aiScope">
|
|
110
|
+
<UTooltip v-if="aiScope.available" :text="t('localeField.translate')">
|
|
111
|
+
<UButton
|
|
112
|
+
icon="i-lucide-languages"
|
|
113
|
+
size="xs"
|
|
114
|
+
variant="ghost"
|
|
115
|
+
color="primary"
|
|
116
|
+
:loading="aiScope.translating"
|
|
117
|
+
:disabled="!aiScope.canTranslate"
|
|
118
|
+
:aria-label="t('localeField.translate')"
|
|
119
|
+
@click.prevent="aiScope.translate()"
|
|
120
|
+
/>
|
|
121
|
+
</UTooltip>
|
|
122
|
+
</slot>
|
|
123
|
+
</div>
|
|
124
|
+
<UInput
|
|
125
|
+
v-model="activeValue"
|
|
126
|
+
:placeholder="placeholder"
|
|
127
|
+
:maxlength="maxlength"
|
|
128
|
+
class="w-full"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</UFormField>
|
|
132
|
+
</template>
|
|
@@ -0,0 +1,44 @@
|
|
|
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.inheritsBaseLocale.
|
|
5
|
+
import { computed } from 'vue'
|
|
6
|
+
import { useI18n } from 'vue-i18n'
|
|
7
|
+
import type { LocaleTabIndicator } from '@octabits-io/nuxt-ui-kit/locale'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Renders a locale tab label with its completeness indicator:
|
|
11
|
+
* red dot = default locale empty, orange = a normal locale empty,
|
|
12
|
+
* neutral dot = a register-variant override empty (inherits its base locale).
|
|
13
|
+
*/
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
label: string
|
|
16
|
+
indicator: LocaleTabIndicator
|
|
17
|
+
}>()
|
|
18
|
+
|
|
19
|
+
const { t } = useI18n()
|
|
20
|
+
|
|
21
|
+
const dotClass = computed(() => {
|
|
22
|
+
switch (props.indicator?.kind) {
|
|
23
|
+
case 'error':
|
|
24
|
+
return 'bg-error'
|
|
25
|
+
case 'warning':
|
|
26
|
+
return 'bg-warning'
|
|
27
|
+
case 'inherits':
|
|
28
|
+
return 'bg-muted'
|
|
29
|
+
default:
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const title = computed(() =>
|
|
35
|
+
props.indicator?.kind === 'inherits' ? t('localeField.inheritsBaseLocale') : undefined,
|
|
36
|
+
)
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<template>
|
|
40
|
+
<span class="flex items-center gap-1.5" :title="title">
|
|
41
|
+
{{ label }}
|
|
42
|
+
<span v-if="dotClass" class="size-1.5 shrink-0 rounded-full" :class="dotClass" />
|
|
43
|
+
</span>
|
|
44
|
+
</template>
|
|
@@ -0,0 +1,131 @@
|
|
|
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.translate (+ LocaleTab's key).
|
|
5
|
+
// Requires provideLocaleFieldContext() near the app root.
|
|
6
|
+
import { computed } from 'vue'
|
|
7
|
+
import { useI18n } from 'vue-i18n'
|
|
8
|
+
import type { LocaleMap } from '@octabits-io/framework/utils'
|
|
9
|
+
import UFormField from '@nuxt/ui/components/FormField.vue'
|
|
10
|
+
import UTabs from '@nuxt/ui/components/Tabs.vue'
|
|
11
|
+
import UTextarea from '@nuxt/ui/components/Textarea.vue'
|
|
12
|
+
import UTooltip from '@nuxt/ui/components/Tooltip.vue'
|
|
13
|
+
import UButton from '@nuxt/ui/components/Button.vue'
|
|
14
|
+
import {
|
|
15
|
+
useLocaleField,
|
|
16
|
+
useLocaleFieldContext,
|
|
17
|
+
type LocaleFieldTranslateScope,
|
|
18
|
+
} from '@octabits-io/nuxt-ui-kit/locale'
|
|
19
|
+
import LocaleTab from './LocaleTab.vue'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Multi-line variant of `LocaleInput` — edits a `LocaleMap<string>` field
|
|
23
|
+
* (descriptions, body copy, …) with a per-locale tab bar. See `LocaleInput`
|
|
24
|
+
* for the completeness-dot semantics.
|
|
25
|
+
*
|
|
26
|
+
* Reader-addressing prose is where formal/informal registers actually differ, so
|
|
27
|
+
* set `register-override` here to expose e.g. `de-formal` as an optional override
|
|
28
|
+
* tab (blank = inherits `de`). Leave it off for register-invariant copy.
|
|
29
|
+
*/
|
|
30
|
+
const model = defineModel<LocaleMap<string>>({ default: () => ({}) })
|
|
31
|
+
|
|
32
|
+
const props = defineProps<{
|
|
33
|
+
label?: string
|
|
34
|
+
description?: string
|
|
35
|
+
help?: string
|
|
36
|
+
placeholder?: string
|
|
37
|
+
/** Form field path — wires validation errors to this field. */
|
|
38
|
+
name?: string
|
|
39
|
+
required?: boolean
|
|
40
|
+
maxlength?: number
|
|
41
|
+
rows?: number
|
|
42
|
+
/** Surface register-variant locales (e.g. `de-formal`) as optional override tabs. */
|
|
43
|
+
registerOverride?: boolean
|
|
44
|
+
/** AI-translate context describing what the field holds. Defaults to `label`. */
|
|
45
|
+
translateContext?: string
|
|
46
|
+
/** Hide the AI-translate button (slugs, codes — values that must not be translated). */
|
|
47
|
+
noTranslate?: boolean
|
|
48
|
+
}>()
|
|
49
|
+
|
|
50
|
+
defineSlots<{
|
|
51
|
+
/**
|
|
52
|
+
* Replaces the default AI-translate button with custom field-level AI
|
|
53
|
+
* actions (e.g. a menu merging generate + translate). The scope carries the
|
|
54
|
+
* field's translate machinery so the override can still offer it.
|
|
55
|
+
*/
|
|
56
|
+
ai?: (scope: LocaleFieldTranslateScope) => unknown
|
|
57
|
+
}>()
|
|
58
|
+
|
|
59
|
+
const { t } = useI18n()
|
|
60
|
+
const { useSource, useTranslate } = useLocaleFieldContext()
|
|
61
|
+
|
|
62
|
+
const { items, active, activeValue, indicatorOf, translateSource, translateTargets } = useLocaleField(
|
|
63
|
+
model,
|
|
64
|
+
useSource(),
|
|
65
|
+
() => props.registerOverride ?? false,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
// The translate provider is optional app wiring — without it the sparkle
|
|
69
|
+
// button never renders and the #ai slot scope reports unavailable.
|
|
70
|
+
const translator = useTranslate?.({
|
|
71
|
+
model,
|
|
72
|
+
context: () => props.translateContext ?? props.label,
|
|
73
|
+
source: translateSource,
|
|
74
|
+
targetLocales: translateTargets,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const aiScope = computed<LocaleFieldTranslateScope>(() => ({
|
|
78
|
+
available: !!translator && items.value.length > 1 && !props.noTranslate,
|
|
79
|
+
canTranslate: translator?.canTranslate.value ?? false,
|
|
80
|
+
translating: translator?.translating.value ?? false,
|
|
81
|
+
translate: () => translator?.translate(),
|
|
82
|
+
}))
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<template>
|
|
86
|
+
<UFormField :label="label" :description="description" :help="help" :name="name" :required="required">
|
|
87
|
+
<div class="flex flex-col gap-2">
|
|
88
|
+
<!-- A single effective locale needs no tab chrome — degrade to a plain textarea
|
|
89
|
+
(the row still renders when a page slots in field-level AI actions). -->
|
|
90
|
+
<div v-if="items.length > 1 || !!$slots.ai" class="flex items-center justify-between gap-2">
|
|
91
|
+
<UTabs
|
|
92
|
+
v-if="items.length > 1"
|
|
93
|
+
v-model="active"
|
|
94
|
+
:items="items"
|
|
95
|
+
:content="false"
|
|
96
|
+
size="sm"
|
|
97
|
+
color="neutral"
|
|
98
|
+
variant="link"
|
|
99
|
+
:ui="{ list: 'gap-2' }"
|
|
100
|
+
>
|
|
101
|
+
<template #default="{ item }">
|
|
102
|
+
<LocaleTab :label="item.label as string" :indicator="indicatorOf(item.value as string)" />
|
|
103
|
+
</template>
|
|
104
|
+
</UTabs>
|
|
105
|
+
<span v-else />
|
|
106
|
+
<slot name="ai" v-bind="aiScope">
|
|
107
|
+
<UTooltip v-if="aiScope.available" :text="t('localeField.translate')">
|
|
108
|
+
<UButton
|
|
109
|
+
icon="i-lucide-languages"
|
|
110
|
+
size="xs"
|
|
111
|
+
variant="ghost"
|
|
112
|
+
color="primary"
|
|
113
|
+
:loading="aiScope.translating"
|
|
114
|
+
:disabled="!aiScope.canTranslate"
|
|
115
|
+
:aria-label="t('localeField.translate')"
|
|
116
|
+
@click.prevent="aiScope.translate()"
|
|
117
|
+
/>
|
|
118
|
+
</UTooltip>
|
|
119
|
+
</slot>
|
|
120
|
+
</div>
|
|
121
|
+
<UTextarea
|
|
122
|
+
v-model="activeValue"
|
|
123
|
+
:placeholder="placeholder"
|
|
124
|
+
:maxlength="maxlength"
|
|
125
|
+
:rows="rows ?? 4"
|
|
126
|
+
autoresize
|
|
127
|
+
class="w-full"
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
</UFormField>
|
|
131
|
+
</template>
|
|
@@ -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>
|