This commit is contained in:
2026-08-19 19:45:04 +08:00
parent 4de893ca94
commit cd24d68481
37 changed files with 1937 additions and 746 deletions
@@ -47,6 +47,6 @@ const props = withDefaults(
}
.ui-data-row.is-disabled {
color: var(--ui-control-disabled-foreground);
color: var(--ui-unavailable-foreground);
}
</style>
@@ -12,8 +12,17 @@ const props = withDefaults(
disabled?: boolean
expandedLabel?: string
collapsedLabel?: string
compact?: boolean
showState?: boolean
indicatorPosition?: 'start' | 'end'
}>(),
{ expandedLabel: '已展开', collapsedLabel: '未展开' },
{
expandedLabel: '已展开',
collapsedLabel: '未展开',
compact: false,
showState: true,
indicatorPosition: 'start',
},
)
const open = defineModel<boolean>('open', { default: true })
@@ -23,12 +32,21 @@ const open = defineModel<boolean>('open', { default: true })
<CollapsibleRoot
v-model:open="open"
:disabled="disabled"
:class="cn('ui-data-section', props.class)"
:class="
cn(
'ui-data-section',
compact && 'ui-data-section--compact',
indicatorPosition === 'end' && 'ui-data-section--indicator-end',
props.class,
)
"
>
<CollapsibleTrigger class="ui-data-section__trigger">
<img :src="open ? expandedIcon : collapsedIcon" alt="" aria-hidden="true" />
<span class="ui-data-section__title">{{ title }}</span>
<span class="ui-data-section__state">{{ open ? expandedLabel : collapsedLabel }}</span>
<span v-if="showState" class="ui-data-section__state">
{{ open ? expandedLabel : collapsedLabel }}
</span>
</CollapsibleTrigger>
<CollapsibleContent class="ui-data-section__content">
<slot />
@@ -99,4 +117,12 @@ const open = defineModel<boolean>('open', { default: true })
.ui-data-section__content {
padding: var(--ui-disclosure-content-padding-block) var(--ui-field-padding-inline);
}
.ui-data-section--compact .ui-data-section__trigger {
min-height: var(--ui-control-height-compact);
}
.ui-data-section--indicator-end .ui-data-section__trigger > img {
order: 3;
}
</style>
@@ -207,4 +207,11 @@ function fastChange(direction: 1 | -1) {
.ui-number-field__fast-step button + button span {
top: 2px;
}
@media (max-width: 520px) {
.ui-number-field {
height: 50px;
grid-template-columns: minmax(0, 1fr) 25px;
}
}
</style>
@@ -66,7 +66,7 @@ const page = defineModel<number>('page', { default: 1 })
border: 1px solid var(--ui-pagination-border);
outline: none;
background: var(--ui-pagination-background);
color: var(--ui-control-disabled-foreground);
color: var(--ui-muted-foreground);
cursor: pointer;
}
@@ -82,6 +82,7 @@ const page = defineModel<number>('page', { default: 1 })
}
.ui-pagination__button:disabled {
color: var(--ui-control-disabled-foreground);
cursor: not-allowed;
opacity: 0.45;
}
@@ -31,7 +31,10 @@ const visibleMarkers = computed(() => {
<template>
<ProgressRoot v-bind="delegatedProps" :class="cn('ui-progress', props.class)">
<ProgressIndicator class="ui-progress__indicator" :style="{ width: `${percentage}%` }" />
<ProgressIndicator
class="ui-progress__indicator"
:style="{ transform: `scaleX(${percentage / 100})` }"
/>
<span
v-for="milestone in visibleMarkers"
:key="milestone"
@@ -52,10 +55,12 @@ const visibleMarkers = computed(() => {
}
.ui-progress__indicator {
width: 100%;
height: 100%;
background: var(--ui-progress-fill);
box-shadow: inset 0 0 0 1px var(--ui-progress-fill-inset);
transition: width 180ms ease-out;
transform-origin: left center;
transition: transform 180ms ease-out;
}
.ui-progress__marker {
@@ -68,4 +73,10 @@ const visibleMarkers = computed(() => {
border-left: 4px solid transparent;
transform: translateX(-50%);
}
@media (prefers-reduced-motion: reduce) {
.ui-progress__indicator {
transition: none;
}
}
</style>
@@ -26,7 +26,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits)
<template>
<RatingRoot v-bind="forwarded" :class="cn('ui-rating', props.class)" :aria-label="props.label">
<RatingItem v-for="item in props.length" :key="item" :item="item" class="ui-rating__item">
<RatingItem
v-for="item in props.length"
:key="item"
:item="item"
class="ui-rating__item"
:aria-label="`${props.label}:${item} / ${props.length}`"
>
<img :src="emptyStar" alt="" />
<RatingItemIndicator :step="item" class="ui-rating__indicator">
<img :src="activeStar" alt="" />
@@ -31,7 +31,7 @@ function selectRow(row: UiShopItem) {
</script>
<template>
<div :class="cn('ui-shop-list', props.class)" role="listbox" aria-label="物品列表">
<div :class="cn('ui-shop-list', props.class)" role="grid" aria-label="物品列表">
<div class="ui-shop-list__surface">
<div
v-for="(row, index) in rows"
@@ -42,7 +42,7 @@ function selectRow(row: UiShopItem) {
'is-selected': selected === row.id,
'is-disabled': row.disabled,
}"
role="option"
role="row"
:aria-selected="selected === row.id"
:aria-disabled="row.disabled || undefined"
:tabindex="row.disabled ? -1 : 0"
@@ -51,11 +51,11 @@ function selectRow(row: UiShopItem) {
@keydown.space.prevent="selectRow(row)"
>
<i class="ui-shop-row__frame" aria-hidden="true" />
<div class="ui-shop-row__icon"><img :src="row.image" alt="" /></div>
<div>{{ row.reinforce }}</div>
<div>{{ row.amplify }}</div>
<div :class="row.rarity && `quality-${row.rarity}`">{{ row.name }}</div>
<div>
<div class="ui-shop-row__icon" role="gridcell"><img :src="row.image" alt="" /></div>
<div role="gridcell">{{ row.reinforce }}</div>
<div role="gridcell">{{ row.amplify }}</div>
<div role="gridcell" :class="row.rarity && `quality-${row.rarity}`">{{ row.name }}</div>
<div role="gridcell">
<button
type="button"
class="ui-shop-row__inspect"
@@ -65,8 +65,10 @@ function selectRow(row: UiShopItem) {
<img :src="inspectIcon" alt="" />
</button>
</div>
<div :class="row.statusTone && `status-${row.statusTone}`">{{ row.status }}</div>
<div class="ui-shop-row__owner">{{ row.owner }}</div>
<div role="gridcell" :class="row.statusTone && `status-${row.statusTone}`">
{{ row.status }}
</div>
<div class="ui-shop-row__owner" role="gridcell">{{ row.owner }}</div>
</div>
</div>
</div>
@@ -305,6 +307,6 @@ function selectRow(row: UiShopItem) {
color: var(--ui-success);
}
.status-danger {
color: var(--ui-destructive);
color: var(--ui-danger-foreground);
}
</style>
@@ -12,7 +12,7 @@ import { cn } from '@/lib/utils'
const props = withDefaults(
defineProps<TabsListProps & { class?: HTMLAttributes['class']; marker?: 'arrow' | 'line' }>(),
{ marker: 'arrow' },
{ marker: 'line' },
)
const delegatedProps = computed(() => {
const { class: _class, marker: _marker, ...delegated } = props
@@ -22,6 +22,12 @@ const delegatedProps = computed(() => {
const listElement = ref<HTMLElement | ComponentPublicInstance>()
const markerLeft = ref('0px')
const markerReady = ref(false)
const itemWidth = ref('auto')
const groupWidth = ref('max-content')
const geometryStyle = computed(() => ({
'--ui-tab-item-width': itemWidth.value,
'--ui-tabs-group-width': groupWidth.value,
}))
let resizeObserver: ResizeObserver | undefined
let mutationObserver: MutationObserver | undefined
@@ -42,19 +48,60 @@ function positionMarker(animate = true) {
requestAnimationFrame(() => (markerReady.value = true))
}
function equalizeItems() {
const list = getListElement()
const triggers = list ? [...list.querySelectorAll<HTMLElement>('[role="tab"]')] : []
if (!list || !triggers.length) return
const labels = triggers.map(
(trigger) => trigger.querySelector<HTMLElement>('.ui-tabs-trigger__label') ?? trigger,
)
const widestLabel = Math.max(
0,
...labels.map((label) => Math.max(label.scrollWidth, label.getBoundingClientRect().width)),
)
const listStyle = getComputedStyle(list)
const triggerStyle = getComputedStyle(triggers[0])
const groupPadding =
Number.parseFloat(listStyle.paddingLeft) + Number.parseFloat(listStyle.paddingRight)
const itemPadding =
Number.parseFloat(triggerStyle.paddingLeft) + Number.parseFloat(triggerStyle.paddingRight)
const parentWidth = list.parentElement?.clientWidth ?? list.clientWidth
const availableWidth = Math.max(0, parentWidth - groupPadding)
if (!availableWidth) return
const equalizedItemWidth = Math.min(
Math.ceil(widestLabel + itemPadding),
availableWidth / triggers.length,
)
itemWidth.value = `${equalizedItemWidth}px`
groupWidth.value = `${equalizedItemWidth * triggers.length + groupPadding}px`
}
async function updateGeometry(animate = true) {
equalizeItems()
await nextTick()
positionMarker(animate)
}
onMounted(async () => {
await nextTick()
positionMarker(false)
document.fonts?.ready.then(() => positionMarker(false))
resizeObserver = new ResizeObserver(() => positionMarker(false))
await updateGeometry(false)
document.fonts?.ready.then(() => updateGeometry(false))
resizeObserver = new ResizeObserver(() => updateGeometry(false))
const list = getListElement()
if (list) resizeObserver.observe(list)
mutationObserver = new MutationObserver(() => positionMarker())
if (list) {
resizeObserver.observe(list)
if (list.parentElement) resizeObserver.observe(list.parentElement)
}
mutationObserver = new MutationObserver(() => updateGeometry())
if (list) {
mutationObserver.observe(list, {
subtree: true,
attributes: true,
attributeFilter: ['data-state'],
childList: true,
characterData: true,
})
}
})
@@ -66,7 +113,12 @@ onBeforeUnmount(() => {
</script>
<template>
<TabsList ref="listElement" v-bind="delegatedProps" :class="cn('ui-tabs-list', props.class)">
<TabsList
ref="listElement"
v-bind="delegatedProps"
:class="cn('ui-tabs-list', props.class)"
:style="geometryStyle"
>
<slot />
<span
aria-hidden="true"
@@ -84,19 +136,22 @@ onBeforeUnmount(() => {
.ui-tabs-list {
position: relative;
display: flex;
width: 100%;
box-sizing: border-box;
width: var(--ui-tabs-group-width, max-content);
max-width: 100%;
height: var(--ui-tabs-height);
align-items: center;
padding: 5px 7px;
justify-content: flex-start;
padding: 4px 6px;
background: var(--ui-tab-list-background);
}
.ui-tabs-list::after {
content: '';
position: absolute;
right: 7px;
top: 35px;
left: 7px;
right: 6px;
top: 28px;
left: 6px;
height: 1px;
background: var(--ui-tab-divider);
}
@@ -23,18 +23,20 @@ const delegatedProps = computed(() => {
position: relative;
z-index: 1;
display: inline-flex;
box-sizing: border-box;
width: var(--ui-tab-item-width, auto);
height: var(--ui-tab-height);
align-items: center;
justify-content: center;
min-width: 0;
flex: 1 1 0;
padding: 0 12px;
flex: 0 0 var(--ui-tab-item-width, auto);
padding: 0 10px;
border: 0;
outline: none;
background: var(--ui-tab-background);
color: var(--ui-tab-foreground);
font:
16px / var(--ui-control-line-height) 'DCalc',
14px / var(--ui-control-line-height) 'DCalc',
SimSun,
sans-serif;
text-shadow: 1px 1px 0 var(--ui-dialog-titlebar-text-shadow);
@@ -44,10 +46,10 @@ const delegatedProps = computed(() => {
.ui-tabs-trigger:not(:last-of-type)::before {
content: '';
position: absolute;
top: 5px;
top: 4px;
right: 0;
width: 1px;
height: 19px;
height: 15px;
background: linear-gradient(180deg, transparent, var(--ui-tab-separator) 22% 78%, transparent);
}
@@ -72,7 +74,11 @@ const delegatedProps = computed(() => {
position: relative;
z-index: 1;
display: inline-block;
max-width: 100%;
overflow: hidden;
line-height: var(--ui-control-line-height);
text-overflow: ellipsis;
white-space: nowrap;
transform: translateY(var(--ui-control-text-offset-y));
}
</style>