@venix-sistemas/jade 1.0.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/LICENSE +21 -0
- package/README.md +481 -0
- package/dist/module.d.mts +26 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +249 -0
- package/dist/runtime/components/VenixIcon.d.vue.ts +14 -0
- package/dist/runtime/components/VenixIcon.vue +37 -0
- package/dist/runtime/components/VenixIcon.vue.d.ts +14 -0
- package/dist/runtime/components/VenixThemeSwitcher.d.vue.ts +9 -0
- package/dist/runtime/components/VenixThemeSwitcher.vue +191 -0
- package/dist/runtime/components/VenixThemeSwitcher.vue.d.ts +9 -0
- package/dist/runtime/composables/useThemeColors.d.ts +10 -0
- package/dist/runtime/composables/useThemeColors.js +25 -0
- package/dist/runtime/composables/useThemeCookies.d.ts +7 -0
- package/dist/runtime/composables/useThemeCookies.js +33 -0
- package/dist/runtime/composables/useThemeLocale.d.ts +12 -0
- package/dist/runtime/composables/useThemeLocale.js +87 -0
- package/dist/runtime/composables/useThemeSeasonal.d.ts +4 -0
- package/dist/runtime/composables/useThemeSeasonal.js +7 -0
- package/dist/runtime/composables/useVenixIcon.d.ts +3 -0
- package/dist/runtime/composables/useVenixIcon.js +11 -0
- package/dist/runtime/composables/useVenixTheme.d.ts +33 -0
- package/dist/runtime/composables/useVenixTheme.js +186 -0
- package/dist/runtime/nitro/theme-init.d.ts +2 -0
- package/dist/runtime/nitro/theme-init.js +11 -0
- package/dist/runtime/plugin.d.ts +2 -0
- package/dist/runtime/plugin.js +3 -0
- package/dist/runtime/plugins/theme-init.server.d.ts +2 -0
- package/dist/runtime/plugins/theme-init.server.js +37 -0
- package/dist/runtime/plugins/vuetify-theme.d.ts +2 -0
- package/dist/runtime/plugins/vuetify-theme.js +42 -0
- package/dist/runtime/public/fonts/Iceberg/Iceberg-Regular.woff2 +0 -0
- package/dist/runtime/public/images/ui/cursor/cursor-hand.png +0 -0
- package/dist/runtime/public/images/ui/cursor/cursor-text.png +0 -0
- package/dist/runtime/public/images/ui/cursor/cursor.png +0 -0
- package/dist/runtime/scripts/theme-init.d.ts +27 -0
- package/dist/runtime/scripts/theme-init.js +89 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/shared/constants.d.ts +28 -0
- package/dist/shared/constants.js +40 -0
- package/dist/shared/css/colors.d.ts +2 -0
- package/dist/shared/css/colors.js +19 -0
- package/dist/shared/css/cursor.d.ts +2 -0
- package/dist/shared/css/cursor.js +74 -0
- package/dist/shared/css/index.d.ts +8 -0
- package/dist/shared/css/index.js +31 -0
- package/dist/shared/css/scrollbar.d.ts +2 -0
- package/dist/shared/css/scrollbar.js +51 -0
- package/dist/shared/css/transition.d.ts +2 -0
- package/dist/shared/css/transition.js +20 -0
- package/dist/shared/css/typography.d.ts +3 -0
- package/dist/shared/css/typography.js +62 -0
- package/dist/shared/theme.json +193 -0
- package/dist/shared/types/color-options.d.ts +14 -0
- package/dist/shared/types/color-options.js +0 -0
- package/dist/shared/types/colors.d.ts +38 -0
- package/dist/shared/types/colors.js +0 -0
- package/dist/shared/types/cursor.d.ts +7 -0
- package/dist/shared/types/cursor.js +0 -0
- package/dist/shared/types/icon.d.ts +7 -0
- package/dist/shared/types/icon.js +0 -0
- package/dist/shared/types/index.d.ts +10 -0
- package/dist/shared/types/index.js +1 -0
- package/dist/shared/types/runtime-config.d.ts +33 -0
- package/dist/shared/types/runtime-config.js +1 -0
- package/dist/shared/types/scrollbar.d.ts +12 -0
- package/dist/shared/types/scrollbar.js +0 -0
- package/dist/shared/types/theme.d.ts +11 -0
- package/dist/shared/types/theme.js +0 -0
- package/dist/shared/types/translation.d.ts +16 -0
- package/dist/shared/types/translation.js +0 -0
- package/dist/shared/types/typography.d.ts +15 -0
- package/dist/shared/types/typography.js +0 -0
- package/dist/shared/types/vuetify.d.ts +3 -0
- package/dist/shared/types/vuetify.js +0 -0
- package/dist/shared/unocss-preset.d.ts +32 -0
- package/dist/shared/unocss-preset.js +13 -0
- package/dist/shared/utils/consent.d.ts +8 -0
- package/dist/shared/utils/consent.js +12 -0
- package/dist/shared/utils/icon-refs.d.ts +10 -0
- package/dist/shared/utils/icon-refs.js +24 -0
- package/dist/shared/utils/icon.d.ts +27 -0
- package/dist/shared/utils/icon.js +20 -0
- package/dist/shared/utils/index.d.ts +9 -0
- package/dist/shared/utils/index.js +12 -0
- package/dist/shared/utils/load.d.ts +4 -0
- package/dist/shared/utils/load.js +24 -0
- package/dist/shared/utils/normalize.d.ts +20 -0
- package/dist/shared/utils/normalize.js +38 -0
- package/dist/shared/utils/options.d.ts +10 -0
- package/dist/shared/utils/options.js +12 -0
- package/dist/shared/utils/theme-config.d.ts +14 -0
- package/dist/shared/utils/theme-config.js +22 -0
- package/dist/shared/utils/theme-resolve.d.ts +24 -0
- package/dist/shared/utils/theme-resolve.js +45 -0
- package/dist/types.d.mts +3 -0
- package/package.json +100 -0
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { addComponent, addPlugin, addServerPlugin, addTemplate, installModule, addImports, defineNuxtModule, createResolver } from '@nuxt/kit';
|
|
2
|
+
import { loadTheme } from '../dist/shared/utils/load.js';
|
|
3
|
+
import { NON_COLOR_PROPERTIES, DEFAULT_LOCALE_COOKIE_NAME, DEFAULT_LOCALE } from '../dist/shared/constants.js';
|
|
4
|
+
import { resolveFeatureOption } from '../dist/shared/utils/options.js';
|
|
5
|
+
import { processTheme } from '../dist/shared/css/index.js';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
7
|
+
import { realpathSync } from 'node:fs';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { getIcons } from '@iconify/utils';
|
|
11
|
+
import { extractIconRefs } from '../dist/shared/utils/index.js';
|
|
12
|
+
|
|
13
|
+
function customizeTheme(baseTheme, options) {
|
|
14
|
+
const customized = JSON.parse(JSON.stringify(baseTheme));
|
|
15
|
+
customized.typography = resolveFeatureOption(options.typography, customized.typography);
|
|
16
|
+
customized.customScrollbar = resolveFeatureOption(options.scrollbar, customized.customScrollbar);
|
|
17
|
+
customized.customCursor = resolveFeatureOption(options.cursor, customized.customCursor);
|
|
18
|
+
const colorOption = options.color;
|
|
19
|
+
if (colorOption === false) {
|
|
20
|
+
customized.colors.enabled = false;
|
|
21
|
+
} else if (typeof colorOption === "object" && colorOption !== null) {
|
|
22
|
+
if (colorOption.enabled === false) {
|
|
23
|
+
customized.colors.enabled = false;
|
|
24
|
+
}
|
|
25
|
+
if (colorOption.defaultColor) {
|
|
26
|
+
customized.colors.defaultColor = colorOption.defaultColor;
|
|
27
|
+
}
|
|
28
|
+
if (colorOption.themes) {
|
|
29
|
+
Object.entries(colorOption.themes).forEach(([themeName, customColors]) => {
|
|
30
|
+
if (customized.colors.themes[themeName]) {
|
|
31
|
+
customized.colors.themes[themeName] = {
|
|
32
|
+
...customized.colors.themes[themeName],
|
|
33
|
+
...customColors
|
|
34
|
+
};
|
|
35
|
+
} else {
|
|
36
|
+
customized.colors.themes[themeName] = {
|
|
37
|
+
dark: true,
|
|
38
|
+
...customColors
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return customized;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function registerPublicAssets(nuxt, resolver) {
|
|
48
|
+
nuxt.options.nitro = nuxt.options.nitro || {};
|
|
49
|
+
nuxt.options.nitro.publicAssets = nuxt.options.nitro.publicAssets || [];
|
|
50
|
+
nuxt.options.nitro.publicAssets.push({
|
|
51
|
+
dir: resolver.resolve("./runtime/public"),
|
|
52
|
+
maxAge: 60 * 60 * 24 * 365,
|
|
53
|
+
baseURL: "/"
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function registerThemeComponents(resolver) {
|
|
58
|
+
addComponent({
|
|
59
|
+
name: "VenixThemeSwitcher",
|
|
60
|
+
filePath: resolver.resolve("./runtime/components/VenixThemeSwitcher")
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function registerThemePlugins(_nuxt, resolver, shouldApplyColors) {
|
|
65
|
+
if (shouldApplyColors) {
|
|
66
|
+
addPlugin({
|
|
67
|
+
src: resolver.resolve("./runtime/plugins/theme-init.server"),
|
|
68
|
+
mode: "server"
|
|
69
|
+
});
|
|
70
|
+
addServerPlugin(resolver.resolve("./runtime/nitro/theme-init"));
|
|
71
|
+
}
|
|
72
|
+
addPlugin(resolver.resolve("./runtime/plugin"));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function registerThemeCSS(nuxt, theme) {
|
|
76
|
+
const css = processTheme(theme);
|
|
77
|
+
if (css) {
|
|
78
|
+
const template = addTemplate({
|
|
79
|
+
filename: "venix-theme.css",
|
|
80
|
+
getContents: () => css,
|
|
81
|
+
write: true
|
|
82
|
+
});
|
|
83
|
+
nuxt.options.css.push(template.dst);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildVuetifyThemes(colors) {
|
|
88
|
+
const themes = {};
|
|
89
|
+
for (const [name, themeColors] of Object.entries(colors.themes)) {
|
|
90
|
+
if (!themeColors.primary && !themeColors.background) continue;
|
|
91
|
+
const isDark = themeColors.dark === true;
|
|
92
|
+
const vuetifyColors = {};
|
|
93
|
+
for (const [key, value] of Object.entries(themeColors)) {
|
|
94
|
+
if (NON_COLOR_PROPERTIES.includes(key)) continue;
|
|
95
|
+
if (typeof value !== "string" || !value.startsWith("#")) continue;
|
|
96
|
+
vuetifyColors[key] = value;
|
|
97
|
+
}
|
|
98
|
+
if (themeColors.background2) vuetifyColors.surface = themeColors.background2;
|
|
99
|
+
vuetifyColors.inverse = isDark ? "#FFFFFF" : "#000000";
|
|
100
|
+
themes[name] = { dark: isDark, colors: vuetifyColors };
|
|
101
|
+
}
|
|
102
|
+
return themes;
|
|
103
|
+
}
|
|
104
|
+
function registerThemeVuetify(nuxt, resolver, theme, defaultColor, enabled) {
|
|
105
|
+
if (!enabled || theme.colors?.enabled === false) return;
|
|
106
|
+
const themes = buildVuetifyThemes(theme.colors);
|
|
107
|
+
if (Object.keys(themes).length === 0) return;
|
|
108
|
+
const hookRegisterModule = nuxt.hook;
|
|
109
|
+
hookRegisterModule("vuetify:registerModule", (register) => {
|
|
110
|
+
register({
|
|
111
|
+
vuetifyOptions: {
|
|
112
|
+
theme: {
|
|
113
|
+
defaultTheme: defaultColor,
|
|
114
|
+
themes
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
addPlugin(resolver.resolve("./runtime/plugins/vuetify-theme"));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const nodeRequire = createRequire(realpathSync(fileURLToPath(import.meta.url)));
|
|
123
|
+
function loadCollectionJson(name, projectRequire) {
|
|
124
|
+
try {
|
|
125
|
+
return nodeRequire(`@iconify-json/${name}/icons.json`);
|
|
126
|
+
} catch {
|
|
127
|
+
try {
|
|
128
|
+
return projectRequire?.(`@iconify-json/${name}/icons.json`) ?? null;
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function loadFullCollection(name, projectRequire) {
|
|
135
|
+
return loadCollectionJson(name, projectRequire) ?? name;
|
|
136
|
+
}
|
|
137
|
+
function loadCollectionSubset(name, iconNames, projectRequire) {
|
|
138
|
+
const json = loadCollectionJson(name, projectRequire);
|
|
139
|
+
if (!json) return null;
|
|
140
|
+
return getIcons(json, [...iconNames]);
|
|
141
|
+
}
|
|
142
|
+
async function registerThemeIcon(nuxt, resolver, options, colors) {
|
|
143
|
+
if (!options.enabled) return;
|
|
144
|
+
let projectRequire = null;
|
|
145
|
+
try {
|
|
146
|
+
projectRequire = createRequire(join(nuxt.options.rootDir, "package.json"));
|
|
147
|
+
} catch {
|
|
148
|
+
projectRequire = null;
|
|
149
|
+
}
|
|
150
|
+
const explicit = new Set(options.collections);
|
|
151
|
+
const collections = options.collections.map((name) => loadFullCollection(name, projectRequire));
|
|
152
|
+
const refs = extractIconRefs(colors.themes, options.aliases);
|
|
153
|
+
for (const [name, iconNames] of refs) {
|
|
154
|
+
if (explicit.has(name)) continue;
|
|
155
|
+
collections.push(loadCollectionSubset(name, iconNames, projectRequire) ?? name);
|
|
156
|
+
}
|
|
157
|
+
await installModule("@nuxt/icon", {
|
|
158
|
+
aliases: options.aliases,
|
|
159
|
+
serverBundle: {
|
|
160
|
+
collections
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
addImports({
|
|
164
|
+
name: "useVenixIcon",
|
|
165
|
+
from: resolver.resolve("./runtime/composables/useVenixIcon")
|
|
166
|
+
});
|
|
167
|
+
addComponent({
|
|
168
|
+
name: "VenixIcon",
|
|
169
|
+
filePath: resolver.resolve("./runtime/components/VenixIcon")
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const module$1 = defineNuxtModule({
|
|
174
|
+
meta: {
|
|
175
|
+
name: "@venix-sistemas/jade",
|
|
176
|
+
configKey: "venixTheme"
|
|
177
|
+
},
|
|
178
|
+
defaults: {
|
|
179
|
+
translation: true,
|
|
180
|
+
color: true,
|
|
181
|
+
scrollbar: true,
|
|
182
|
+
cursor: true,
|
|
183
|
+
typography: true,
|
|
184
|
+
vuetify: false,
|
|
185
|
+
icon: true
|
|
186
|
+
},
|
|
187
|
+
async setup(options, nuxt) {
|
|
188
|
+
const resolver = createResolver(import.meta.url);
|
|
189
|
+
registerPublicAssets(nuxt, resolver);
|
|
190
|
+
const baseTheme = loadTheme();
|
|
191
|
+
const theme = customizeTheme(baseTheme, options);
|
|
192
|
+
addImports({
|
|
193
|
+
name: "useVenixTheme",
|
|
194
|
+
from: resolver.resolve("./runtime/composables/useVenixTheme")
|
|
195
|
+
});
|
|
196
|
+
registerThemeComponents(resolver);
|
|
197
|
+
const colorOptions = resolveFeatureOption(options.color, {
|
|
198
|
+
enabled: theme.colors?.enabled !== false,
|
|
199
|
+
apply: true,
|
|
200
|
+
defaultColor: theme.colors?.defaultColor || "dark",
|
|
201
|
+
themes: {},
|
|
202
|
+
iconFormat: "svg"
|
|
203
|
+
});
|
|
204
|
+
const shouldApplyColors = colorOptions.enabled && colorOptions.apply && theme.colors?.defaults !== false;
|
|
205
|
+
const translationOptions = resolveFeatureOption(options.translation, {
|
|
206
|
+
enabled: true,
|
|
207
|
+
locale: "",
|
|
208
|
+
defaultLocale: DEFAULT_LOCALE,
|
|
209
|
+
cookieSync: DEFAULT_LOCALE_COOKIE_NAME,
|
|
210
|
+
manageHtmlLang: false
|
|
211
|
+
});
|
|
212
|
+
registerThemeCSS(nuxt, theme);
|
|
213
|
+
registerThemePlugins(nuxt, resolver, shouldApplyColors);
|
|
214
|
+
const vuetifyOptions = resolveFeatureOption(options.vuetify, {
|
|
215
|
+
enabled: true
|
|
216
|
+
});
|
|
217
|
+
registerThemeVuetify(nuxt, resolver, theme, colorOptions.defaultColor, vuetifyOptions.enabled);
|
|
218
|
+
const iconOptions = resolveFeatureOption(options.icon, {
|
|
219
|
+
enabled: true,
|
|
220
|
+
collections: ["line-md"],
|
|
221
|
+
aliases: {}
|
|
222
|
+
});
|
|
223
|
+
await registerThemeIcon(nuxt, resolver, iconOptions, theme.colors);
|
|
224
|
+
nuxt.options.runtimeConfig.public.venixTheme = {
|
|
225
|
+
defaultTheme: theme.colors?.defaultColor || "dark",
|
|
226
|
+
colorThemes: colorOptions.themes,
|
|
227
|
+
applyColors: shouldApplyColors,
|
|
228
|
+
localeCookie: translationOptions.cookieSync,
|
|
229
|
+
defaultLocale: translationOptions.defaultLocale,
|
|
230
|
+
locale: translationOptions.locale,
|
|
231
|
+
iconFormat: colorOptions.iconFormat,
|
|
232
|
+
icon: {
|
|
233
|
+
aliases: iconOptions.aliases
|
|
234
|
+
},
|
|
235
|
+
enabled: {
|
|
236
|
+
typography: theme.typography?.enabled !== false,
|
|
237
|
+
scrollbar: theme.customScrollbar?.enabled !== false,
|
|
238
|
+
cursor: theme.customCursor?.enabled !== false,
|
|
239
|
+
color: colorOptions.enabled,
|
|
240
|
+
translation: translationOptions.enabled,
|
|
241
|
+
manageHtmlLang: translationOptions.manageHtmlLang,
|
|
242
|
+
vuetify: vuetifyOptions.enabled,
|
|
243
|
+
icon: iconOptions.enabled
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
export { module$1 as default };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
/** An emoji ('🎨'), an Iconify icon name ('line-md:home') or inline SVG. */
|
|
3
|
+
icon: string;
|
|
4
|
+
/**
|
|
5
|
+
* Forces `@nuxt/icon`'s rendering mode for Iconify icons: `'svg'` (a real
|
|
6
|
+
* `<svg>` element — needed for animated icons like `line-md`) or `'css'`
|
|
7
|
+
* (background/mask, lighter, no animation). No effect for emoji or inline
|
|
8
|
+
* SVG. Without a value, uses the module's configured default mode.
|
|
9
|
+
*/
|
|
10
|
+
mode?: 'css' | 'svg';
|
|
11
|
+
};
|
|
12
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
declare const _default: typeof __VLS_export;
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
v-if="resolved.kind === 'emoji'"
|
|
4
|
+
class="venix-icon venix-icon--emoji"
|
|
5
|
+
v-bind="$attrs"
|
|
6
|
+
>{{ resolved.value }}</span>
|
|
7
|
+
<!-- eslint-disable vue/no-v-html -- SVG comes from the theme config/consumer dev's props, not end-user input -->
|
|
8
|
+
<span
|
|
9
|
+
v-else-if="resolved.kind === 'svg'"
|
|
10
|
+
class="venix-icon venix-icon--svg"
|
|
11
|
+
v-bind="$attrs"
|
|
12
|
+
v-html="resolved.value"
|
|
13
|
+
/>
|
|
14
|
+
<!-- eslint-enable vue/no-v-html -->
|
|
15
|
+
<Icon
|
|
16
|
+
v-else
|
|
17
|
+
class="venix-icon venix-icon--iconify"
|
|
18
|
+
:name="resolved.value"
|
|
19
|
+
:mode="mode"
|
|
20
|
+
v-bind="$attrs"
|
|
21
|
+
/>
|
|
22
|
+
</template>
|
|
23
|
+
|
|
24
|
+
<script setup>
|
|
25
|
+
import { computed } from "vue";
|
|
26
|
+
import { useVenixIcon } from "../composables/useVenixIcon";
|
|
27
|
+
defineOptions({ inheritAttrs: false });
|
|
28
|
+
const { icon, mode } = defineProps({
|
|
29
|
+
icon: { type: String, required: true },
|
|
30
|
+
mode: { type: String, required: false }
|
|
31
|
+
});
|
|
32
|
+
const resolved = useVenixIcon(computed(() => icon));
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<style>
|
|
36
|
+
.venix-icon--svg svg{height:1em;width:1em}
|
|
37
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
/** An emoji ('🎨'), an Iconify icon name ('line-md:home') or inline SVG. */
|
|
3
|
+
icon: string;
|
|
4
|
+
/**
|
|
5
|
+
* Forces `@nuxt/icon`'s rendering mode for Iconify icons: `'svg'` (a real
|
|
6
|
+
* `<svg>` element — needed for animated icons like `line-md`) or `'css'`
|
|
7
|
+
* (background/mask, lighter, no animation). No effect for emoji or inline
|
|
8
|
+
* SVG. Without a value, uses the module's configured default mode.
|
|
9
|
+
*/
|
|
10
|
+
mode?: 'css' | 'svg';
|
|
11
|
+
};
|
|
12
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
|
+
declare const _default: typeof __VLS_export;
|
|
14
|
+
export default _default;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
/** Accessible label for the button and the menu. Without a value, uses the component's own translation based on the current locale. */
|
|
3
|
+
label?: string;
|
|
4
|
+
/** Shows the active theme's name next to the icon on the button. */
|
|
5
|
+
showLabel?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
8
|
+
declare const _default: typeof __VLS_export;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
ref="rootRef"
|
|
4
|
+
class="venix-theme-switcher"
|
|
5
|
+
@focusout="handleFocusOut"
|
|
6
|
+
>
|
|
7
|
+
<span
|
|
8
|
+
class="venix-theme-switcher__sr-only"
|
|
9
|
+
role="status"
|
|
10
|
+
aria-live="polite"
|
|
11
|
+
>{{ announcement }}</span>
|
|
12
|
+
|
|
13
|
+
<button
|
|
14
|
+
ref="triggerRef"
|
|
15
|
+
type="button"
|
|
16
|
+
class="venix-theme-switcher__trigger"
|
|
17
|
+
:class="{ 'venix-theme-switcher__trigger--visible': visible }"
|
|
18
|
+
:aria-label="resolvedLabel"
|
|
19
|
+
:aria-expanded="open"
|
|
20
|
+
aria-haspopup="menu"
|
|
21
|
+
@click="toggleOpen"
|
|
22
|
+
>
|
|
23
|
+
<VenixIcon
|
|
24
|
+
v-if="activeIcon"
|
|
25
|
+
:key="activeIconKey"
|
|
26
|
+
:icon="activeIcon.value"
|
|
27
|
+
:mode="activeIcon.format === 'emote' ? void 0 : activeIcon.format"
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
/>
|
|
30
|
+
<span
|
|
31
|
+
v-if="showLabel"
|
|
32
|
+
class="venix-theme-switcher__trigger-label"
|
|
33
|
+
>{{ theme.data.value?.name }}</span>
|
|
34
|
+
</button>
|
|
35
|
+
|
|
36
|
+
<ul
|
|
37
|
+
v-show="open"
|
|
38
|
+
class="venix-theme-switcher__menu"
|
|
39
|
+
role="menu"
|
|
40
|
+
:aria-label="resolvedLabel"
|
|
41
|
+
@keydown="handleMenuKeydown"
|
|
42
|
+
>
|
|
43
|
+
<li
|
|
44
|
+
class="venix-theme-switcher__heading"
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
>
|
|
47
|
+
{{ resolvedLabel }}
|
|
48
|
+
</li>
|
|
49
|
+
<li
|
|
50
|
+
v-for="item in themes"
|
|
51
|
+
:key="item.value"
|
|
52
|
+
role="none"
|
|
53
|
+
>
|
|
54
|
+
<button
|
|
55
|
+
type="button"
|
|
56
|
+
role="menuitemradio"
|
|
57
|
+
class="venix-theme-switcher__item"
|
|
58
|
+
:class="{ 'venix-theme-switcher__item--active': item.value === theme.preference.value }"
|
|
59
|
+
:aria-checked="item.value === theme.preference.value"
|
|
60
|
+
@click="select(item.value)"
|
|
61
|
+
>
|
|
62
|
+
<VenixIcon
|
|
63
|
+
v-if="item.icon"
|
|
64
|
+
class="venix-theme-switcher__item-icon"
|
|
65
|
+
:icon="item.icon.value"
|
|
66
|
+
:mode="item.icon.format === 'emote' ? void 0 : item.icon.format"
|
|
67
|
+
aria-hidden="true"
|
|
68
|
+
/>
|
|
69
|
+
<span>{{ item.name }}</span>
|
|
70
|
+
</button>
|
|
71
|
+
</li>
|
|
72
|
+
</ul>
|
|
73
|
+
</div>
|
|
74
|
+
</template>
|
|
75
|
+
|
|
76
|
+
<script setup>
|
|
77
|
+
import { ref, computed, onMounted, onUnmounted, nextTick } from "vue";
|
|
78
|
+
import { useVenixTheme } from "../composables/useVenixTheme";
|
|
79
|
+
const LABEL_TRANSLATIONS = {
|
|
80
|
+
"pt-BR": "Tema de cor",
|
|
81
|
+
"en-US": "Color theme",
|
|
82
|
+
"es-ES": "Tema de color",
|
|
83
|
+
"fr-FR": "Th\xE8me de couleur",
|
|
84
|
+
"de-DE": "Farbthema",
|
|
85
|
+
"it-IT": "Tema colore",
|
|
86
|
+
"ja-JP": "\u30AB\u30E9\u30FC\u30C6\u30FC\u30DE",
|
|
87
|
+
"ko-KR": "\uC0C9\uC0C1 \uD14C\uB9C8",
|
|
88
|
+
"zh-CN": "\u989C\u8272\u4E3B\u9898"
|
|
89
|
+
};
|
|
90
|
+
const CHANGED_ANNOUNCEMENT_TRANSLATIONS = {
|
|
91
|
+
"pt-BR": "Tema de cor alterado para {theme}",
|
|
92
|
+
"en-US": "Color theme changed to {theme}",
|
|
93
|
+
"es-ES": "Tema de color cambiado a {theme}",
|
|
94
|
+
"fr-FR": "Th\xE8me de couleur chang\xE9 pour {theme}",
|
|
95
|
+
"de-DE": "Farbthema ge\xE4ndert zu {theme}",
|
|
96
|
+
"it-IT": "Tema colore cambiato in {theme}",
|
|
97
|
+
"ja-JP": "\u30AB\u30E9\u30FC\u30C6\u30FC\u30DE\u304C{theme}\u306B\u5909\u66F4\u3055\u308C\u307E\u3057\u305F",
|
|
98
|
+
"ko-KR": "\uC0C9\uC0C1 \uD14C\uB9C8\uAC00 {theme}(\uC73C)\uB85C \uBCC0\uACBD\uB418\uC5C8\uC2B5\uB2C8\uB2E4",
|
|
99
|
+
"zh-CN": "\u989C\u8272\u4E3B\u9898\u5DF2\u66F4\u6539\u4E3A{theme}"
|
|
100
|
+
};
|
|
101
|
+
const props = defineProps({
|
|
102
|
+
label: { type: String, required: false },
|
|
103
|
+
showLabel: { type: Boolean, required: false }
|
|
104
|
+
});
|
|
105
|
+
const { theme, themes, locale } = useVenixTheme();
|
|
106
|
+
const resolvedLabel = computed(() => props.label ?? locale.translate(LABEL_TRANSLATIONS, "Color theme"));
|
|
107
|
+
const open = ref(false);
|
|
108
|
+
const visible = ref(false);
|
|
109
|
+
const announcement = ref("");
|
|
110
|
+
const rootRef = ref(null);
|
|
111
|
+
const triggerRef = ref(null);
|
|
112
|
+
const activeIcon = computed(() => theme.data.value?.icon ?? null);
|
|
113
|
+
const activeIconKey = computed(() => `${theme.value.value}:${activeIcon.value?.format ?? ""}:${activeIcon.value?.value ?? ""}`);
|
|
114
|
+
const getMenuItems = () => {
|
|
115
|
+
return Array.from(rootRef.value?.querySelectorAll(".venix-theme-switcher__item") ?? []);
|
|
116
|
+
};
|
|
117
|
+
const focusItemAt = (index) => {
|
|
118
|
+
const items = getMenuItems();
|
|
119
|
+
if (items.length === 0) return;
|
|
120
|
+
items[(index + items.length) % items.length]?.focus();
|
|
121
|
+
};
|
|
122
|
+
const toggleOpen = async () => {
|
|
123
|
+
open.value = !open.value;
|
|
124
|
+
if (!open.value) return;
|
|
125
|
+
await nextTick();
|
|
126
|
+
const items = getMenuItems();
|
|
127
|
+
const activeIndex = themes.value.findIndex((item) => item.value === theme.preference.value);
|
|
128
|
+
(items[activeIndex] ?? items[0])?.focus();
|
|
129
|
+
};
|
|
130
|
+
const close = (options = {}) => {
|
|
131
|
+
if (!open.value) return;
|
|
132
|
+
open.value = false;
|
|
133
|
+
if (options.restoreFocus) triggerRef.value?.focus();
|
|
134
|
+
};
|
|
135
|
+
const select = (value) => {
|
|
136
|
+
const selected = themes.value.find((item) => item.value === value);
|
|
137
|
+
theme.toggle(value);
|
|
138
|
+
const template = locale.translate(CHANGED_ANNOUNCEMENT_TRANSLATIONS, "Color theme changed to {theme}");
|
|
139
|
+
announcement.value = template.replace("{theme}", selected?.name ?? value);
|
|
140
|
+
close({ restoreFocus: true });
|
|
141
|
+
};
|
|
142
|
+
const handleMenuKeydown = (event) => {
|
|
143
|
+
const items = getMenuItems();
|
|
144
|
+
const currentIndex = items.findIndex((el) => el === document.activeElement);
|
|
145
|
+
switch (event.key) {
|
|
146
|
+
case "ArrowDown":
|
|
147
|
+
event.preventDefault();
|
|
148
|
+
focusItemAt(currentIndex + 1);
|
|
149
|
+
break;
|
|
150
|
+
case "ArrowUp":
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
focusItemAt(currentIndex - 1);
|
|
153
|
+
break;
|
|
154
|
+
case "Home":
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
focusItemAt(0);
|
|
157
|
+
break;
|
|
158
|
+
case "End":
|
|
159
|
+
event.preventDefault();
|
|
160
|
+
focusItemAt(items.length - 1);
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
const handleFocusOut = (event) => {
|
|
165
|
+
const nextTarget = event.relatedTarget;
|
|
166
|
+
if (!nextTarget || !rootRef.value?.contains(nextTarget)) close();
|
|
167
|
+
};
|
|
168
|
+
const handleClickOutside = (event) => {
|
|
169
|
+
if (!rootRef.value?.contains(event.target)) close();
|
|
170
|
+
};
|
|
171
|
+
const handleKeydown = (event) => {
|
|
172
|
+
if (event.key === "Escape") close({ restoreFocus: true });
|
|
173
|
+
};
|
|
174
|
+
onMounted(() => {
|
|
175
|
+
requestAnimationFrame(() => {
|
|
176
|
+
requestAnimationFrame(() => {
|
|
177
|
+
visible.value = true;
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
document.addEventListener("click", handleClickOutside);
|
|
181
|
+
document.addEventListener("keydown", handleKeydown);
|
|
182
|
+
});
|
|
183
|
+
onUnmounted(() => {
|
|
184
|
+
document.removeEventListener("click", handleClickOutside);
|
|
185
|
+
document.removeEventListener("keydown", handleKeydown);
|
|
186
|
+
});
|
|
187
|
+
</script>
|
|
188
|
+
|
|
189
|
+
<style>
|
|
190
|
+
.venix-theme-switcher{display:inline-block;position:relative}.venix-theme-switcher__sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}.venix-theme-switcher__trigger{align-items:center;background:transparent;border:none;border-radius:999px;color:inherit;cursor:pointer;display:inline-flex;font-size:1.25rem;gap:.5rem;line-height:1;opacity:0;padding:.5rem;transition:opacity .3s ease,background-color .2s ease}.venix-theme-switcher__trigger--visible{opacity:1}.venix-theme-switcher__trigger:focus-visible,.venix-theme-switcher__trigger:hover{background:var(--color-background3,hsla(0,0%,50%,.15))}.venix-theme-switcher__trigger-label{font-size:.875rem}.venix-theme-switcher__menu{background:#1f2128;border:1px solid #3a3d46;border-radius:.75rem;box-shadow:0 8px 24px rgba(0,0,0,.25);list-style:none;margin:0;min-width:12rem;padding:.375rem;position:absolute;right:0;top:calc(100% + .5rem);z-index:50}.venix-theme-switcher__heading{color:rgba(245,245,247,.6);font-size:.75rem;font-weight:600;letter-spacing:.04em;padding:.375rem .625rem;text-align:center;text-transform:uppercase}.venix-theme-switcher__item{align-items:center;background:transparent;border:none;border-radius:.5rem;color:#f5f5f7;cursor:pointer;display:flex;font:inherit;gap:.625rem;padding:.5rem .625rem;text-align:left;width:100%}.venix-theme-switcher__item:focus-visible,.venix-theme-switcher__item:hover{background:hsla(0,0%,100%,.08)}.venix-theme-switcher__item--active{background:hsla(0,0%,100%,.14);color:#fff;font-weight:600}.venix-theme-switcher__item-icon{flex-shrink:0}
|
|
191
|
+
</style>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type __VLS_Props = {
|
|
2
|
+
/** Accessible label for the button and the menu. Without a value, uses the component's own translation based on the current locale. */
|
|
3
|
+
label?: string;
|
|
4
|
+
/** Shows the active theme's name next to the icon on the button. */
|
|
5
|
+
showLabel?: boolean;
|
|
6
|
+
};
|
|
7
|
+
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
8
|
+
declare const _default: typeof __VLS_export;
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ThemeConfig } from '../../shared/types/index.js';
|
|
2
|
+
import type { ThemeIconFormat } from '../../shared/utils/icon.js';
|
|
3
|
+
export declare const useThemeColors: (theme: ThemeConfig, translate: (translations?: Record<string, string>, fallback?: string) => string, iconFormat?: ThemeIconFormat) => {
|
|
4
|
+
themes: import("vue").ComputedRef<{
|
|
5
|
+
value: string;
|
|
6
|
+
name: string;
|
|
7
|
+
icon: import("../../shared/utils/icon.js").ResolvedThemeIcon | null;
|
|
8
|
+
}[]>;
|
|
9
|
+
themeValues: import("vue").ComputedRef<string[]>;
|
|
10
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import { resolveThemeIcon } from "../../shared/utils/icon";
|
|
3
|
+
export const useThemeColors = (theme, translate, iconFormat = "svg") => {
|
|
4
|
+
const themes = computed(() => {
|
|
5
|
+
const list = Object.entries(theme.colors.themes).filter(([value]) => value !== "system").map(([value, themeConfig]) => ({
|
|
6
|
+
value,
|
|
7
|
+
name: translate(themeConfig.translations, value),
|
|
8
|
+
icon: resolveThemeIcon(themeConfig.icon, iconFormat)
|
|
9
|
+
}));
|
|
10
|
+
const systemTheme = theme.colors.themes["system"];
|
|
11
|
+
if (systemTheme) {
|
|
12
|
+
list.unshift({
|
|
13
|
+
value: "system",
|
|
14
|
+
name: translate(systemTheme.translations, "System"),
|
|
15
|
+
icon: resolveThemeIcon(systemTheme.icon, iconFormat)
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return list;
|
|
19
|
+
});
|
|
20
|
+
const themeValues = computed(() => themes.value.map((t) => t.value));
|
|
21
|
+
return {
|
|
22
|
+
themes,
|
|
23
|
+
themeValues
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const useThemeCookies: (localeCookieName?: string) => {
|
|
2
|
+
preferenceCookie: import("nuxt/app").CookieRef<string>;
|
|
3
|
+
resolvedCookie: import("nuxt/app").CookieRef<string>;
|
|
4
|
+
localeCookie: import("nuxt/app").CookieRef<string>;
|
|
5
|
+
persistIfConsented: (preference: string, resolved: string) => void;
|
|
6
|
+
disablePersistence: () => void;
|
|
7
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useCookie } from "#app";
|
|
2
|
+
import {
|
|
3
|
+
THEME_PREFERENCE_COOKIE,
|
|
4
|
+
THEME_RESOLVED_COOKIE,
|
|
5
|
+
DEFAULT_LOCALE_COOKIE_NAME
|
|
6
|
+
} from "../../shared/constants";
|
|
7
|
+
import { hasCookieConsent } from "../../shared/utils/consent";
|
|
8
|
+
export const useThemeCookies = (localeCookieName = DEFAULT_LOCALE_COOKIE_NAME) => {
|
|
9
|
+
const preferenceCookie = useCookie(THEME_PREFERENCE_COOKIE);
|
|
10
|
+
const resolvedCookie = useCookie(THEME_RESOLVED_COOKIE);
|
|
11
|
+
const localeCookie = useCookie(localeCookieName);
|
|
12
|
+
const removeCookie = (name) => {
|
|
13
|
+
if (typeof document !== "undefined") {
|
|
14
|
+
document.cookie = `${name}=; path=/; max-age=0; SameSite=Lax`;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const persistIfConsented = (preference, resolved) => {
|
|
18
|
+
if (!hasCookieConsent()) return;
|
|
19
|
+
preferenceCookie.value = preference;
|
|
20
|
+
resolvedCookie.value = resolved;
|
|
21
|
+
};
|
|
22
|
+
const disablePersistence = () => {
|
|
23
|
+
removeCookie(THEME_PREFERENCE_COOKIE);
|
|
24
|
+
removeCookie(THEME_RESOLVED_COOKIE);
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
preferenceCookie,
|
|
28
|
+
resolvedCookie,
|
|
29
|
+
localeCookie,
|
|
30
|
+
persistIfConsented,
|
|
31
|
+
disablePersistence
|
|
32
|
+
};
|
|
33
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Ref } from 'vue';
|
|
2
|
+
import type { ThemeConfig } from '../../shared/types/index.js';
|
|
3
|
+
export interface UseThemeLocaleOptions {
|
|
4
|
+
enabled?: boolean;
|
|
5
|
+
forcedLocale?: string;
|
|
6
|
+
defaultLocale?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const useThemeLocale: (theme: ThemeConfig, localeCookie: Ref<string | undefined>, options?: UseThemeLocaleOptions) => {
|
|
9
|
+
currentLocale: Ref<string, string>;
|
|
10
|
+
translate: (translations?: Record<string, string>, fallback?: string) => string;
|
|
11
|
+
setLocale: (locale: string) => void;
|
|
12
|
+
};
|