This commit is contained in:
2026-08-05 19:01:52 +08:00
parent cef594cc86
commit 6a1b49c385
25 changed files with 1354 additions and 109 deletions
@@ -12,7 +12,6 @@ import {
ComboboxTrigger,
ComboboxViewport,
} from 'reka-ui'
import selectArrow from '../../../../../design/src-web/shared/img/sprite_interface.NPK/windowcommon_2x.img/0.PNG'
import { cn } from '@/lib/utils'
export interface UiAutocompleteOption {
@@ -56,12 +55,17 @@ function displayValue(value: string | undefined) {
:display-value="displayValue"
:placeholder="placeholder"
/>
<ComboboxTrigger class="ui-autocomplete__trigger" aria-label="展开候选项">
<img :src="selectArrow" alt="" />
<ComboboxTrigger class="ui-autocomplete__trigger ui-select-indicator" aria-label="展开候选项">
<span class="ui-select-indicator__glyph" />
</ComboboxTrigger>
</ComboboxAnchor>
<ComboboxPortal>
<ComboboxContent class="ui-autocomplete__content" position="popper" :side-offset="2">
<ComboboxContent
class="ui-autocomplete__content"
position="popper"
:side-offset="2"
:collision-padding="8"
>
<ComboboxViewport class="ui-autocomplete__viewport">
<ComboboxEmpty class="ui-autocomplete__empty">{{ emptyText }}</ComboboxEmpty>
<ComboboxItem
@@ -82,6 +86,10 @@ function displayValue(value: string | undefined) {
</template>
<style lang="scss">
[data-reka-popper-content-wrapper]:has(> .ui-autocomplete__content) {
z-index: 80 !important;
}
.ui-autocomplete {
display: block;
min-width: 0;
@@ -127,38 +135,18 @@ function displayValue(value: string | undefined) {
}
.ui-autocomplete__trigger {
display: inline-flex;
width: var(--ui-select-icon-inline-size);
height: var(--ui-select-icon-block-size);
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid var(--ui-control-border);
outline: none;
background: var(--ui-control-background);
box-shadow: 0 0 0 1px var(--ui-select-icon-shadow);
cursor: pointer;
&:hover,
&:focus-visible {
border-color: var(--ui-control-border-active);
background: var(--ui-control-background-hover);
}
&:active img {
transform: translateY(1px);
}
img {
width: var(--ui-select-glyph-inline-size);
height: var(--ui-select-glyph-block-size);
image-rendering: pixelated;
}
}
.ui-autocomplete__content {
position: relative;
z-index: 80;
min-width: var(--reka-combobox-trigger-width);
box-sizing: border-box;
width: min(var(--reka-combobox-trigger-width), calc(100vw - 16px));
min-width: min(var(--reka-combobox-trigger-width), calc(100vw - 16px));
max-width: calc(100vw - 16px);
max-height: min(
var(--ui-autocomplete-max-block-size),
var(--reka-combobox-content-available-height)
@@ -0,0 +1,257 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { PopoverContent, PopoverPortal, PopoverRoot, PopoverTrigger } from 'reka-ui'
import { cn } from '@/lib/utils'
export interface UiCascaderItem {
value: string
label: string
disabled?: boolean
}
export interface UiCascaderGroup extends UiCascaderItem {
children: UiCascaderItem[]
}
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
options: UiCascaderGroup[]
label?: string
placeholder?: string
disabled?: boolean
}>(),
{ label: '级联筛选', placeholder: '请选择' },
)
const model = defineModel<string[]>({ default: () => [] })
const open = ref(false)
const activeGroupValue = ref<string>()
const selectedGroup = computed(
() => props.options.find((group) => group.value === model.value[0]) ?? props.options[0],
)
const activeGroup = computed(
() =>
props.options.find((group) => group.value === activeGroupValue.value) ?? selectedGroup.value,
)
const selectedChild = computed(() =>
selectedGroup.value?.children.find((child) => child.value === model.value[1]),
)
function chooseGroup(group: UiCascaderGroup) {
if (group.disabled) return
activeGroupValue.value = group.value
}
function chooseChild(child: UiCascaderItem) {
if (child.disabled || !activeGroup.value) return
model.value = [activeGroup.value.value, child.value]
open.value = false
}
</script>
<template>
<PopoverRoot v-model:open="open">
<PopoverTrigger as-child>
<button
type="button"
:class="cn('ui-cascader__trigger', props.class)"
:disabled="disabled"
:aria-label="label"
>
<span>{{ selectedGroup?.label ?? placeholder }}</span>
<span>{{ selectedChild?.label ?? selectedGroup?.children[0]?.label ?? placeholder }}</span>
</button>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent
class="ui-cascader__content"
position="popper"
:side-offset="2"
:collision-padding="8"
>
<div class="ui-cascader__column" role="listbox" aria-label="一级分类">
<button
v-for="group in options"
:key="group.value"
type="button"
role="option"
class="ui-cascader__option"
:class="{ 'is-active': activeGroup?.value === group.value }"
:aria-selected="activeGroup?.value === group.value"
:disabled="group.disabled"
@click="chooseGroup(group)"
>
{{ group.label }}<span aria-hidden="true">›</span>
</button>
</div>
<div class="ui-cascader__column" role="listbox" aria-label="二级分类">
<button
v-for="child in activeGroup?.children ?? []"
:key="child.value"
type="button"
role="option"
class="ui-cascader__option"
:class="{
'is-active':
activeGroup?.value === selectedGroup?.value && child.value === selectedChild?.value,
}"
:aria-selected="
activeGroup?.value === selectedGroup?.value && child.value === selectedChild?.value
"
:disabled="child.disabled"
@click="chooseChild(child)"
>
{{ child.label }}
</button>
</div>
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</template>
<style lang="scss">
[data-reka-popper-content-wrapper]:has(> .ui-cascader__content) {
z-index: 80 !important;
}
.ui-cascader__trigger {
appearance: none;
box-sizing: border-box;
display: grid;
width: 100%;
min-width: 0;
height: var(--ui-control-height-default);
grid-template-columns: minmax(0, var(--ui-cascader-primary-ratio)) minmax(0, 1fr);
padding: 0;
border: 0;
border-bottom: 1px solid var(--ui-cascader-underline);
outline: none;
background: var(--ui-cascader-shell-background);
cursor: pointer;
span {
position: relative;
display: flex;
min-width: 0;
height: calc(var(--ui-control-height-default) - 1px);
align-items: center;
padding: 0 22px 0 10px;
overflow: hidden;
border: 1px solid var(--ui-cascader-trigger-border);
border-bottom: 0;
background: var(--ui-cascader-trigger-background);
color: var(--ui-cascader-trigger-foreground);
text-overflow: ellipsis;
white-space: nowrap;
&::after {
content: '';
position: absolute;
top: 50%;
right: 9px;
border-top: 5px solid var(--ui-cascader-trigger-foreground);
border-right: 4px solid transparent;
border-left: 4px solid transparent;
filter: drop-shadow(0 1px 0 var(--ui-popup-shadow));
transform: translateY(-50%);
transition: transform 90ms ease-out;
}
+ span {
border-left: 0;
}
}
&:hover:not(:disabled) span,
&:focus-visible span,
&[data-state='open'] span {
border-color: var(--ui-cascader-trigger-hover-border);
background: var(--ui-cascader-trigger-hover-background);
color: var(--ui-cascader-trigger-hover-foreground);
box-shadow: inset 0 1px 0 var(--ui-cascader-trigger-highlight);
}
&[data-state='open'] span::after {
transform: translateY(-50%) rotate(180deg);
}
&:disabled {
cursor: not-allowed;
opacity: 0.48;
}
}
.ui-cascader__content {
position: relative;
z-index: 80;
box-sizing: border-box;
display: grid;
width: min(var(--ui-cascader-popup-inline-size), calc(100vw - 16px));
max-height: min(var(--ui-cascader-popup-max-block-size), var(--reka-popper-available-height));
grid-template-columns: repeat(2, minmax(0, 1fr));
overflow: hidden;
border: 1px solid var(--ui-cascader-popup-border);
outline: none;
background: var(--ui-tree-background);
box-shadow: 0 8px 16px var(--ui-popup-shadow);
}
.ui-cascader__column {
min-width: 0;
overflow-y: auto;
+ .ui-cascader__column {
border-left: 1px solid var(--ui-option-divider);
}
}
.ui-cascader__option {
appearance: none;
display: flex;
width: 100%;
height: var(--ui-cascader-option-block-size);
align-items: center;
justify-content: space-between;
padding: 0 7px;
border: 0;
outline: none;
background: var(--ui-tree-background);
color: var(--ui-primary-foreground);
text-align: left;
cursor: pointer;
&:hover:not(:disabled),
&:focus-visible,
&.is-active {
background: var(--ui-cascader-option-highlight);
box-shadow: inset 2px 0 0 var(--ui-ring);
color: var(--ui-option-highlight-foreground);
}
&:disabled {
cursor: not-allowed;
opacity: 0.45;
}
}
@media (max-width: 420px) {
.ui-cascader__trigger {
--ui-cascader-primary-ratio: 44%;
}
.ui-cascader__content {
grid-template-columns: 1fr;
}
.ui-cascader__column {
max-height: 132px;
+ .ui-cascader__column {
border-top: 1px solid var(--ui-option-divider);
border-left: 0;
}
}
}
</style>
@@ -0,0 +1 @@
export { default as UiCascader, type UiCascaderGroup, type UiCascaderItem } from './UiCascader.vue'
@@ -1,5 +1,5 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { CSSProperties, HTMLAttributes } from 'vue'
import {
DialogContent,
type DialogContentEmits,
@@ -8,23 +8,108 @@ import {
} from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
const props = withDefaults(
defineProps<
DialogContentProps & {
class?: HTMLAttributes['class']
draggable?: boolean
}
>(),
{ draggable: true },
)
const emits = defineEmits<DialogContentEmits>()
const dragOffset = ref({ x: 0, y: 0 })
let activePointerId: number | undefined
let dragOrigin = { clientX: 0, clientY: 0, x: 0, y: 0 }
let dragBounds = { minX: 0, maxX: 0, minY: 0, maxY: 0 }
const delegatedProps = computed(() => {
const { class: _class, ...delegated } = props
const { class: _class, draggable: _draggable, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
const dragStyle = computed<CSSProperties>(() => ({
'--ui-dialog-drag-x': `${dragOffset.value.x}px`,
'--ui-dialog-drag-y': `${dragOffset.value.y}px`,
}))
function clamp(value: number, min: number, max: number) {
return Math.min(Math.max(value, min), max)
}
function stopDragging() {
activePointerId = undefined
document.documentElement.classList.remove('is-dialog-dragging')
window.removeEventListener('pointermove', handlePointerMove)
window.removeEventListener('pointerup', stopDragging)
window.removeEventListener('pointercancel', stopDragging)
}
function resetPosition() {
stopDragging()
dragOffset.value = { x: 0, y: 0 }
}
function handlePointerMove(event: PointerEvent) {
if (activePointerId !== event.pointerId) return
const deltaX = event.clientX - dragOrigin.clientX
const deltaY = event.clientY - dragOrigin.clientY
dragOffset.value = {
x: dragOrigin.x + clamp(deltaX, dragBounds.minX, dragBounds.maxX),
y: dragOrigin.y + clamp(deltaY, dragBounds.minY, dragBounds.maxY),
}
}
function handlePointerDown(event: PointerEvent) {
if (!props.draggable || event.button !== 0) return
const target = event.target
if (!(target instanceof Element)) return
const handle = target.closest<HTMLElement>('[data-ui-dialog-drag-handle]')
if (!handle || target.closest('button, a, input, textarea, select, [data-ui-dialog-no-drag]'))
return
const content = event.currentTarget
if (!(content instanceof HTMLElement)) return
const rect = content.getBoundingClientRect()
const viewportMargin = 8
activePointerId = event.pointerId
dragOrigin = {
clientX: event.clientX,
clientY: event.clientY,
x: dragOffset.value.x,
y: dragOffset.value.y,
}
dragBounds = {
minX: viewportMargin - rect.left,
maxX: window.innerWidth - viewportMargin - rect.right,
minY: viewportMargin - rect.top,
maxY: window.innerHeight - viewportMargin - rect.bottom,
}
document.documentElement.classList.add('is-dialog-dragging')
window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointerup', stopDragging)
window.addEventListener('pointercancel', stopDragging)
event.preventDefault()
}
onMounted(() => window.addEventListener('resize', resetPosition))
onBeforeUnmount(() => {
stopDragging()
window.removeEventListener('resize', resetPosition)
})
</script>
<template>
<DialogContent
v-bind="forwarded"
:style="dragStyle"
:data-draggable="draggable ? '' : undefined"
@pointerdown="handlePointerDown"
@open-auto-focus="resetPosition"
:class="
cn(
'ui-dialog-content fixed left-1/2 top-1/2 z-50 grid -translate-x-1/2 -translate-y-1/2 outline-none data-[state=closed]:animate-[dialog-out_120ms_ease-in] data-[state=open]:animate-[dialog-in_160ms_ease-out]',
'ui-dialog-content fixed left-1/2 top-1/2 z-50 grid outline-none data-[state=closed]:animate-[dialog-out_120ms_ease-in] data-[state=open]:animate-[dialog-in_160ms_ease-out]',
props.class,
)
"
@@ -36,6 +121,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<style scoped>
.ui-dialog-content {
width: min(var(--ui-dialog-viewport-inline-size), var(--ui-dialog-max-inline-size));
max-height: calc(100dvh - 16px);
overflow: hidden;
border: 1px solid var(--ui-dialog-border);
border-radius: 0;
@@ -44,5 +130,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
0 18px 38px var(--ui-dialog-shadow),
inset 0 0 0 1px var(--ui-dialog-inset);
color: var(--ui-surface-foreground);
transform: translate(
calc(-50% + var(--ui-dialog-drag-x, 0px)),
calc(-50% + var(--ui-dialog-drag-y, 0px))
);
will-change: transform;
}
:global(.is-dialog-dragging),
:global(.is-dialog-dragging *) {
cursor: grabbing !important;
user-select: none !important;
}
</style>
@@ -28,7 +28,11 @@ onBeforeUnmount(() => clearInterval(timer))
</script>
<template>
<div class="ui-dialog-header" :class="{ 'is-focused': focused, 'is-unfocused': !focused }">
<div
class="ui-dialog-header"
:class="{ 'is-focused': focused, 'is-unfocused': !focused }"
data-ui-dialog-drag-handle
>
<img
v-if="focused && effectFrames.length"
class="ui-dialog-header__effect"
@@ -60,6 +64,9 @@ onBeforeUnmount(() => clearInterval(timer))
background: var(--ui-dialog-titlebar-background);
color: var(--ui-titlebar-foreground);
text-shadow: 0 1px 1px var(--ui-dialog-titlebar-text-shadow);
touch-action: none;
user-select: none;
cursor: grab;
}
.ui-dialog-header.is-focused {
@@ -87,6 +94,10 @@ onBeforeUnmount(() => clearInterval(timer))
position: relative;
z-index: 2;
grid-column: 2;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-align: center;
}
.ui-dialog-header__actions {
@@ -18,9 +18,15 @@ const delegatedProps = computed(() => {
<style scoped>
.ui-dialog-title {
display: block;
min-width: 0;
overflow: hidden;
color: var(--ui-titlebar-foreground);
font-size: var(--ui-control-font-size-default);
font-weight: 400;
line-height: var(--ui-control-line-height);
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
@@ -0,0 +1,118 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { PaginationNext, PaginationPrev, PaginationRoot } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
total: number
itemsPerPage?: number
disabled?: boolean
label?: string
resultLabel?: string
showResult?: boolean
}>(),
{
itemsPerPage: 10,
label: '分页',
resultLabel: '搜索结果',
showResult: true,
},
)
const page = defineModel<number>('page', { default: 1 })
</script>
<template>
<PaginationRoot
v-slot="{ page: currentPage, pageCount }"
v-model:page="page"
:total="total"
:items-per-page="itemsPerPage"
:disabled="disabled"
:class="cn('ui-pagination', props.class)"
:aria-label="label"
>
<PaginationPrev class="ui-pagination__button" aria-label="上一页">◀</PaginationPrev>
<output class="ui-pagination__status" aria-live="polite">
<strong>{{ currentPage }}</strong
><span>/</span><span>{{ pageCount }}</span>
</output>
<PaginationNext class="ui-pagination__button" aria-label="下一页">▶</PaginationNext>
<span v-if="showResult" class="ui-pagination__result">{{ resultLabel }} {{ total }} 个</span>
</PaginationRoot>
</template>
<style scoped>
.ui-pagination {
display: flex;
max-width: 100%;
min-height: var(--ui-pagination-block-size);
align-items: center;
gap: var(--ui-pagination-gap);
color: var(--ui-primary-foreground);
}
.ui-pagination__button {
appearance: none;
display: inline-flex;
width: var(--ui-pagination-button-inline-size);
height: var(--ui-pagination-button-block-size);
flex: 0 0 auto;
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid var(--ui-pagination-border);
outline: none;
background: var(--ui-pagination-background);
color: var(--ui-control-disabled-foreground);
cursor: pointer;
}
.ui-pagination__button:not(:disabled):hover,
.ui-pagination__button:not(:disabled):focus-visible {
border-color: var(--ui-control-border-active);
background: var(--ui-pagination-active-background);
color: var(--ui-pagination-active-foreground);
}
.ui-pagination__button:not(:disabled):active {
transform: translateY(1px);
}
.ui-pagination__button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.ui-pagination__status {
display: inline-flex;
min-width: 46px;
align-items: baseline;
justify-content: center;
gap: 4px;
white-space: nowrap;
}
.ui-pagination__status strong {
color: var(--ui-selected);
font-weight: 400;
}
.ui-pagination__result {
min-width: 0;
color: var(--ui-muted-foreground);
white-space: nowrap;
}
@media (max-width: 420px) {
.ui-pagination {
flex-wrap: wrap;
}
.ui-pagination__result {
width: 100%;
}
}
</style>
@@ -0,0 +1 @@
export { default as UiPagination } from './UiPagination.vue'
@@ -0,0 +1,94 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import {
PopoverContent,
type PopoverContentEmits,
type PopoverContentProps,
PopoverPortal,
useForwardPropsEmits,
} from 'reka-ui'
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<
PopoverContentProps & {
class?: HTMLAttributes['class']
variant?: 'default' | 'rich'
}
>(),
{ collisionPadding: 8, sideOffset: 6, variant: 'default' },
)
const emits = defineEmits<PopoverContentEmits>()
const delegatedProps = computed(() => {
const { class: _class, variant: _variant, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<PopoverPortal>
<PopoverContent
v-bind="forwarded"
:class="cn('ui-popover-content', `ui-popover-content--${variant}`, props.class)"
>
<slot />
</PopoverContent>
</PopoverPortal>
</template>
<style>
[data-reka-popper-content-wrapper]:has(> .ui-popover-content) {
z-index: 80 !important;
}
.ui-popover-content {
position: relative;
z-index: 80;
box-sizing: border-box;
width: min(var(--ui-popover-inline-size), var(--reka-popper-available-width));
max-width: calc(100vw - 16px);
padding: var(--ui-popover-padding);
border: 1px solid var(--ui-popover-border);
border-radius: 0;
outline: none;
background: var(--ui-popover-background);
box-shadow: 0 8px 18px var(--ui-popover-shadow);
color: var(--ui-popover-foreground);
transform-origin: var(--reka-popover-content-transform-origin);
}
.ui-popover-content--rich {
--ui-popover-inline-size: var(--ui-popover-rich-inline-size);
line-height: 1.45;
}
.ui-popover-content[data-state='open'] {
animation: ui-popover-in 100ms ease-out;
}
.ui-popover-content[data-state='closed'] {
animation: ui-popover-out 80ms ease-in;
}
@keyframes ui-popover-in {
from {
opacity: 0;
transform: translateY(2px);
}
}
@keyframes ui-popover-out {
to {
opacity: 0;
}
}
@media (prefers-reduced-motion: reduce) {
.ui-popover-content {
animation: none !important;
}
}
</style>
@@ -0,0 +1,2 @@
export { PopoverAnchor, PopoverClose, PopoverRoot, PopoverTrigger } from 'reka-ui'
export { default as UiPopoverContent } from './UiPopoverContent.vue'
@@ -2,7 +2,6 @@
import type { HTMLAttributes } from 'vue'
import { SelectIcon, SelectTrigger, type SelectTriggerProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import selectArrow from '../../../../../design/src-web/shared/img/sprite_interface.NPK/windowcommon_2x.img/0.PNG'
const props = defineProps<SelectTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = computed(() => {
@@ -14,9 +13,9 @@ const delegatedProps = computed(() => {
<template>
<SelectTrigger v-bind="delegatedProps" :class="cn('ui-select-trigger', props.class)">
<slot />
<SelectIcon class="ui-select-trigger__icon" aria-hidden="true"
><img :src="selectArrow" alt=""
/></SelectIcon>
<SelectIcon class="ui-select-indicator" aria-hidden="true">
<span class="ui-select-indicator__glyph" />
</SelectIcon>
</SelectTrigger>
</template>
@@ -54,32 +53,4 @@ const delegatedProps = computed(() => {
cursor: not-allowed;
opacity: 0.48;
}
.ui-select-trigger__icon {
display: inline-flex;
width: var(--ui-select-icon-inline-size);
height: var(--ui-select-icon-block-size);
align-items: center;
justify-content: center;
border: 1px solid var(--ui-control-border);
background: var(--ui-control-background);
box-shadow: 0 0 0 1px var(--ui-select-icon-shadow);
}
.ui-select-trigger__icon img {
width: var(--ui-select-glyph-inline-size);
height: var(--ui-select-glyph-block-size);
image-rendering: pixelated;
}
.ui-select-trigger:hover .ui-select-trigger__icon,
.ui-select-trigger:focus-visible .ui-select-trigger__icon {
border-color: var(--ui-control-border-active);
background: var(--ui-control-background-hover);
}
.ui-select-trigger:active .ui-select-trigger__icon,
.ui-select-trigger:active .ui-select-trigger__icon img {
transform: translateY(1px);
}
</style>
@@ -10,12 +10,17 @@ import {
import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<TooltipContentProps & { class?: HTMLAttributes['class'] }>(),
{ collisionPadding: 8, sideOffset: 6 },
defineProps<
TooltipContentProps & {
class?: HTMLAttributes['class']
variant?: 'simple' | 'rich'
}
>(),
{ collisionPadding: 8, sideOffset: 6, variant: 'simple' },
)
const emits = defineEmits<TooltipContentEmits>()
const delegatedProps = computed(() => {
const { class: _class, ...delegated } = props
const { class: _class, variant: _variant, ...delegated } = props
return delegated
})
const forwarded = useForwardPropsEmits(delegatedProps, emits)
@@ -23,7 +28,10 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<template>
<TooltipPortal>
<TooltipContent v-bind="forwarded" :class="cn('ui-tooltip-content', props.class)">
<TooltipContent
v-bind="forwarded"
:class="cn('ui-tooltip-content', `ui-tooltip-content--${variant}`, props.class)"
>
<slot />
</TooltipContent>
</TooltipPortal>
@@ -49,6 +57,19 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
transform-origin: var(--reka-tooltip-content-transform-origin);
}
.ui-tooltip-content--rich {
box-sizing: border-box;
width: min(var(--ui-tooltip-rich-inline-size), calc(100vw - 16px));
max-width: calc(100vw - 16px);
padding: 9px 10px;
line-height: 1.45;
}
.ui-tooltip-content--rich strong {
color: var(--ui-tooltip-strong);
font-weight: 400;
}
.ui-tooltip-content[data-state='delayed-open'],
.ui-tooltip-content[data-state='instant-open'] {
animation: tooltip-in 100ms ease-out;
@@ -0,0 +1,350 @@
<script setup lang="ts">
import type { CSSProperties, HTMLAttributes } from 'vue'
import {
PopoverContent,
PopoverPortal,
PopoverRoot,
PopoverTrigger,
TreeItem,
TreeRoot,
} from 'reka-ui'
import { cn } from '@/lib/utils'
export interface UiTreeSelectItem {
value: string
label: string
tone?: 'normal' | 'advanced' | 'rare' | 'artifact' | 'legend' | 'epic' | 'myth' | 'primeval'
disabled?: boolean
children?: UiTreeSelectItem[]
}
const props = withDefaults(
defineProps<{
class?: HTMLAttributes['class']
items: UiTreeSelectItem[]
placeholder?: string
label?: string
disabled?: boolean
}>(),
{ placeholder: '请选择', label: '树形选择' },
)
const model = defineModel<string>()
const open = ref(false)
const expanded = ref<string[]>([])
function findItem(
items: UiTreeSelectItem[],
value: string | undefined,
): UiTreeSelectItem | undefined {
for (const item of items) {
if (item.value === value) return item
const nested = item.children && findItem(item.children, value)
if (nested) return nested
}
}
const selectedItem = computed(() => findItem(props.items, model.value))
function findAncestorValues(
items: UiTreeSelectItem[],
value: string,
ancestors: string[] = [],
): string[] | undefined {
for (const item of items) {
if (item.value === value) return ancestors
if (!item.children?.length) continue
const nested = findAncestorValues(item.children, value, [...ancestors, item.value])
if (nested) return nested
}
}
function expandSelectedPath() {
if (!model.value) return
const ancestors = findAncestorValues(props.items, model.value)
if (!ancestors?.length) return
expanded.value = [...new Set([...expanded.value, ...ancestors])]
}
watch(open, (isOpen) => {
if (isOpen) expandSelectedPath()
})
watch(
() => [props.items, model.value] as const,
() => {
if (open.value) expandSelectedPath()
},
)
function handleSelect(item: UiTreeSelectItem | undefined) {
if (!item || item.disabled || item.children?.length) return
model.value = item.value
open.value = false
}
function preventParentSelection(event: Event, item: UiTreeSelectItem) {
if (item.children?.length) event.preventDefault()
}
function depthStyle(level: number): CSSProperties {
return { '--ui-tree-depth': Math.max(0, level - 1) } as CSSProperties
}
</script>
<template>
<PopoverRoot v-model:open="open">
<PopoverTrigger as-child>
<button
type="button"
:class="cn('ui-tree-select__trigger', props.class)"
:disabled="disabled"
:aria-label="label"
>
<span :class="selectedItem?.tone && `quality-${selectedItem.tone}`">
{{ selectedItem?.label ?? placeholder }}
</span>
<i class="ui-select-indicator" aria-hidden="true">
<span class="ui-select-indicator__glyph" />
</i>
</button>
</PopoverTrigger>
<PopoverPortal>
<PopoverContent
class="ui-tree-select__content"
position="popper"
:side-offset="2"
:collision-padding="8"
>
<TreeRoot
v-slot="{ flattenItems }"
class="ui-tree-select__tree"
:model-value="selectedItem"
v-model:expanded="expanded"
:items="items"
:get-key="(item) => item.value"
:get-children="(item) => item.children"
aria-label="树形选项"
@update:model-value="handleSelect"
>
<TreeItem
v-for="item in flattenItems"
:key="item._id"
v-slot="{ isExpanded }"
v-bind="item.bind"
class="ui-tree-select__item"
:class="[
item.hasChildren && 'is-parent',
item.value.tone && `quality-${item.value.tone}`,
]"
:disabled="item.value.disabled"
:style="depthStyle(item.level)"
@select="preventParentSelection($event, item.value)"
>
<span
v-if="item.hasChildren"
class="ui-tree-select__branch"
:class="{ 'is-expanded': isExpanded }"
aria-hidden="true"
/>
<span>{{ item.value.label }}</span>
</TreeItem>
</TreeRoot>
</PopoverContent>
</PopoverPortal>
</PopoverRoot>
</template>
<style lang="scss">
[data-reka-popper-content-wrapper]:has(> .ui-tree-select__content) {
z-index: 80 !important;
}
.ui-tree-select__trigger {
box-sizing: border-box;
display: inline-flex;
width: 100%;
min-width: 0;
height: var(--ui-control-height-default);
align-items: center;
justify-content: space-between;
gap: var(--ui-control-gap);
padding: 0 4px 0 var(--ui-field-padding-inline);
border: 1px solid var(--ui-input);
border-radius: 0;
outline: none;
background: var(--ui-field-background);
box-shadow: inset 0 0 0 1px var(--ui-field-inset);
color: var(--ui-primary-foreground);
cursor: pointer;
> span {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
&:focus-visible,
&[data-state='open'] {
border-color: var(--ui-ring);
box-shadow:
inset 0 0 0 1px var(--ui-field-focus-inset),
0 0 0 1px var(--ui-field-focus-edge);
}
&:disabled {
cursor: not-allowed;
opacity: 0.48;
}
}
.ui-tree-select__content {
position: relative;
z-index: 80;
box-sizing: border-box;
width: min(var(--reka-popper-anchor-width), calc(100vw - 16px));
min-width: min(var(--ui-tree-select-min-inline-size), calc(100vw - 16px));
max-height: min(var(--ui-tree-select-max-block-size), var(--reka-popper-available-height));
padding: 2px 3px;
overflow-y: auto;
border: 1px solid var(--ui-popup-border);
outline: none;
background: var(--ui-tree-background);
box-shadow: 0 8px 18px var(--ui-popup-shadow);
}
.ui-tree-select__tree {
display: flex;
width: 100%;
flex-direction: column;
gap: 2px;
margin: 0;
padding: 0;
outline: none;
list-style: none;
font-size: var(--ui-tree-item-font-size);
}
.ui-tree-select__trigger .quality-normal,
.ui-tree-select__item.quality-normal {
color: var(--dcalc-quality-normal);
}
.ui-tree-select__trigger .quality-advanced,
.ui-tree-select__item.quality-advanced {
color: var(--dcalc-quality-advanced);
}
.ui-tree-select__trigger .quality-rare,
.ui-tree-select__item.quality-rare {
color: var(--dcalc-quality-rare);
}
.ui-tree-select__trigger .quality-artifact,
.ui-tree-select__item.quality-artifact {
color: var(--dcalc-quality-artifact);
}
.ui-tree-select__trigger .quality-legend,
.ui-tree-select__item.quality-legend {
color: var(--dcalc-quality-legend);
}
.ui-tree-select__trigger .quality-epic,
.ui-tree-select__item.quality-epic {
color: var(--dcalc-quality-epic);
}
.ui-tree-select__trigger .quality-myth,
.ui-tree-select__item.quality-myth {
color: var(--dcalc-quality-myth) !important;
}
.ui-tree-select__trigger .quality-primeval,
.ui-tree-select__item.quality-primeval {
color: var(--dcalc-quality-primeval) !important;
}
.ui-tree-select__item {
box-sizing: border-box;
display: flex;
min-height: var(--ui-tree-item-block-size);
align-items: center;
gap: 6px;
padding: 0 7px 0 calc(7px + var(--ui-tree-depth) * var(--ui-tree-depth-step));
border: 1px solid var(--ui-tree-item-border);
outline: none;
background: var(--ui-tree-item-background);
box-shadow: var(--ui-tree-item-inset);
color: var(--ui-tree-item-foreground);
font-size: var(--ui-tree-item-font-size);
cursor: pointer;
&:hover {
border-color: var(--ui-tree-item-hover-border);
color: var(--ui-tree-item-hover-foreground);
filter: brightness(1.12);
}
&:active {
border-color: var(--ui-tree-item-active-border);
filter: brightness(0.86);
}
&:focus-visible {
outline: 1px solid var(--ui-ring);
outline-offset: -2px;
}
&[data-selected] {
border-color: var(--ui-tree-selected-border);
box-shadow: var(--ui-tree-selected-shadow);
}
&[data-disabled] {
cursor: not-allowed;
opacity: 0.45;
}
}
.ui-tree-select__branch {
position: relative;
display: inline-flex;
width: 13px;
height: 13px;
flex: 0 0 auto;
border: 1px solid var(--ui-tree-branch-border);
background: var(--ui-tree-branch-background);
box-shadow:
0 0 0 1px var(--ui-tree-branch-ring),
inset 0 1px 0 var(--ui-tree-branch-highlight);
&::before,
&::after {
position: absolute;
top: 50%;
left: 50%;
background: var(--ui-tree-branch-foreground);
box-shadow: 0 1px #000;
content: '';
transform: translate(-50%, -50%);
}
&::before {
width: 7px;
height: 1px;
}
&::after {
width: 1px;
height: 7px;
}
&.is-expanded::after {
display: none;
}
}
</style>
@@ -0,0 +1 @@
export { default as UiTreeSelect, type UiTreeSelectItem } from './UiTreeSelect.vue'
@@ -4,6 +4,7 @@ import { UiAutocomplete, type UiAutocompleteOption } from '@/components/ui/autoc
import { UiBadge } from '@/components/ui/badge'
import { buttonVariants, UiButton } from '@/components/ui/button'
import { UiCheckbox } from '@/components/ui/checkbox'
import { UiCascader, type UiCascaderGroup } from '@/components/ui/cascader'
import { UiDataRow, UiDataSection } from '@/components/ui/data-section'
import {
UiDataTable,
@@ -33,6 +34,8 @@ import { UiItemSlot } from '@/components/ui/item-slot'
import { UiNumberField } from '@/components/ui/number-field'
import { UiNavigation, type UiNavigationItem } from '@/components/ui/navigation'
import { UiPanel } from '@/components/ui/panel'
import { UiPagination } from '@/components/ui/pagination'
import { PopoverRoot, PopoverTrigger, UiPopoverContent } from '@/components/ui/popover'
import { UiProgress } from '@/components/ui/progress'
import { UiRadioGroup, UiRadioGroupItem } from '@/components/ui/radio-group'
import { UiRating } from '@/components/ui/rating'
@@ -49,6 +52,7 @@ import { UiSettingRow, UiSettingsGroup, UiSettingsSection } from '@/components/u
import { UiShopItemList, type UiShopItem } from '@/components/ui/shop-item-row'
import { UiSwitch } from '@/components/ui/switch'
import { TabsRoot, UiTabsContent, UiTabsList, UiTabsTrigger } from '@/components/ui/tabs'
import { UiTreeSelect, type UiTreeSelectItem } from '@/components/ui/tree-select'
import {
TooltipProvider,
TooltipRoot,
@@ -87,6 +91,9 @@ const dataSectionOpen = ref(true)
const shopSelected = ref('shop-rare')
const sortKey = ref('reinforce')
const sortDirection = ref<'ascending' | 'descending'>('ascending')
const treeValue = ref('rare')
const cascaderValue = ref(['celestial', 'silent-city'])
const currentPage = ref(1)
const progress = computed(() => Math.min(100, 18 + clickCount.value * 14))
@@ -115,6 +122,47 @@ const componentNames = [
'ScrollArea',
'Settings',
'ShopItemRow',
'TreeSelect',
'Cascader',
'Pagination',
'Popover',
]
const treeSelectItems: UiTreeSelectItem[] = [
{
value: 'rarity',
label: '所有稀有度',
children: [
{ value: 'normal', label: '普通', tone: 'normal' },
{ value: 'advanced', label: '高级', tone: 'advanced' },
{ value: 'rare', label: '稀有', tone: 'rare' },
{ value: 'artifact', label: '神器', tone: 'artifact' },
{ value: 'legend', label: '传说', tone: 'legend' },
{ value: 'epic', label: '史诗', tone: 'epic' },
{ value: 'myth', label: '神话', tone: 'myth' },
{ value: 'primeval', label: '太初', tone: 'primeval' },
],
},
]
const cascaderOptions: UiCascaderGroup[] = [
{
value: 'celestial',
label: '天界',
children: [
{ value: 'silent-city', label: '寂静城' },
{ value: 'noblesse-code', label: '贵族机要' },
{ value: 'meister-lab', label: '机械七战神实验室' },
],
},
{
value: 'seon',
label: '神界',
children: [
{ value: 'white-cloud', label: '白云溪谷' },
{ value: 'solidaris', label: '索利达里斯' },
],
},
]
const autocompleteOptions: UiAutocompleteOption[] = [
@@ -344,6 +392,57 @@ function handleSort(key: string) {
</UiPanel>
</div>
<div class="two-column adaptive-components-row">
<UiPanel
title="TreeSelect / Cascader"
note="Hierarchical value · collision-aware responsive popup"
>
<div class="adaptive-field-grid">
<label class="field-stack">
<span>TreeSelect · keyboard navigation</span>
<UiTreeSelect v-model="treeValue" :items="treeSelectItems" />
</label>
<label class="field-stack">
<span>Cascader · two-level filter</span>
<UiCascader v-model="cascaderValue" :options="cascaderOptions" />
</label>
</div>
<output class="adaptive-output">
{{ treeValue }} · {{ cascaderValue.join(' / ') }}
</output>
</UiPanel>
<UiPanel
title="Pagination / Popover / RichHelp"
note="Reka state · fluid overlay width · viewport collision"
>
<div class="adaptive-overlay-grid">
<UiPagination v-model:page="currentPage" :total="87" result-label="装备结果" />
<div class="adaptive-overlay-actions">
<PopoverRoot>
<PopoverTrigger as-child>
<UiButton variant="outline">打开属性说明</UiButton>
</PopoverTrigger>
<UiPopoverContent variant="rich" align="start">
<strong class="popover-title">属性计算说明</strong>
<p>最终结果会合并装备基础值、强化等级与当前筛选条件。</p>
<UiProgress :model-value="68" :marker="50" />
</UiPopoverContent>
</PopoverRoot>
<TooltipRoot>
<TooltipTrigger as-child>
<UiButton variant="outline">聚焦或悬停查看帮助</UiButton>
</TooltipTrigger>
<UiTooltipContent variant="rich">
<strong>高级帮助</strong
><br />支持键盘聚焦、碰撞翻转,并在窄屏内限制最大宽度。
</UiTooltipContent>
</TooltipRoot>
</div>
</div>
</UiPanel>
</div>
<div class="two-column controls-row">
<UiPanel
title="Checkbox / RadioGroup / Switch / Rate"
@@ -563,13 +662,11 @@ function handleSort(key: string) {
><UiButton variant="outline">打开组合设置窗口</UiButton></DialogTrigger
><DialogPortal
><UiDialogOverlay /><UiDialogContent class="complex-dialog-demo"
><UiDialogHeader
><UiDialogTitle>管理设置</UiDialogTitle>
><UiDialogHeader><UiDialogTitle>管理设置</UiDialogTitle></UiDialogHeader>
<div class="complex-dialog-body">
<UiDialogDescription
>组合展示已迁移的导航、设置行、滚动区与表单控件。</UiDialogDescription
></UiDialogHeader
>
<div class="complex-dialog-body">
>
<p>※ 直接组合迁移后的 Navigation、SettingsRow、ScrollArea 与表单控件。</p>
<div class="complex-dialog-main">
<UiNavigation
@@ -816,6 +913,37 @@ label > span,
background: #070807;
color: var(--ui-muted-foreground);
}
.adaptive-field-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: clamp(8px, 1.2vw, 14px);
}
.adaptive-output {
display: block;
min-width: 0;
margin-top: 10px;
overflow-wrap: anywhere;
color: var(--ui-muted-foreground);
}
.adaptive-overlay-grid {
display: grid;
gap: 10px;
}
.adaptive-overlay-actions {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
:global(.popover-title) {
display: block;
margin-bottom: 6px;
color: var(--ui-tooltip-strong);
font-weight: 400;
}
:global(.ui-popover-content p) {
margin: 0 0 10px;
color: var(--ui-muted-foreground);
}
.static-select {
margin-top: 8px;
border: 1px solid var(--ui-border);
@@ -904,7 +1032,7 @@ label > span,
color: var(--dcalc-quality-epic);
}
.quality-primeval {
color: #5aff62;
color: var(--dcalc-quality-primeval);
}
.status-success {
color: var(--dcalc-success);
@@ -1065,7 +1193,8 @@ footer {
font-size: 24px;
}
.field-grid,
.control-grid {
.control-grid,
.adaptive-field-grid {
grid-template-columns: 1fr;
}
.button-showcase {
+4 -4
View File
@@ -78,13 +78,13 @@ const accepted = ref(false)
<UiDialogContent>
<UiDialogHeader>
<UiDialogTitle>组合式弹窗</UiDialogTitle>
<UiDialogDescription>
Portal、Overlay、Content、Title 和 Close
均显式组合,业务逻辑不再隐藏在基础组件中。
</UiDialogDescription>
</UiDialogHeader>
<div class="border-y border-border py-4 text-xs text-foreground">
<UiDialogDescription class="mb-3">
Portal、Overlay、Content、Title 和 Close
均显式组合,业务逻辑不再隐藏在基础组件中。
</UiDialogDescription>
当前主题:{{ activeTheme }}
</div>
@@ -0,0 +1,39 @@
.ui-select-indicator {
box-sizing: border-box;
display: inline-flex;
width: var(--ui-select-icon-inline-size);
height: var(--ui-select-icon-block-size);
flex: 0 0 auto;
align-items: center;
justify-content: center;
border: 1px solid transparent;
background:
var(--ui-select-indicator-background) padding-box,
var(--ui-select-indicator-border) border-box;
box-shadow:
0 0 0 1px var(--ui-select-icon-shadow),
inset 0 1px 0 var(--ui-control-inner-highlight);
}
.ui-select-indicator__glyph {
width: var(--ui-select-glyph-inline-size);
height: var(--ui-select-glyph-block-size);
background: var(--ui-select-indicator-glyph);
clip-path: polygon(0 0, 100% 0, 50% 100%);
}
:where(.ui-select-trigger, .ui-tree-select__trigger):hover:not(:disabled) .ui-select-indicator,
:where(.ui-select-trigger, .ui-tree-select__trigger):focus-visible .ui-select-indicator,
:where(.ui-select-trigger, .ui-tree-select__trigger)[data-state='open'] .ui-select-indicator,
.ui-autocomplete__trigger:hover:not(:disabled),
.ui-autocomplete__trigger:focus-visible,
.ui-autocomplete__trigger[data-state='open'] {
background:
var(--ui-select-indicator-background-hover) padding-box,
var(--ui-select-indicator-border-hover) border-box;
}
:where(.ui-select-trigger, .ui-tree-select__trigger):active:not(:disabled) .ui-select-indicator,
.ui-autocomplete__trigger:active:not(:disabled) {
transform: translateY(1px);
}
+11 -2
View File
@@ -5,6 +5,7 @@
@import './themes/dcalc.css';
@import './themes/midnight.css';
@import './components/button.css';
@import './components/select-indicator.css';
@import 'tailwindcss/utilities.css' layer(utilities);
@source '../**/*.{vue,ts,tsx}';
@@ -52,13 +53,21 @@
@keyframes dialog-in {
from {
opacity: 0;
transform: translate(-50%, -48%) scale(0.98);
transform: translate(
calc(-50% + var(--ui-dialog-drag-x, 0px)),
calc(-48% + var(--ui-dialog-drag-y, 0px))
)
scale(0.98);
}
}
@keyframes dialog-out {
to {
opacity: 0;
transform: translate(-50%, -48%) scale(0.98);
transform: translate(
calc(-50% + var(--ui-dialog-drag-x, 0px)),
calc(-48% + var(--ui-dialog-drag-y, 0px))
)
scale(0.98);
}
}
+42 -1
View File
@@ -153,7 +153,12 @@
/* Reusable one-pixel control depth; kept separate from panel elevation. */
--ui-control-outer-shadow: rgb(0 0 0 / 40%);
--ui-control-inner-highlight: rgb(255 255 255 / 2.5%);
--ui-select-icon-shadow: rgb(0 0 0 / 60%); /* Outer pixel edge around the raster arrow button. */
--ui-select-icon-shadow: rgb(0 0 0 / 60%);
--ui-select-indicator-background: linear-gradient(180deg, #1f355e, #1a2849);
--ui-select-indicator-background-hover: linear-gradient(180deg, #29457f, #24386a);
--ui-select-indicator-border: linear-gradient(180deg, #684f2d, #483423);
--ui-select-indicator-border-hover: linear-gradient(180deg, #725f4e, #524444);
--ui-select-indicator-glyph: #b69260;
--ui-check-outer-shadow: rgb(0 0 0 / 30%);
--ui-check-mark-shadow: rgb(230 192 86 / 45%);
/* NumberField spinner and the source-derived +/-10 raster-like surfaces. */
@@ -198,6 +203,42 @@
--ui-tooltip-background: #020302;
--ui-tooltip-shadow: #000;
--ui-tooltip-foreground: #f5f5ef;
--ui-tooltip-strong: #e8c968;
--ui-popover-border: #5a4828;
--ui-popover-background: #020302;
--ui-popover-shadow: #000;
--ui-pagination-border: #514125;
--ui-pagination-background: #101211;
--ui-pagination-active-background: #1b315c;
--ui-pagination-active-foreground: #e3c977;
--ui-tree-background: #010201;
--ui-tree-item-border: #0d0f0d;
--ui-tree-item-background: linear-gradient(180deg, #2c2a2c 0, #202220 16%, #0c0e0c 100%);
--ui-tree-item-inset:
inset 0 1px 0 #3e3c3e, inset 1px 0 0 #252725, inset -1px 0 0 #111311, inset 0 -1px 0 #111311;
--ui-tree-item-hover-border: #4f4b3c;
--ui-tree-item-foreground: #d6c487;
--ui-tree-item-hover-foreground: #f2da85;
--ui-tree-item-active-border: #806329;
--ui-tree-selected-border: #efb62f;
--ui-tree-selected-shadow:
inset 0 0 0 1px #7c5d1d, inset 0 1px 0 #f1c75f, 0 0 4px rgb(239 182 47 / 48%);
--ui-tree-branch-border: #5f4a2b;
--ui-tree-branch-background: linear-gradient(180deg, #272720, #11120f);
--ui-tree-branch-foreground: #a98b46;
--ui-tree-branch-ring: #090a09;
--ui-tree-branch-highlight: rgb(255 255 255 / 4%);
--ui-cascader-underline: #583916;
--ui-cascader-shell-background: #030403;
--ui-cascader-trigger-border: #332a1b;
--ui-cascader-trigger-background: linear-gradient(180deg, #262721, #11120f);
--ui-cascader-trigger-foreground: #c3a15e;
--ui-cascader-trigger-hover-border: #756132;
--ui-cascader-trigger-hover-background: linear-gradient(180deg, #403a28, #191a15);
--ui-cascader-trigger-hover-foreground: #fff1a4;
--ui-cascader-trigger-highlight: #a89a61;
--ui-cascader-popup-border: #432b14;
--ui-cascader-option-highlight: #09284b;
--ui-popup-border: #5a4828;
--ui-popup-background: #050606;
--ui-popup-shadow: #000;
+42 -1
View File
@@ -134,7 +134,12 @@
--ui-slider-disabled-thumb-bottom: #29313d;
--ui-control-outer-shadow: rgb(0 0 0 / 42%);
--ui-control-inner-highlight: rgb(255 255 255 / 4%);
--ui-select-icon-shadow: rgb(0 0 0 / 60%); /* Keeps the arrow button legible on blue fields. */
--ui-select-icon-shadow: rgb(0 0 0 / 60%);
--ui-select-indicator-background: linear-gradient(180deg, #294d80, #172b4b);
--ui-select-indicator-background-hover: linear-gradient(180deg, #376aa7, #1e3b64);
--ui-select-indicator-border: linear-gradient(180deg, #526f98, #354b6e);
--ui-select-indicator-border-hover: linear-gradient(180deg, #9ac8ef, #658db8);
--ui-select-indicator-glyph: #9ac8ef;
--ui-check-outer-shadow: rgb(0 0 0 / 34%);
--ui-check-mark-shadow: rgb(111 181 230 / 42%);
--ui-number-spinner-divider: #40577a;
@@ -205,6 +210,42 @@
--ui-tooltip-background: #080d16;
--ui-tooltip-shadow: #020611;
--ui-tooltip-foreground: #edf5ff;
--ui-tooltip-strong: #9ac8ef;
--ui-popover-border: #526f98;
--ui-popover-background: #080d16;
--ui-popover-shadow: #020611;
--ui-pagination-border: #40577a;
--ui-pagination-background: #101a29;
--ui-pagination-active-background: #315c97;
--ui-pagination-active-foreground: #edf5ff;
--ui-tree-background: #080d16;
--ui-tree-item-border: #17243a;
--ui-tree-item-background: linear-gradient(180deg, #253650, #16243a 18%, #0b111d 100%);
--ui-tree-item-inset:
inset 0 1px 0 #40577a, inset 1px 0 0 #263b57, inset -1px 0 0 #101722, inset 0 -1px 0 #101722;
--ui-tree-item-hover-border: #6e91ba;
--ui-tree-item-foreground: #a9c9e7;
--ui-tree-item-hover-foreground: #edf5ff;
--ui-tree-item-active-border: #315c97;
--ui-tree-selected-border: #8ebce4;
--ui-tree-selected-shadow:
inset 0 0 0 1px #315c97, inset 0 1px 0 #b6d9f4, 0 0 4px rgb(111 181 230 / 48%);
--ui-tree-branch-border: #40577a;
--ui-tree-branch-background: linear-gradient(180deg, #263b57, #101722);
--ui-tree-branch-foreground: #9ac8ef;
--ui-tree-branch-ring: #050914;
--ui-tree-branch-highlight: rgb(255 255 255 / 4%);
--ui-cascader-underline: #263b57;
--ui-cascader-shell-background: #080d16;
--ui-cascader-trigger-border: #314968;
--ui-cascader-trigger-background: linear-gradient(180deg, #253650, #101722);
--ui-cascader-trigger-foreground: #a9c9e7;
--ui-cascader-trigger-hover-border: #6e91ba;
--ui-cascader-trigger-hover-background: linear-gradient(180deg, #385579, #17243a);
--ui-cascader-trigger-hover-foreground: #ffffff;
--ui-cascader-trigger-highlight: #8ebce4;
--ui-cascader-popup-border: #40577a;
--ui-cascader-option-highlight: #183d68;
--ui-popup-border: #40577a;
--ui-popup-background: #0c1220;
--ui-popup-shadow: #020611;
@@ -32,6 +32,8 @@
--dcalc-quality-artifact: #ef28c1;
--dcalc-quality-legend: #ff7700;
--dcalc-quality-epic: #fcb600;
--dcalc-quality-myth: #f8c63d;
--dcalc-quality-primeval: #5aff62;
--dcalc-quality-chronicle: #ff6666;
/*
@@ -80,6 +82,14 @@
--ui-slider-value-min-inline-size: 40px;
--ui-progress-block-size: 14px;
--ui-tooltip-max-inline-size: 260px;
--ui-tooltip-rich-inline-size: 310px;
--ui-popover-inline-size: clamp(240px, 32vw, 420px);
--ui-popover-rich-inline-size: clamp(270px, 36vw, 480px);
--ui-popover-padding: 10px;
--ui-pagination-block-size: 34px;
--ui-pagination-gap: 8px;
--ui-pagination-button-inline-size: 27px;
--ui-pagination-button-block-size: 25px;
--ui-menu-min-inline-size: 190px;
--ui-menu-item-block-size: 28px;
--ui-select-icon-inline-size: 21px;
@@ -111,6 +121,15 @@
--ui-autocomplete-max-block-size: 240px;
--ui-autocomplete-trigger-gap: 4px;
--ui-autocomplete-indicator-font-size: 8px;
--ui-tree-select-min-inline-size: 230px;
--ui-tree-select-max-block-size: 280px;
--ui-tree-item-block-size: 24px;
--ui-tree-item-font-size: 16px;
--ui-tree-depth-step: 14px;
--ui-cascader-primary-ratio: 39%;
--ui-cascader-popup-inline-size: clamp(280px, 42vw, 480px);
--ui-cascader-popup-max-block-size: 280px;
--ui-cascader-option-block-size: 24px;
--ui-navigation-inline-size: 250px;
--ui-navigation-row-block-size: 48px;
--ui-navigation-icon-size: 24px;