feat(module): implement --ui-radius CSS variable (#2341)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
Sandro Circi
2024-10-09 14:28:29 +02:00
committed by GitHub
parent 68ee3f11ca
commit 057e86cfda
60 changed files with 2406 additions and 2292 deletions

View File

@@ -4,6 +4,9 @@ export default defineAppConfig({
expand: true,
duration: 5000
},
theme: {
radius: 0.25
},
ui: {
colors: {
primary: 'green',

View File

@@ -52,6 +52,7 @@ const links = computed(() => {
})
const color = computed(() => colorMode.value === 'dark' ? (colors as any)[appConfig.ui.colors.neutral][900] : 'white')
const radius = computed(() => `:root { --ui-radius: ${appConfig.theme.radius}rem; }`)
useHead({
meta: [
@@ -62,6 +63,9 @@ useHead({
{ rel: 'icon', type: 'image/svg+xml', href: '/icon.svg' },
{ rel: 'canonical', href: `https://ui.nuxt.com${withoutTrailingSlash(route.path)}` }
],
style: [
{ innerHTML: radius, id: 'nuxt-ui-radius', tagPriority: -2 }
],
htmlAttrs: {
lang: 'en'
}

View File

@@ -11,6 +11,12 @@ const config = useRuntimeConfig().public
const navigation = inject<Ref<NavItem[]>>('navigation')
// const items = computed(() => props.links.map(({ icon, ...link }) => link))
defineShortcuts({
meta_g: () => {
window.open('https://github.com/nuxt/ui/tree/v3', '_blank')
}
})
</script>
<template>
@@ -26,22 +32,22 @@ const navigation = inject<Ref<NavItem[]>>('navigation')
<!-- <UNavigationMenu :items="items" variant="link" /> -->
<template #right>
<ColorPicker />
<ThemePicker />
<UTooltip text="Search" :kbds="['meta', 'K']">
<UContentSearchButton />
</UTooltip>
<ColorModeButton />
<UButton
color="neutral"
variant="ghost"
to="https://github.com/nuxt/ui/tree/v3"
target="_blank"
icon="i-simple-icons-github"
aria-label="GitHub"
/>
<UTooltip text="Open on GitHub" :kbds="['meta', 'G']">
<UButton
color="neutral"
variant="ghost"
to="https://github.com/nuxt/ui/tree/v3"
target="_blank"
icon="i-simple-icons-github"
aria-label="GitHub"
/>
</UTooltip>
</template>
<template #content>

View File

@@ -1,38 +0,0 @@
<template>
<ClientOnly v-if="!colorMode?.forced">
<UButton
:icon="isDark ? appConfig.ui.icons.dark : appConfig.ui.icons.light"
color="neutral"
variant="ghost"
v-bind="{
...$attrs
}"
:aria-label="`Switch to ${isDark ? 'light' : 'dark'} mode`"
@click="isDark = !isDark"
/>
<template #fallback>
<div class="w-8 h-8" />
</template>
</ClientOnly>
</template>
<script setup lang="ts">
defineOptions({
inheritAttrs: false
})
const colorMode = useColorMode()
const appConfig = useAppConfig()
// Computed
const isDark = computed({
get() {
return colorMode.value === 'dark'
},
set() {
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
}
})
</script>

View File

@@ -1,66 +0,0 @@
<template>
<UPopover mode="hover" :ui="{ content: 'p-2' }">
<template #default="{ open }">
<UButton
icon="i-heroicons-swatch-20-solid"
color="neutral"
:variant="open ? 'soft' : 'ghost'"
square
aria-label="Color picker"
:ui="{ leadingIcon: 'text-[--ui-primary]' }"
/>
</template>
<template #content>
<fieldset class="grid grid-cols-5 gap-px">
<legend class="text-[11px] font-bold mb-1">
Primary
</legend>
<ColorPickerPill v-for="color in primaryColors" :key="color" :color="color" :selected="primary" @select="primary = color" />
</fieldset>
<USeparator class="my-2" type="dashed" />
<fieldset class="grid grid-cols-5 gap-px">
<legend class="text-[11px] font-bold mb-1">
Neutral
</legend>
<ColorPickerPill v-for="color in neutralColors" :key="color" :color="color" :selected="neutral" @select="neutral = color" />
</fieldset>
</template>
</UPopover>
</template>
<script setup lang="ts">
import colors from 'tailwindcss/colors'
import { omit } from '#ui/utils'
const appConfig = useAppConfig()
// Computed
const neutralColors = ['slate', 'gray', 'zinc', 'neutral', 'stone']
const neutral = computed({
get() {
return appConfig.ui.colors.neutral
},
set(option) {
appConfig.ui.colors.neutral = option
window.localStorage.setItem('nuxt-ui-neutral', appConfig.ui.colors.neutral)
}
})
const colorsToOmit = ['inherit', 'current', 'transparent', 'black', 'white', ...neutralColors]
const primaryColors = Object.keys(omit(colors, colorsToOmit as any))
const primary = computed({
get() {
return appConfig.ui.colors.primary
},
set(option) {
appConfig.ui.colors.primary = option
window.localStorage.setItem('nuxt-ui-primary', appConfig.ui.colors.primary)
}
})
</script>

View File

@@ -1,24 +0,0 @@
<template>
<UTooltip :text="color" class="capitalize" :portal="false">
<UButton
color="neutral"
square
:variant="color === selected ? 'soft' : 'ghost'"
@click.stop.prevent="$emit('select')"
>
<span
class="inline-block w-3 h-3 rounded-full"
:class="`bg-[--color-light] dark:bg-[--color-dark]`"
:style="{
'--color-light': `var(--color-${color}-500)`,
'--color-dark': `var(--color-${color}-400)`
}"
/>
</UButton>
</UTooltip>
</template>
<script setup lang="ts">
defineProps<{ color: string, selected: string }>()
defineEmits(['select'])
</script>

View File

@@ -214,7 +214,7 @@ const { data: ast } = await useAsyncData(`component-code-${name}-${hash({ props:
<template>
<div class="my-5">
<div>
<div v-if="options.length" class="flex items-center gap-2.5 border border-[--ui-color-neutral-200] dark:border-[--ui-color-neutral-700] border-b-0 relative rounded-t-md px-4 py-2.5 overflow-x-auto">
<div v-if="options.length" class="flex items-center gap-2.5 border border-[--ui-color-neutral-200] dark:border-[--ui-color-neutral-700] border-b-0 relative rounded-t-[calc(var(--ui-radius)*1.5)] px-4 py-2.5 overflow-x-auto">
<template v-for="option in options" :key="option.name">
<UFormField
:label="option.label"
@@ -263,7 +263,7 @@ const { data: ast } = await useAsyncData(`component-code-${name}-${hash({ props:
</template>
</div>
<div class="flex justify-center border border-b-0 border-[--ui-color-neutral-200] dark:border-[--ui-color-neutral-700] relative p-4 z-[1]" :class="[!options.length && 'rounded-t-md', props.class]">
<div class="flex justify-center border border-b-0 border-[--ui-color-neutral-200] dark:border-[--ui-color-neutral-700] relative p-4 z-[1]" :class="[!options.length && 'rounded-t-[calc(var(--ui-radius)*1.5)]', props.class]">
<component :is="name" v-bind="{ ...componentProps, ...componentEvents }">
<template v-for="slot in Object.keys(slots || {})" :key="slot" #[slot]>
<ContentSlot :name="slot" unwrap="p">

View File

@@ -114,7 +114,7 @@ const optionsValues = ref(props.options?.reduce((acc, option) => {
<template>
<div class="my-5">
<div v-if="preview">
<div class="border border-[--ui-color-neutral-200] dark:border-[--ui-color-neutral-700] relative z-[1]" :class="[{ 'border-b-0 rounded-t-md': props.source, 'rounded-md': !props.source }]">
<div class="border border-[--ui-color-neutral-200] dark:border-[--ui-color-neutral-700] relative z-[1]" :class="[{ 'border-b-0 rounded-t-[calc(var(--ui-radius)*1.5)]': props.source, 'rounded-[calc(var(--ui-radius)*1.5)]': !props.source }]">
<div v-if="props.options?.length || !!slots.options" class="flex gap-4 p-4 border-b border-[--ui-color-neutral-200] dark:border-[--ui-color-neutral-700]">
<slot name="options" />

View File

@@ -1,5 +1,5 @@
<template>
<div class="relative overflow-hidden rounded border border-dashed border-[--ui-border-accented] opacity-75 px-4 flex items-center justify-center">
<div class="relative overflow-hidden rounded-[--ui-radius] border border-dashed border-[--ui-border-accented] opacity-75 px-4 flex items-center justify-center">
<svg class="absolute inset-0 h-full w-full stroke-[--ui-border-inverted]/10" fill="none">
<defs>
<pattern

View File

@@ -0,0 +1,140 @@
<template>
<UPopover :ui="{ content: 'w-72 px-6 py-4 flex flex-col gap-4' }">
<template #default="{ open }">
<UButton
icon="i-heroicons-swatch"
color="neutral"
:variant="open ? 'soft' : 'ghost'"
square
aria-label="Color picker"
:ui="{ leadingIcon: 'text-[--ui-primary]' }"
/>
</template>
<template #content>
<fieldset>
<legend class="text-[11px] leading-none font-semibold mb-2">
Primary
</legend>
<div class="grid grid-cols-3 gap-1 -mx-2">
<ThemePickerButton
v-for="color in primaryColors"
:key="color"
:label="color"
:chip="color"
:selected="primary === color"
@select="primary = color"
/>
</div>
</fieldset>
<fieldset>
<legend class="text-[11px] leading-none font-semibold mb-2">
Neutral
</legend>
<div class="grid grid-cols-3 gap-1 -mx-2">
<ThemePickerButton
v-for="color in neutralColors"
:key="color"
:label="color"
:chip="color"
:selected="neutral === color"
@select="neutral = color"
/>
</div>
</fieldset>
<fieldset>
<legend class="text-[11px] leading-none font-semibold mb-2">
Radius
</legend>
<div class="grid grid-cols-5 gap-1 -mx-2">
<ThemePickerButton
v-for="r in radiuses"
:key="r"
:label="String(r)"
class="justify-center px-0"
:selected="radius === r"
@select="radius = r"
/>
</div>
</fieldset>
<fieldset>
<legend class="text-[11px] leading-none font-semibold mb-2">
Theme
</legend>
<div class="flex gap-1 -mx-2">
<ThemePickerButton
v-for="m in modes"
:key="m.label"
v-bind="m"
:selected="mode === m.label"
@select="mode = m.label"
/>
</div>
</fieldset>
</template>
</UPopover>
</template>
<script setup lang="ts">
import colors from 'tailwindcss/colors'
import { omit } from '#ui/utils'
const appConfig = useAppConfig()
const colorMode = useColorMode()
// Computed
const neutralColors = ['slate', 'gray', 'zinc', 'neutral', 'stone']
const neutral = computed({
get() {
return appConfig.ui.colors.neutral
},
set(option) {
appConfig.ui.colors.neutral = option
window.localStorage.setItem('nuxt-ui-neutral', appConfig.ui.colors.neutral)
}
})
const colorsToOmit = ['inherit', 'current', 'transparent', 'black', 'white', ...neutralColors]
const primaryColors = Object.keys(omit(colors, colorsToOmit as any))
const primary = computed({
get() {
return appConfig.ui.colors.primary
},
set(option) {
appConfig.ui.colors.primary = option
window.localStorage.setItem('nuxt-ui-primary', appConfig.ui.colors.primary)
}
})
const radiuses = [0, 0.125, 0.25, 0.375, 0.5]
const radius = computed({
get() {
return appConfig.theme.radius
},
set(option) {
appConfig.theme.radius = option
window.localStorage.setItem('nuxt-ui-radius', String(appConfig.theme.radius))
}
})
const modes = [
{ label: 'light', icon: appConfig.ui.icons.light },
{ label: 'dark', icon: appConfig.ui.icons.dark }
]
const mode = computed({
get() {
return colorMode.value
},
set(option) {
colorMode.preference = option
}
})
</script>

View File

@@ -0,0 +1,32 @@
<template>
<UButton
size="sm"
color="neutral"
:icon="icon"
:label="label"
:variant="selected ? 'soft' : 'outline'"
class="capitalize ring-[--ui-border] rounded-[--ui-radius] text-[11px]"
@click.stop.prevent="$emit('select')"
>
<template v-if="chip" #leading>
<span
class="inline-block w-2 h-2 rounded-full"
:class="`bg-[--color-light] dark:bg-[--color-dark]`"
:style="{
'--color-light': `var(--color-${chip}-500)`,
'--color-dark': `var(--color-${chip}-400)`
}"
/>
</template>
</UButton>
</template>
<script setup lang="ts">
defineProps<{
label: string
icon?: string
chip?: string
selected?: boolean
}>()
defineEmits(['select'])
</script>

View File

@@ -11,15 +11,22 @@ export default defineNuxtPlugin({
}
}
function updateRadius() {
const radius = localStorage.getItem('nuxt-ui-radius')
if (radius) {
appConfig.theme.radius = Number.parseFloat(radius)
}
}
updateColor('primary')
updateColor('neutral')
updateRadius()
}
if (import.meta.server) {
useHead({
script: [
{
innerHTML: `
script: [{
innerHTML: `
let html = document.querySelector('style#nuxt-ui-colors').innerHTML;
if (localStorage.getItem('nuxt-ui-primary')) {
@@ -39,10 +46,17 @@ export default defineNuxtPlugin({
document.querySelector('style#nuxt-ui-colors').innerHTML = html;
`.replace(/\s+/g, ' '),
type: 'text/javascript',
tagPriority: -1
}
]
type: 'text/javascript',
tagPriority: -1
}, {
innerHTML: `
if (localStorage.getItem('nuxt-ui-radius')) {
document.querySelector('style#nuxt-ui-radius').innerHTML = ':root { --ui-radius: ' + localStorage.getItem('nuxt-ui-radius') + 'rem; }';
}
`.replace(/\s+/g, ' '),
type: 'text/javascript',
tagPriority: -1
}]
})
}
}

View File

@@ -110,7 +110,7 @@ export default defineAppConfig({
```
::note
Try the :prose-icon{name="i-heroicons-swatch-20-solid" class="text-[--ui-primary]"} picker in the header above to change `primary` and `neutral` colors.
Try the :prose-icon{name="i-heroicons-swatch" class="text-[--ui-primary]"} theme picker in the header above to change `primary` and `neutral` colors.
::
These colors are used to style the components but also to generate the `color` variants:
@@ -149,7 +149,7 @@ export default defineNuxtConfig({
::
::note
::warning
These color aliases are not automatically defined as Tailwind CSS colors, so classes like `text-primary-500 dark:text-primary-400` won't be available by default as in Nuxt UI v2. This approach provides more flexibility and prevents overwriting of user-defined Tailwind CSS colors.<br><br>
However, you can generate these classes using Tailwind's `@theme` directive, allowing you to use custom color utility classes while maintaining dynamic color aliases:
@@ -178,7 +178,11 @@ However, you can generate these classes using Tailwind's `@theme` directive, all
### Tokens
Nuxt UI generates CSS variables as design tokens for component styling. These tokens enable consistent theming and support both light and dark modes. You can use them in Tailwind classes like `text-[--ui-primary]`, which automatically adapts to the current color scheme.
Nuxt UI leverages a robust system of CSS variables as design tokens to ensure consistent and flexible component styling. These tokens form the foundation of the theming system, offering smooth support for both light and dark modes.
#### Color Shades
Nuxt UI automatically creates a CSS variable for each color alias you define which represent the default shade used in both light and dark modes:
::code-group
@@ -206,6 +210,10 @@ Nuxt UI generates CSS variables as design tokens for component styling. These to
::
::note
You can use these variables in classes like `text-[--ui-primary]`, it will automatically adapt to the current color scheme.
::
::tip
You can change which shade is used for each color on light and dark mode:
@@ -225,6 +233,8 @@ You can change which shade is used for each color on light and dark mode:
```
::
#### Neutral Palette
Nuxt UI provides a comprehensive set of design tokens for the `neutral` color palette, ensuring consistent and accessible UI styling across both light and dark modes. These tokens offer fine-grained control over text, background, and border colors:
::code-group
@@ -293,7 +303,18 @@ Nuxt UI provides a comprehensive set of design tokens for the `neutral` color pa
::
You can easily customize these CSS variables in your `app.vue`{lang="ts-type"} or [CSS](https://nuxt.com/docs/getting-started/styling#the-css-property) to tailor the appearance of your application:
::note
Nuxt UI automatically applies a text and background color on the `<body>` element of your app:
```css
body {
@apply antialiased font-sans text-[--ui-text] bg-[--ui-bg];
}
```
::
::tip
You can customize these CSS variables to tailor the appearance of your application:
```vue [app.vue]
<style>
@@ -311,15 +332,35 @@ You can easily customize these CSS variables in your `app.vue`{lang="ts-type"} o
}
</style>
```
::
::note
Nuxt UI automatically applies a text and background color on the `<body>` element of your app:
#### Border Radius
Nuxt UI uses a global `--ui-radius` CSS variable for consistent border rounding. Components use variations of this base value, like `rounded-[calc(var(--ui-radius)*2)]`, to create different levels of roundness throughout the UI:
```css
body {
@apply antialiased font-sans text-[--ui-text] bg-[--ui-bg];
:root {
--ui-radius: var(--radius-sm);
}
```
::note
Try the :prose-icon{name="i-heroicons-swatch" class="text-[--ui-primary]"} theme picker in the header above to change the base radius value.
::
::tip
You can customize the default radius value using the default Tailwind CSS variables or a value of your choice:
```vue [app.vue]
<style>
@import "tailwindcss";
@import "@nuxt/ui";
:root {
--ui-radius: var(--radius-sm);
}
</style>
```
::
## Components theme
@@ -335,7 +376,7 @@ Components in Nuxt UI can have multiple `slots`, each representing a distinct HT
```ts [src/theme/card.ts]
export default {
slots: {
root: 'bg-[--ui-bg] ring ring-[--ui-border] divide-y divide-[--ui-border] rounded-lg shadow',
root: 'bg-[--ui-bg] ring ring-[--ui-border] divide-y divide-[--ui-border] rounded-[calc(var(--ui-radius)*2)] shadow',
header: 'p-4 sm:px-6',
body: 'p-4 sm:p-6',
footer: 'p-4 sm:px-6'