init
This commit is contained in:
@@ -39,8 +39,10 @@ interactions. `/ui-preview` remains the smaller foundation smoke-test page.
|
||||
See [`docs/ui-migration-standard.md`](docs/ui-migration-standard.md) for the reusable component
|
||||
migration, theming, responsive implementation and browser QA standard.
|
||||
|
||||
The current catalog covers Button, Badge, Panel, Input, NumberField, Checkbox, RadioGroup, Switch,
|
||||
Rating, Select, Slider, Progress, Tabs, ItemSlot, DataTable, Tooltip, DropdownMenu and Dialog.
|
||||
The current catalog covers 28 component families: Button, Badge, Panel, Input, NumberField,
|
||||
Checkbox, RadioGroup, Switch, Rating, Select, Slider, Progress, Tabs, ItemSlot, DataTable, Tooltip,
|
||||
DropdownMenu, Dialog, Autocomplete, Navigation, DataSection, ScrollArea, Settings, ShopItemRow,
|
||||
TreeSelect, Cascader, Pagination and Popover.
|
||||
|
||||
## Migration rule
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ The implementation keeps the source board's full-width dark surface, 46 px panel
|
||||
- NumberField: 30 px field, 23 px spinner, visible up/down arrows, and two 14 px `+10`/`-10` fast-step buttons match the source anatomy.
|
||||
- Tabs: 40 px list, 30 px triggers, glow layers, divider line, animated 39 × 5 px arrow marker, and line-marker variant are present. Marker movement follows Reka selection state and respects reduced motion.
|
||||
- Dialog: the titlebar uses the exact 45 raster frames from `windowheadereffect.img`, advances every 66 ms while focused, and hides the effect in the unfocused/reduced-motion state.
|
||||
- TreeSelect: the Reka tree now opens the selected path, uses the source's flush 2 px row rhythm,
|
||||
eight rarity colors, pixel branch glyph, selected border, and distinct hover/active/focus-visible
|
||||
states. Desktop and 390 px evidence is captured at `tree-select-fixed-1440.png` and
|
||||
`tree-select-fixed-390.png`.
|
||||
- Responsive: document overflow is absent at 1920, 1280, 768, and 390 widths. At 390 px, DataTable keeps a 328 px viewport with an intentional 560 px internal scroll surface.
|
||||
|
||||
## Required fidelity surfaces
|
||||
@@ -25,7 +29,10 @@ The implementation keeps the source board's full-width dark surface, 46 px panel
|
||||
- Fonts and typography: DCalc/SimSun control typography and Segoe UI/Chinese fallbacks retain the source hierarchy and compact line-height.
|
||||
- Spacing and layout rhythm: page gutters use `clamp()`, panels use container-relative padding, grids collapse at 1280/900/520 breakpoints, and atomic game-control sizes stay pixel-aligned.
|
||||
- Colors and tokens: blue action surfaces, brass edges, gold selection, blue focus, disabled gray, semantic status colors, and rarity colors map to DCalc CSS variables.
|
||||
- Image quality and assets: stars, rarity frames, item icons, select arrow, dialog close icon, and all 45 title-effect frames use original raster assets with zero broken images.
|
||||
- Image quality and assets: stars, rarity frames, item icons, dialog close icon, and all 45
|
||||
title-effect frames use original raster assets with zero broken images. Select, TreeSelect and
|
||||
Autocomplete use the Design source's CSS gradient button and clipped arrow instead of a raster
|
||||
arrow.
|
||||
- Copy and content: catalog copy differs only where it documents the Reka implementation or exposes live state; component labels and source examples remain representative.
|
||||
|
||||
## Interaction verification
|
||||
@@ -38,12 +45,32 @@ The implementation keeps the source board's full-width dark surface, 46 px panel
|
||||
- Select: 装备 → 材料.
|
||||
- DropdownMenu: `导出结果` updated live status.
|
||||
- Dialog: title frame changed from `4.PNG` to `5.PNG`; cancel restored focus to `打开 WindowDialog`.
|
||||
- Dialog drag: the standard window moved by `120 × 66 px`; the complex window moved by
|
||||
`-100 × 39 px`, while both title center deltas remained `0 px`.
|
||||
- Dialog responsive boundary: at 390 px, reopening reset the desktop offset and dragging to the
|
||||
upper-left stopped at the 8 px viewport margin.
|
||||
- TreeSelect parent toggle: collapsing and reopening `所有稀有度` preserved the `稀有` leaf value;
|
||||
the parent never received `data-selected`.
|
||||
- TreeSelect keyboard: ArrowDown moved focus to `稀有` with a solid 1 px blue outline at -2 px
|
||||
offset, without applying the hover brightness filter.
|
||||
- TreeSelect responsive boundary: at 390 px the 319 px popup remained between x=30 and x=349,
|
||||
rendered all nine rows, and retained the selected leaf.
|
||||
|
||||
## Comparison history
|
||||
|
||||
- P1: NumberField step affordances were too small and FastStep was missing. Fixed with source-sized arrows and `+10`/`-10` controls.
|
||||
- P1: Tabs used a trigger-local pseudo marker and could not reproduce source movement. Fixed with a list-owned marker driven by active Reka tab geometry.
|
||||
- P1: Dialog titlebar had a static gradient only. Fixed with the exact focused 45-frame animation.
|
||||
- P1: Dialog had no draggable window behavior and long header descriptions could offset the title.
|
||||
Fixed with a bounded Pointer Events drag model, open/resize recentering, and title-only centered
|
||||
header composition.
|
||||
- P1: TreeSelect inherited native `ul` margin and padding, so rows were inset and narrowed; Portal
|
||||
content also lost scoped rarity colors, and parent clicks replaced the selected leaf. Fixed with
|
||||
a reset tree root, global rarity semantics, selected-path expansion, and prevented parent select
|
||||
events while retaining Reka expansion.
|
||||
- P2: Select, TreeSelect and Autocomplete imported the shared arrow PNG even though the Design
|
||||
control anatomy is CSS-rendered. Replaced all three with one theme-aware CSS indicator contract;
|
||||
retained raster assets only where the source treats them as game artwork.
|
||||
- P2: Button showcase omitted source state rows. Fixed with the complete three-row state matrix.
|
||||
- P2: shadcn-like radii, generic focus rings, menu surface, and tooltip transitions drifted from DNF styling. Replaced with source-specific square surfaces and state treatments.
|
||||
- P2: layout relied on fixed page spacing. Replaced page-level spacing and grids with fluid/container-responsive rules while preserving atomic raster dimensions.
|
||||
@@ -51,6 +78,10 @@ The implementation keeps the source board's full-width dark surface, 46 px panel
|
||||
## Remaining intentional deviations
|
||||
|
||||
- The catalog header includes theme switching and a component index that are not present on Design board 2.
|
||||
- Components not yet migrated (CommandInput, TreeSelect, Cascader, navigation, ScrollArea, and complex product dialogs) are out of this comparison and must be covered in later migration batches.
|
||||
- The cited full-page 1920 px comparison predates Cascader, Pagination and Popover. TreeSelect has
|
||||
since received a focused Design/source-CSS comparison and browser verification; the other three
|
||||
still require refreshed design-side screenshots before claiming pixel-level comparison evidence.
|
||||
- SearchTree/CommandInput and domain-specific complex product dialogs remain outside this comparison.
|
||||
- Navigation and ScrollArea are migrated; business call-site replacement remains a separate phase.
|
||||
|
||||
final result: passed
|
||||
|
||||
@@ -15,26 +15,27 @@ mean recreating the effect from screenshots.
|
||||
|
||||
## Matrix
|
||||
|
||||
| Design source | Current target | Status | Migration notes |
|
||||
| ----------------------- | --------------------------------------------------------- | ----------- | -------------------------------------------------------- |
|
||||
| `foundations`, `shared` | `style/tokens`, `style/themes`, shared UI contract | Migrated | Semantic color and overridable geometry contracts |
|
||||
| `button` | `ui/button` | Migrated | Preserved three-slice states; exposed density and width |
|
||||
| `input` | `ui/input`, `ui/number-field` | Migrated | Removed forced inline width; normalized text metrics |
|
||||
| `form-controls` | `ui/checkbox`, `ui/radio-group`, `ui/switch`, `ui/rating` | Migrated | Tokenized state colors and aligned label/control anatomy |
|
||||
| `select` | `ui/select` | Migrated | Preserved popup anatomy and Reka keyboard behavior |
|
||||
| `tabs` | `ui/tabs` | Migrated | Preserved list-owned animated marker |
|
||||
| `slider` | `ui/slider` | Migrated | Preserved the Reka coordinate system |
|
||||
| `data-table` | `ui/data-table` | Migrated | Ported hover, selected and density behavior |
|
||||
| `dialog` | `ui/dialog` | Migrated | Preserved the focused title effect and focus management |
|
||||
| `menu` | `ui/dropdown-menu` | Migrated | Ported source menu spacing and state tokens |
|
||||
| `rarity` | `ui/item-slot` | Migrated | Retained rarity presentation as data-semantic tokens |
|
||||
| `autocomplete` | `ui/autocomplete` | Migrated | Reka Combobox composition with source popup anatomy |
|
||||
| `navigation` | `ui/navigation` | Migrated | Selectable navigation primitive with source assets |
|
||||
| `scrollbar` | `ui/scroll-area` | Migrated | Reusable Reka viewport/scrollbar composition |
|
||||
| `data-section` | `ui/data-section` | Migrated | Collapsible section and vertically centered data rows |
|
||||
| `shop-item-row` | `ui/shop-item-row` | Migrated | Ported source row structure and selectable states |
|
||||
| `complex-dialog` | `ui/dialog` + migrated primitives | Composition | Kept domain-independent by composing existing primitives |
|
||||
| `settings` | `ui/settings` | Migrated | Responsive settings group, section and row primitives |
|
||||
| Design source | Current target | Status | Migration notes |
|
||||
| ----------------------- | --------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| `foundations`, `shared` | `style/tokens`, `style/themes`, shared UI contract | Migrated | Semantic color and overridable geometry contracts |
|
||||
| `button` | `ui/button` | Migrated | Preserved three-slice states; exposed density and width |
|
||||
| `input` | `ui/input`, `ui/number-field` | Migrated | Removed forced inline width; normalized text metrics |
|
||||
| `form-controls` | `ui/checkbox`, `ui/radio-group`, `ui/switch`, `ui/rating` | Migrated | Tokenized state colors and aligned label/control anatomy |
|
||||
| `select` | `ui/select`, `ui/tree-select`, `ui/cascader` | Migrated | Flat, hierarchical and two-level responsive selection; TreeSelect preserves parent-toggle and rarity states |
|
||||
| `tabs` | `ui/tabs`, `ui/pagination` | Migrated | Animated list marker and compact result navigation |
|
||||
| `slider` | `ui/slider` | Migrated | Preserved the Reka coordinate system |
|
||||
| `data-table` | `ui/data-table` | Migrated | Ported hover, selected and density behavior |
|
||||
| `dialog` | `ui/dialog` | Migrated | Preserved the focused title effect and focus management |
|
||||
| `menu` | `ui/dropdown-menu` | Migrated | Ported source menu spacing and state tokens |
|
||||
| `rarity` | `ui/item-slot` | Migrated | Retained rarity presentation as data-semantic tokens |
|
||||
| `autocomplete` | `ui/autocomplete` | Partial | Combobox is migrated; domain SearchTree remains deferred |
|
||||
| `navigation` | `ui/navigation` | Migrated | Selectable navigation primitive with source assets |
|
||||
| `scrollbar` | `ui/scroll-area` | Migrated | Reusable Reka viewport/scrollbar composition |
|
||||
| shared overlay help | `ui/popover`, rich `ui/tooltip` | Migrated | Portal z-index, collision and responsive width contract |
|
||||
| `data-section` | `ui/data-section` | Migrated | Collapsible section and vertically centered data rows |
|
||||
| `shop-item-row` | `ui/shop-item-row` | Migrated | Ported source row structure and selectable states |
|
||||
| `complex-dialog` | `ui/dialog` + migrated primitives | Composition | Kept domain-independent by composing existing primitives |
|
||||
| `settings` | `ui/settings` | Migrated | Responsive settings group, section and row primitives |
|
||||
|
||||
## Legacy replacement order
|
||||
|
||||
@@ -45,3 +46,12 @@ Deferred: this component-library pass does not change business call sites.
|
||||
3. Equipment dictionary filters, tree, cascader and pagination.
|
||||
4. Skill tree, skill detail and VP controls.
|
||||
5. Result tables and remaining domain-specific components.
|
||||
|
||||
## Remaining gaps
|
||||
|
||||
- SearchTree/CommandInput still requires a domain-neutral data and filtering contract before it can
|
||||
join `src/components/ui`.
|
||||
- Complex product dialogs are compositions, not a claim that every business dialog has reached
|
||||
source-level visual parity.
|
||||
- Existing `frontend` business call sites remain on their current components until each behavior
|
||||
slice is migrated and browser-verified.
|
||||
|
||||
@@ -99,6 +99,8 @@ Token 分成两层:
|
||||
- 建立完整状态矩阵:rest、hover、pressed、focus、disabled、selected/open。
|
||||
- 动效需记录帧数、间隔、缓动、焦点条件和 reduced-motion 行为。
|
||||
- 优先复用原始 PNG、字体、图标和 rarity frame,不用近似图标替代。
|
||||
- 先核对 Design 的真实实现方式:源组件使用 CSS 渐变、伪元素或 `clip-path` 构造的
|
||||
控制图标继续使用 CSS;只有被 Design 明确作为像素素材引用的图标才引入图片。
|
||||
|
||||
### 6.3 选择 Reka Primitive
|
||||
|
||||
@@ -177,6 +179,10 @@ Reka 的 Select、Tooltip、DropdownMenu、Popover 等通常经过 Portal 和 Po
|
||||
- 使用确定的数字顺序加载帧,避免字符串排序造成 `10` 排在 `2` 前。
|
||||
- 仅在 focused 状态推进和展示动效;unfocused 状态使用静态标题栏。
|
||||
- 组件卸载时清理计时器,reduced-motion 下隐藏或静态化动效。
|
||||
- 可拖动窗口仅以标题栏空白区域作为手柄,关闭按钮和标题栏操作控件不得触发拖动。
|
||||
- 使用 Pointer Events 同时覆盖鼠标和触摸;拖动范围限制在当前视口安全边距内。
|
||||
- 每次重新打开或视口尺寸变化时恢复几何居中,避免桌面偏移泄漏到移动端。
|
||||
- 标题使用对称网格独立居中,长 Description 放入内容区,不参与标题栏宽度计算。
|
||||
|
||||
### 7.5 NumberField 扩展操作
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user