diff --git a/frontend-ui/docs/design-migration-matrix.md b/frontend-ui/docs/design-migration-matrix.md new file mode 100644 index 00000000..c678b893 --- /dev/null +++ b/frontend-ui/docs/design-migration-matrix.md @@ -0,0 +1,47 @@ +# Design migration matrix + +`design/src-web` is the implementation source for component structure, CSS, assets and visual +interaction states. Migration means adapting that implementation to Vue and Reka UI; it does not +mean recreating the effect from screenshots. + +## Rules + +- Port the existing DOM anatomy, CSS values and raster assets before optimizing them. +- Keep pixel-accurate atomic dimensions, but expose component width and density through props or + CSS custom properties. +- Move reusable colors and metrics into semantic `--ui-*` tokens after visual parity is established. +- Keep page data, routing and stores outside `src/components/ui`. +- Delete a legacy `calc-*` component only after all of its call sites have migrated. + +## 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 | + +## Legacy replacement order + +Deferred: this component-library pass does not change business call sites. + +1. Shared form controls and overlays used by DNF Helper and import dialogs. +2. Forge controls (`select`, `checkbox`, `slider`, tabs and panels). +3. Equipment dictionary filters, tree, cascader and pagination. +4. Skill tree, skill detail and VP controls. +5. Result tables and remaining domain-specific components. diff --git a/frontend-ui/src/components/ui/autocomplete/UiAutocomplete.vue b/frontend-ui/src/components/ui/autocomplete/UiAutocomplete.vue new file mode 100644 index 00000000..a432eb0f --- /dev/null +++ b/frontend-ui/src/components/ui/autocomplete/UiAutocomplete.vue @@ -0,0 +1,234 @@ + + + + + diff --git a/frontend-ui/src/components/ui/autocomplete/index.ts b/frontend-ui/src/components/ui/autocomplete/index.ts new file mode 100644 index 00000000..a39cf053 --- /dev/null +++ b/frontend-ui/src/components/ui/autocomplete/index.ts @@ -0,0 +1,2 @@ +export { default as UiAutocomplete } from './UiAutocomplete.vue' +export type { UiAutocompleteOption } from './UiAutocomplete.vue' diff --git a/frontend-ui/src/components/ui/badge/badgeVariants.ts b/frontend-ui/src/components/ui/badge/badgeVariants.ts index bea0d658..33ed04f8 100644 --- a/frontend-ui/src/components/ui/badge/badgeVariants.ts +++ b/frontend-ui/src/components/ui/badge/badgeVariants.ts @@ -1,20 +1,17 @@ import { cva, type VariantProps } from 'class-variance-authority' -export const badgeVariants = cva( - 'inline-flex h-[18px] items-center rounded-none border px-[7px] text-[10px] font-normal leading-none', - { - variants: { - variant: { - default: 'border-border bg-primary text-primary-foreground', - secondary: 'border-border bg-secondary text-secondary-foreground', - outline: 'border-border bg-transparent text-foreground', - destructive: 'border-destructive bg-destructive text-destructive-foreground', - }, - }, - defaultVariants: { - variant: 'default', +export const badgeVariants = cva('ui-badge', { + variants: { + variant: { + default: 'border-border bg-primary text-primary-foreground', + secondary: 'border-border bg-secondary text-secondary-foreground', + outline: 'border-border bg-transparent text-foreground', + destructive: 'border-destructive bg-destructive text-destructive-foreground', }, }, -) + defaultVariants: { + variant: 'default', + }, +}) export type BadgeVariants = VariantProps diff --git a/frontend-ui/src/components/ui/button/buttonVariants.ts b/frontend-ui/src/components/ui/button/buttonVariants.ts index fb1778cf..5e1acbab 100644 --- a/frontend-ui/src/components/ui/button/buttonVariants.ts +++ b/frontend-ui/src/components/ui/button/buttonVariants.ts @@ -1,7 +1,7 @@ import { cva, type VariantProps } from 'class-variance-authority' export const buttonVariants = cva( - 'ui-button inline-flex items-center justify-center gap-2 whitespace-nowrap outline-none transition-[color,background,border-color,transform] disabled:pointer-events-none [&_[data-icon]]:pointer-events-none [&_[data-icon]]:shrink-0', + 'ui-button inline-flex items-center justify-center whitespace-nowrap text-center leading-none outline-none transition-[color,background,border-color,transform] disabled:pointer-events-none [&_[data-icon]]:pointer-events-none [&_[data-icon]]:shrink-0', { variants: { variant: { @@ -12,10 +12,10 @@ export const buttonVariants = cva( ghost: 'ui-button--ghost', }, size: { - default: 'h-[30px] min-w-[110px] px-[14px] text-[13px]', - sm: 'h-6 min-w-20 px-2.5 text-xs', - lg: 'h-11 min-w-[168px] px-5 text-base', - icon: 'size-[30px] min-w-[30px] p-0', + default: 'ui-button--size-default', + sm: 'ui-button--size-compact', + lg: 'ui-button--size-large', + icon: 'ui-button--size-icon', }, }, defaultVariants: { diff --git a/frontend-ui/src/components/ui/checkbox/UiCheckbox.vue b/frontend-ui/src/components/ui/checkbox/UiCheckbox.vue index a849c45d..76914417 100644 --- a/frontend-ui/src/components/ui/checkbox/UiCheckbox.vue +++ b/frontend-ui/src/components/ui/checkbox/UiCheckbox.vue @@ -25,7 +25,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) v-bind="forwarded" :class=" cn( - 'ui-checkbox peer inline-flex size-4 shrink-0 items-center justify-center border outline-none disabled:cursor-not-allowed', + 'ui-checkbox peer inline-flex shrink-0 items-center justify-center border outline-none disabled:cursor-not-allowed', props.class, ) " @@ -36,27 +36,29 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) diff --git a/frontend-ui/src/components/ui/data-section/UiDataSection.vue b/frontend-ui/src/components/ui/data-section/UiDataSection.vue new file mode 100644 index 00000000..b1b0c632 --- /dev/null +++ b/frontend-ui/src/components/ui/data-section/UiDataSection.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/frontend-ui/src/components/ui/data-section/index.ts b/frontend-ui/src/components/ui/data-section/index.ts new file mode 100644 index 00000000..5a476d68 --- /dev/null +++ b/frontend-ui/src/components/ui/data-section/index.ts @@ -0,0 +1,2 @@ +export { default as UiDataRow } from './UiDataRow.vue' +export { default as UiDataSection } from './UiDataSection.vue' diff --git a/frontend-ui/src/components/ui/data-table/UiDataTable.vue b/frontend-ui/src/components/ui/data-table/UiDataTable.vue index 1a9380a0..5b3f24d4 100644 --- a/frontend-ui/src/components/ui/data-table/UiDataTable.vue +++ b/frontend-ui/src/components/ui/data-table/UiDataTable.vue @@ -75,26 +75,26 @@ const gridTemplate = computed(() => props.columns.map((column) => column.width ? .ui-data-table { overflow-x: auto; overflow-y: hidden; - border: 1px solid #4a3c26; + border: 1px solid var(--ui-table-border); border-radius: 0; - background: #030403; - color: #d2c78f; + background: var(--ui-table-background); + color: var(--ui-table-foreground); } .ui-data-table__row { display: grid; width: 100%; - min-height: 38px; + min-height: var(--ui-table-row-min-block-size); align-items: center; padding: 0; border: 0; - border-bottom: 1px solid #2c2b26; + border-bottom: 1px solid var(--ui-table-row-divider); border-radius: 0; background: transparent; color: inherit; font: 12px/1 inherit; text-align: left; - min-width: 560px; + min-width: var(--ui-table-scroll-inline-size); } button.ui-data-table__row { @@ -108,43 +108,45 @@ button.ui-data-table__row:nth-child(odd) { background: var(--ui-row-background); } button.ui-data-table__row:hover { - background: linear-gradient(90deg, #132b43, #0b1824 58%, #070a0d); - outline: 1px solid #3b91bd; + background: var(--ui-table-row-hover); + outline: 1px solid var(--ui-table-row-hover-outline); outline-offset: -1px; } button.ui-data-table__row.is-selected { - background: linear-gradient(90deg, #352b11, #161207 60%, #090a08); - outline: 1px solid #d6ad37; + background: var(--ui-table-row-selected); + outline: 1px solid var(--ui-table-row-selected-outline); outline-offset: -1px; - box-shadow: inset 0 0 0 1px #6d5218; + box-shadow: inset 0 0 0 1px var(--ui-table-row-selected-inset); } .ui-data-table__row > * { box-sizing: border-box; + display: flex; min-width: 0; height: 100%; + align-items: center; padding: 0 9px; overflow: hidden; - border-right: 1px solid #292720; + border-right: 1px solid var(--ui-table-cell-divider); text-overflow: ellipsis; white-space: nowrap; } .ui-data-table__header { - min-height: 30px; - height: 30px; + min-height: var(--ui-table-header-block-size); + height: var(--ui-table-header-block-size); column-gap: 1px; border-bottom: 0; - background: #030403; + background: var(--ui-table-background); } .ui-data-table__header > * { display: flex; align-items: center; - border: 1px solid #070806; + border: 1px solid var(--ui-table-header-border); background: var(--ui-table-header); box-shadow: - inset 0 0 0 1px #4a3c26, - inset 0 1px 0 #65502a; + inset 0 0 0 1px var(--ui-table-header-inset), + inset 0 1px 0 var(--ui-table-header-highlight); color: var(--ui-table-header-foreground); font: 11px/1 inherit; } diff --git a/frontend-ui/src/components/ui/dialog/UiDialogContent.vue b/frontend-ui/src/components/ui/dialog/UiDialogContent.vue index 26cb1d6f..8fa0a092 100644 --- a/frontend-ui/src/components/ui/dialog/UiDialogContent.vue +++ b/frontend-ui/src/components/ui/dialog/UiDialogContent.vue @@ -24,7 +24,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) v-bind="forwarded" :class=" cn( - 'ui-dialog-content fixed left-1/2 top-1/2 z-50 grid w-[min(92vw,620px)] -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 -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]', props.class, ) " @@ -35,13 +35,14 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) diff --git a/frontend-ui/src/components/ui/dialog/UiDialogFooter.vue b/frontend-ui/src/components/ui/dialog/UiDialogFooter.vue index 38655003..e279902c 100644 --- a/frontend-ui/src/components/ui/dialog/UiDialogFooter.vue +++ b/frontend-ui/src/components/ui/dialog/UiDialogFooter.vue @@ -7,12 +7,12 @@ diff --git a/frontend-ui/src/components/ui/dialog/UiDialogHeader.vue b/frontend-ui/src/components/ui/dialog/UiDialogHeader.vue index b63e55fb..08c966c5 100644 --- a/frontend-ui/src/components/ui/dialog/UiDialogHeader.vue +++ b/frontend-ui/src/components/ui/dialog/UiDialogHeader.vue @@ -52,19 +52,19 @@ onBeforeUnmount(() => clearInterval(timer)) isolation: isolate; display: grid; grid-template-columns: 1fr auto 1fr; - height: 28px; + height: var(--ui-dialog-header-block-size); align-items: center; padding: 0 8px; overflow: hidden; - border-bottom: 1px solid #1d2735; - background: linear-gradient(#303b50, #263247 56%, #1d2839); + border-bottom: 1px solid var(--ui-dialog-titlebar-border); + background: var(--ui-dialog-titlebar-background); color: var(--ui-titlebar-foreground); - text-shadow: 0 1px 1px #000; + text-shadow: 0 1px 1px var(--ui-dialog-titlebar-text-shadow); } .ui-dialog-header.is-focused { - background: linear-gradient(#6079a5, #435a80 55%, #30425f); - color: #f4f6f2; + background: var(--ui-dialog-titlebar-focused-background); + color: var(--ui-dialog-titlebar-focused-foreground); } .ui-dialog-header__effect { @@ -72,7 +72,7 @@ onBeforeUnmount(() => clearInterval(timer)) z-index: 1; top: 0; left: 50%; - width: 1100px; + width: var(--ui-dialog-effect-inline-size); height: 100%; max-width: none; object-fit: fill; @@ -101,8 +101,8 @@ onBeforeUnmount(() => clearInterval(timer)) .ui-dialog-header__close { appearance: none; - width: 20px; - height: 20px; + width: var(--ui-dialog-close-size); + height: var(--ui-dialog-close-size); padding: 0; display: inline-flex; align-items: center; @@ -114,8 +114,8 @@ onBeforeUnmount(() => clearInterval(timer)) } .ui-dialog-header__close img { - width: 10px; - height: 9px; + width: var(--ui-dialog-close-glyph-inline-size); + height: var(--ui-dialog-close-glyph-block-size); filter: brightness(1); } @@ -124,7 +124,7 @@ onBeforeUnmount(() => clearInterval(timer)) filter: brightness(1.28); } .ui-dialog-header__close:focus-visible { - outline: 1px solid rgb(225 204 135 / 72%); + outline: 1px solid var(--ui-dialog-close-ring); outline-offset: -2px; } .ui-dialog-header__close:active img { diff --git a/frontend-ui/src/components/ui/dialog/UiDialogTitle.vue b/frontend-ui/src/components/ui/dialog/UiDialogTitle.vue index 16720693..96a85df4 100644 --- a/frontend-ui/src/components/ui/dialog/UiDialogTitle.vue +++ b/frontend-ui/src/components/ui/dialog/UiDialogTitle.vue @@ -11,10 +11,16 @@ const delegatedProps = computed(() => { + + diff --git a/frontend-ui/src/components/ui/dropdown-menu/UiDropdownMenuContent.vue b/frontend-ui/src/components/ui/dropdown-menu/UiDropdownMenuContent.vue index 6c9c9207..93f62b60 100644 --- a/frontend-ui/src/components/ui/dropdown-menu/UiDropdownMenuContent.vue +++ b/frontend-ui/src/components/ui/dropdown-menu/UiDropdownMenuContent.vue @@ -37,12 +37,12 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) .ui-dropdown-content { position: relative; z-index: 80; - min-width: 190px; + min-width: var(--ui-menu-min-inline-size); padding: 3px 3px 4px; - border: 1px solid rgb(19 17 13 / 92%); + border: 1px solid var(--ui-menu-border); border-radius: 0; - background: rgb(48 46 38 / 72%); - box-shadow: 0 10px 20px rgb(0 0 0 / 78%); + background: var(--ui-menu-background); + box-shadow: 0 10px 20px var(--ui-menu-shadow); backdrop-filter: blur(1px); color: var(--ui-popover-foreground); transform-origin: var(--reka-dropdown-menu-content-transform-origin); diff --git a/frontend-ui/src/components/ui/dropdown-menu/UiDropdownMenuItem.vue b/frontend-ui/src/components/ui/dropdown-menu/UiDropdownMenuItem.vue index 45c3d5e3..d0a7ac2b 100644 --- a/frontend-ui/src/components/ui/dropdown-menu/UiDropdownMenuItem.vue +++ b/frontend-ui/src/components/ui/dropdown-menu/UiDropdownMenuItem.vue @@ -31,21 +31,23 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) diff --git a/frontend-ui/src/components/ui/input/UiInput.vue b/frontend-ui/src/components/ui/input/UiInput.vue index 057accc2..0a6f1c0a 100644 --- a/frontend-ui/src/components/ui/input/UiInput.vue +++ b/frontend-ui/src/components/ui/input/UiInput.vue @@ -25,15 +25,18 @@ const model = defineModel() .ui-input { box-sizing: border-box; width: 100%; - height: 30px; - padding: 0 9px; + min-width: 0; + height: var(--ui-control-height-default); + padding: 0 var(--ui-field-padding-inline); border: 1px solid var(--ui-input); border-radius: 0; outline: none; - background: linear-gradient(#0b0c0d, #000100); - box-shadow: inset 0 0 0 1px #050505; + background: var(--ui-field-background); + box-shadow: inset 0 0 0 1px var(--ui-field-inset); color: var(--ui-foreground); font: inherit; + font-size: var(--ui-control-font-size-default); + line-height: var(--ui-control-line-height); } .ui-input::placeholder { @@ -44,14 +47,14 @@ const model = defineModel() .ui-input:focus-visible { border-color: var(--ui-ring); box-shadow: - inset 0 0 0 1px #071722, - 0 0 0 1px #24617c; + inset 0 0 0 1px var(--ui-field-focus-inset), + 0 0 0 1px var(--ui-field-focus-edge); } .ui-input:disabled { cursor: not-allowed; border-color: var(--ui-control-disabled-border); - background: #101110; + background: var(--ui-field-disabled-background); color: var(--ui-control-disabled-foreground); } diff --git a/frontend-ui/src/components/ui/item-slot/UiItemSlot.vue b/frontend-ui/src/components/ui/item-slot/UiItemSlot.vue index b7a9319d..5872390c 100644 --- a/frontend-ui/src/components/ui/item-slot/UiItemSlot.vue +++ b/frontend-ui/src/components/ui/item-slot/UiItemSlot.vue @@ -42,18 +42,18 @@ const frames: Record = { .ui-item-slot { position: relative; display: flex; - width: 40px; - height: 40px; + width: var(--ui-item-slot-size); + height: var(--ui-item-slot-size); align-items: center; justify-content: center; border: 0; - background: #030404; + background: var(--ui-item-slot-background); } .ui-item-slot__item, .ui-item-slot__frame { - width: 28px; - height: 28px; + width: var(--ui-item-image-size); + height: var(--ui-item-image-size); image-rendering: pixelated; } @@ -64,7 +64,7 @@ const frames: Record = { .ui-item-slot__frame { position: absolute; z-index: 2; - inset: 5px; + inset: var(--ui-item-frame-inset); } .ui-item-slot.is-emphasized::before, @@ -76,22 +76,22 @@ const frames: Record = { .ui-item-slot.is-emphasized::before { z-index: 0; inset: 4px; - border: 1px solid #ffa800; + border: 1px solid var(--ui-item-slot-emphasis); box-shadow: - 0 0 2px 1px rgb(255 168 0 / 88%), - 0 0 5px 4px rgb(255 168 0 / 46%), - inset 0 0 2px rgb(255 168 0 / 70%); + 0 0 2px 1px var(--ui-item-slot-emphasis-shadow-strong), + 0 0 5px 4px var(--ui-item-slot-emphasis-shadow), + inset 0 0 2px var(--ui-item-slot-emphasis-inset); } .ui-item-slot.is-emphasized::after { z-index: 3; - inset: 5px; - border: 1px solid #fffc2a; + inset: var(--ui-item-frame-inset); + border: 1px solid var(--ui-item-slot-emphasis-strong); } .ui-item-slot.is-locked::after { content: ''; position: absolute; z-index: 4; inset: 0; - background: rgb(0 0 0 / 68%); + background: var(--ui-item-slot-lock-overlay); } diff --git a/frontend-ui/src/components/ui/navigation/UiNavigation.vue b/frontend-ui/src/components/ui/navigation/UiNavigation.vue new file mode 100644 index 00000000..213a8f3e --- /dev/null +++ b/frontend-ui/src/components/ui/navigation/UiNavigation.vue @@ -0,0 +1,112 @@ + + + + + diff --git a/frontend-ui/src/components/ui/navigation/index.ts b/frontend-ui/src/components/ui/navigation/index.ts new file mode 100644 index 00000000..c171f309 --- /dev/null +++ b/frontend-ui/src/components/ui/navigation/index.ts @@ -0,0 +1,2 @@ +export { default as UiNavigation } from './UiNavigation.vue' +export type { UiNavigationItem } from './UiNavigation.vue' diff --git a/frontend-ui/src/components/ui/number-field/UiNumberField.vue b/frontend-ui/src/components/ui/number-field/UiNumberField.vue index 5dcf4dda..4f25dceb 100644 --- a/frontend-ui/src/components/ui/number-field/UiNumberField.vue +++ b/frontend-ui/src/components/ui/number-field/UiNumberField.vue @@ -65,41 +65,45 @@ function fastChange(direction: 1 | -1) { diff --git a/frontend-ui/src/components/ui/rating/UiRating.vue b/frontend-ui/src/components/ui/rating/UiRating.vue index 39f66b46..bcc65307 100644 --- a/frontend-ui/src/components/ui/rating/UiRating.vue +++ b/frontend-ui/src/components/ui/rating/UiRating.vue @@ -39,14 +39,14 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) .ui-rating { display: flex; width: max-content; - min-height: 46px; + min-height: var(--ui-rating-min-block-size); align-items: center; gap: 2px; padding: 8px 12px; border-radius: 22px; outline: none; - background: #1a1917; - box-shadow: inset 0 1px 0 rgb(255 255 255 / 3%); + background: var(--ui-rating-background); + box-shadow: inset 0 1px 0 var(--ui-rating-highlight); } .ui-rating:focus-visible { @@ -54,8 +54,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) } .ui-rating__item { position: relative; - width: 24px; - height: 24px; + width: var(--ui-rating-item-size); + height: var(--ui-rating-item-size); padding: 0; border: 0; background: transparent; @@ -63,15 +63,15 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) } .ui-rating__item > img, .ui-rating :deep(.ui-rating__indicator img) { - width: 24px; + width: var(--ui-rating-item-size); height: auto; image-rendering: pixelated; } .ui-rating :deep(.ui-rating__indicator) { position: absolute; inset: 0; - width: 24px; - height: 24px; + width: var(--ui-rating-item-size); + height: var(--ui-rating-item-size); padding: 0; overflow: hidden; border: 0; diff --git a/frontend-ui/src/components/ui/scroll-area/UiScrollArea.vue b/frontend-ui/src/components/ui/scroll-area/UiScrollArea.vue new file mode 100644 index 00000000..000ed22a --- /dev/null +++ b/frontend-ui/src/components/ui/scroll-area/UiScrollArea.vue @@ -0,0 +1,98 @@ + + + + + diff --git a/frontend-ui/src/components/ui/scroll-area/index.ts b/frontend-ui/src/components/ui/scroll-area/index.ts new file mode 100644 index 00000000..c4689ef6 --- /dev/null +++ b/frontend-ui/src/components/ui/scroll-area/index.ts @@ -0,0 +1 @@ +export { default as UiScrollArea } from './UiScrollArea.vue' diff --git a/frontend-ui/src/components/ui/select/UiSelectContent.vue b/frontend-ui/src/components/ui/select/UiSelectContent.vue index 702da2cb..2b2b6faf 100644 --- a/frontend-ui/src/components/ui/select/UiSelectContent.vue +++ b/frontend-ui/src/components/ui/select/UiSelectContent.vue @@ -46,10 +46,10 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) z-index: 80; max-height: min(18rem, var(--reka-select-content-available-height)); overflow: hidden; - border: 1px solid #5a4828; + border: 1px solid var(--ui-popup-border); border-radius: 0; - background: #050606; - box-shadow: 0 8px 18px #000; + background: var(--ui-popup-background); + box-shadow: 0 8px 18px var(--ui-popup-shadow); color: var(--ui-popover-foreground); transform-origin: var(--reka-select-content-transform-origin); } diff --git a/frontend-ui/src/components/ui/select/UiSelectItem.vue b/frontend-ui/src/components/ui/select/UiSelectItem.vue index 0dd1f867..34b93bdd 100644 --- a/frontend-ui/src/components/ui/select/UiSelectItem.vue +++ b/frontend-ui/src/components/ui/select/UiSelectItem.vue @@ -30,12 +30,14 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) .ui-select-item { position: relative; display: flex; - height: 27px; + height: var(--ui-option-height); align-items: center; padding: 0 10px; - border-bottom: 1px solid #25231f; + border-bottom: 1px solid var(--ui-option-divider); outline: none; color: var(--ui-primary-foreground); + font-size: var(--ui-control-font-size-default); + line-height: var(--ui-control-line-height); cursor: pointer; user-select: none; } @@ -43,8 +45,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) .ui-select-item[data-highlighted] { outline: 1px solid var(--ui-ring); outline-offset: -2px; - background: #091731; - color: #f3d777; + background: var(--ui-option-highlight-background); + color: var(--ui-option-highlight-foreground); } .ui-select-item[data-state='checked'] { @@ -57,8 +59,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) } .ui-select-item:active { - background: #07121b; - color: #fff0a4; + background: var(--ui-option-active-background); + color: var(--ui-option-active-foreground); } .ui-select-item__indicator { display: none; diff --git a/frontend-ui/src/components/ui/select/UiSelectTrigger.vue b/frontend-ui/src/components/ui/select/UiSelectTrigger.vue index 4bd83c43..30e4e992 100644 --- a/frontend-ui/src/components/ui/select/UiSelectTrigger.vue +++ b/frontend-ui/src/components/ui/select/UiSelectTrigger.vue @@ -25,18 +25,20 @@ const delegatedProps = computed(() => { box-sizing: border-box; display: inline-flex; width: 100%; - height: 30px; + height: var(--ui-control-height-default); align-items: center; justify-content: space-between; gap: 8px; - padding: 0 4px 0 9px; + padding: 0 4px 0 var(--ui-field-padding-inline); border: 1px solid var(--ui-input); border-radius: 0; outline: none; - background: linear-gradient(#0b0c0d, #000100); - box-shadow: inset 0 0 0 1px #050505; + background: var(--ui-field-background); + box-shadow: inset 0 0 0 1px var(--ui-field-inset); color: var(--ui-primary-foreground); font: inherit; + font-size: var(--ui-control-font-size-default); + line-height: var(--ui-control-line-height); cursor: pointer; } @@ -44,8 +46,8 @@ const delegatedProps = computed(() => { .ui-select-trigger[data-state='open'] { border-color: var(--ui-ring); box-shadow: - inset 0 0 0 1px #071722, - 0 0 0 1px #24617c; + inset 0 0 0 1px var(--ui-field-focus-inset), + 0 0 0 1px var(--ui-field-focus-edge); } .ui-select-trigger:disabled { @@ -55,25 +57,25 @@ const delegatedProps = computed(() => { .ui-select-trigger__icon { display: inline-flex; - width: 21px; - height: 18px; + 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 rgb(0 0 0 / 60%); + box-shadow: 0 0 0 1px var(--ui-select-icon-shadow); } .ui-select-trigger__icon img { - width: 11px; - height: 6px; + 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: #725f4e; - background: linear-gradient(180deg, #29457f, #24386a); + border-color: var(--ui-control-border-active); + background: var(--ui-control-background-hover); } .ui-select-trigger:active .ui-select-trigger__icon, diff --git a/frontend-ui/src/components/ui/settings/UiSettingRow.vue b/frontend-ui/src/components/ui/settings/UiSettingRow.vue new file mode 100644 index 00000000..553b0ffb --- /dev/null +++ b/frontend-ui/src/components/ui/settings/UiSettingRow.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/frontend-ui/src/components/ui/settings/UiSettingsGroup.vue b/frontend-ui/src/components/ui/settings/UiSettingsGroup.vue new file mode 100644 index 00000000..680d2677 --- /dev/null +++ b/frontend-ui/src/components/ui/settings/UiSettingsGroup.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/frontend-ui/src/components/ui/settings/UiSettingsSection.vue b/frontend-ui/src/components/ui/settings/UiSettingsSection.vue new file mode 100644 index 00000000..bc92f4af --- /dev/null +++ b/frontend-ui/src/components/ui/settings/UiSettingsSection.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend-ui/src/components/ui/settings/index.ts b/frontend-ui/src/components/ui/settings/index.ts new file mode 100644 index 00000000..5a74b968 --- /dev/null +++ b/frontend-ui/src/components/ui/settings/index.ts @@ -0,0 +1,3 @@ +export { default as UiSettingRow } from './UiSettingRow.vue' +export { default as UiSettingsGroup } from './UiSettingsGroup.vue' +export { default as UiSettingsSection } from './UiSettingsSection.vue' diff --git a/frontend-ui/src/components/ui/shop-item-row/UiShopItemList.vue b/frontend-ui/src/components/ui/shop-item-row/UiShopItemList.vue new file mode 100644 index 00000000..56819109 --- /dev/null +++ b/frontend-ui/src/components/ui/shop-item-row/UiShopItemList.vue @@ -0,0 +1,310 @@ + + + + + diff --git a/frontend-ui/src/components/ui/shop-item-row/index.ts b/frontend-ui/src/components/ui/shop-item-row/index.ts new file mode 100644 index 00000000..2d49a410 --- /dev/null +++ b/frontend-ui/src/components/ui/shop-item-row/index.ts @@ -0,0 +1,2 @@ +export { default as UiShopItemList } from './UiShopItemList.vue' +export type { UiShopItem } from './UiShopItemList.vue' diff --git a/frontend-ui/src/components/ui/slider/UiSlider.vue b/frontend-ui/src/components/ui/slider/UiSlider.vue index 1406ba29..fe574d37 100644 --- a/frontend-ui/src/components/ui/slider/UiSlider.vue +++ b/frontend-ui/src/components/ui/slider/UiSlider.vue @@ -53,8 +53,8 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) position: relative; display: flex; width: 100%; - min-width: 160px; - height: 34px; + min-width: var(--ui-slider-min-inline-size); + height: var(--ui-slider-block-size); align-items: center; gap: 10px; } @@ -64,7 +64,7 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) display: flex; min-width: 0; flex: 1; - height: 17px; + height: var(--ui-slider-control-size); align-items: center; touch-action: none; user-select: none; @@ -82,28 +82,28 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) .ui-slider__track { position: relative; flex: 1; - height: 3px; + height: var(--ui-slider-track-size); overflow: visible; - background: #515351; - box-shadow: 0 1px 0 #020202; + background: var(--ui-slider-track); + box-shadow: 0 1px 0 var(--ui-slider-track-shadow); } .ui-slider__range { position: absolute; inset-block: 0; - background: #f3d367; - box-shadow: 0 0 3px rgb(243 211 103 / 25%); + background: var(--ui-slider-range); + box-shadow: 0 0 3px var(--ui-slider-range-shadow); } .ui-slider__thumb { - --thumb-edge-top: #86693f; - --thumb-edge-bottom: #76593a; - --thumb-top: #21365e; - --thumb-bottom: #1b2b4d; + --thumb-edge-top: var(--ui-slider-thumb-edge-top); + --thumb-edge-bottom: var(--ui-slider-thumb-edge-bottom); + --thumb-top: var(--ui-slider-thumb-top); + --thumb-bottom: var(--ui-slider-thumb-bottom); box-sizing: border-box; display: block; - width: 17px; - height: 17px; + width: var(--ui-slider-control-size); + height: var(--ui-slider-control-size); border: 1px solid transparent; border-radius: 50%; outline: none; @@ -111,37 +111,37 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) radial-gradient(circle at 50% 28%, var(--thumb-top), var(--thumb-bottom) 78%) padding-box, linear-gradient(180deg, var(--thumb-edge-top), var(--thumb-edge-bottom)) border-box; box-shadow: - 0 0 0 1px rgb(0 0 0 / 40%), - inset 0 1px 0 rgb(255 255 255 / 2.5%); + 0 0 0 1px var(--ui-control-outer-shadow), + inset 0 1px 0 var(--ui-control-inner-highlight); } .ui-slider__control:hover .ui-slider__thumb, .ui-slider__control:active .ui-slider__thumb, .ui-slider__thumb:hover, .ui-slider__thumb:focus-visible { - --thumb-edge-top: #907960; - --thumb-edge-bottom: #80695b; - --thumb-top: #2b467f; - --thumb-bottom: #253b6e; - box-shadow: 0 0 0 1px rgb(0 0 0 / 40%); + --thumb-edge-top: var(--ui-slider-thumb-hover-edge-top); + --thumb-edge-bottom: var(--ui-slider-thumb-hover-edge-bottom); + --thumb-top: var(--ui-slider-thumb-hover-top); + --thumb-bottom: var(--ui-slider-thumb-hover-bottom); + box-shadow: 0 0 0 1px var(--ui-control-outer-shadow); } .ui-slider__control[data-disabled] .ui-slider__range { - background: #686868; + background: var(--ui-slider-disabled-range); box-shadow: none; } .ui-slider__control[data-disabled] .ui-slider__thumb { - --thumb-edge-top: #454545; - --thumb-edge-bottom: #3e3e3e; - --thumb-top: #2c2c2c; - --thumb-bottom: #242424; + --thumb-edge-top: var(--ui-slider-disabled-thumb-edge-top); + --thumb-edge-bottom: var(--ui-slider-disabled-thumb-edge-bottom); + --thumb-top: var(--ui-slider-disabled-thumb-top); + --thumb-bottom: var(--ui-slider-disabled-thumb-bottom); } .ui-slider:has(.ui-slider__control[data-disabled]) .ui-slider__value { color: var(--ui-control-disabled-foreground); } .ui-slider__value { - min-width: 40px; + min-width: var(--ui-slider-value-min-inline-size); color: var(--ui-primary-foreground); font-size: 12px; text-align: right; diff --git a/frontend-ui/src/components/ui/switch/UiSwitch.vue b/frontend-ui/src/components/ui/switch/UiSwitch.vue index a18d5eb6..dbd7bc74 100644 --- a/frontend-ui/src/components/ui/switch/UiSwitch.vue +++ b/frontend-ui/src/components/ui/switch/UiSwitch.vue @@ -29,16 +29,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) .ui-switch { box-sizing: border-box; position: relative; - width: 28px; - height: 16px; + width: var(--ui-switch-inline-size); + height: var(--ui-switch-block-size); padding: 0; border: 1px solid transparent; border-radius: 8px / 50%; outline: none; background: - linear-gradient(#141414, #0f0f0f) padding-box, - linear-gradient(#846a48, #453520) border-box; - box-shadow: inset 0 1px 3px #090909; + var(--ui-switch-surface) padding-box, + var(--ui-switch-border) border-box; + box-shadow: inset 0 1px 3px var(--ui-switch-inset); cursor: pointer; } @@ -54,14 +54,14 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) transform: translate(1px, 1px); } .ui-switch[data-state='checked']:active:not(:disabled) .ui-switch__thumb { - transform: translate(13px, 1px); + transform: translate(var(--ui-switch-thumb-travel), 1px); } .ui-switch[data-state='checked'] { background: - linear-gradient(#eccb65, #eccb65) padding-box, - linear-gradient(#846a48, #453520) border-box; - box-shadow: inset 0 0 0 1px #0d0d0d; + var(--ui-switch-checked-surface) padding-box, + var(--ui-switch-border) border-box; + box-shadow: inset 0 0 0 1px var(--ui-switch-checked-inset); } .ui-switch:disabled { @@ -77,16 +77,16 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) .ui-switch__thumb { display: block; - width: 12px; - height: 12px; + width: var(--ui-switch-thumb-size); + height: var(--ui-switch-thumb-size); border-radius: 50%; background: var(--ui-control-background); - box-shadow: inset 0 1px 0 rgb(255 255 255 / 4%); + box-shadow: inset 0 1px 0 var(--ui-control-inner-highlight); transition: transform 90ms ease-out; transform: translateX(1px); } .ui-switch__thumb[data-state='checked'] { - transform: translateX(13px); + transform: translateX(var(--ui-switch-thumb-travel)); } diff --git a/frontend-ui/src/components/ui/tabs/UiTabsContent.vue b/frontend-ui/src/components/ui/tabs/UiTabsContent.vue index dd23b2b1..85bfaa30 100644 --- a/frontend-ui/src/components/ui/tabs/UiTabsContent.vue +++ b/frontend-ui/src/components/ui/tabs/UiTabsContent.vue @@ -22,7 +22,7 @@ const delegatedProps = computed(() => { border: 1px solid var(--ui-border); border-top: 0; outline: none; - background: rgb(0 0 0 / 22%); + background: var(--ui-tabs-content-background); color: var(--ui-foreground); } diff --git a/frontend-ui/src/components/ui/tabs/UiTabsList.vue b/frontend-ui/src/components/ui/tabs/UiTabsList.vue index e00a4993..a1d60d15 100644 --- a/frontend-ui/src/components/ui/tabs/UiTabsList.vue +++ b/frontend-ui/src/components/ui/tabs/UiTabsList.vue @@ -85,10 +85,10 @@ onBeforeUnmount(() => { position: relative; display: flex; width: 100%; - height: 40px; - align-items: flex-start; + height: var(--ui-tabs-height); + align-items: center; padding: 5px 7px; - background: #090709; + background: var(--ui-tab-list-background); } .ui-tabs-list::after { @@ -98,15 +98,15 @@ onBeforeUnmount(() => { top: 35px; left: 7px; height: 1px; - background: #584840; + background: var(--ui-tab-divider); } .ui-tabs-list__marker { position: absolute; z-index: 4; bottom: 0; - width: 39px; - height: 5px; + width: var(--ui-tab-marker-inline-size); + height: var(--ui-tab-marker-block-size); opacity: 0; transform: translateX(-50%); pointer-events: none; @@ -117,7 +117,7 @@ onBeforeUnmount(() => { position: absolute; inset: 0 0 auto; height: 1px; - background: linear-gradient(90deg, transparent, #d0a56a 50%, transparent); + background: linear-gradient(90deg, transparent, var(--ui-tab-marker) 50%, transparent); } .ui-tabs-list__marker::after { @@ -125,9 +125,14 @@ onBeforeUnmount(() => { position: absolute; top: 1px; left: 50%; - width: 9px; - height: 4px; - background: linear-gradient(180deg, #9d7a35, #a7823f 46%, #d0a56a); + width: var(--ui-tab-marker-arrow-inline-size); + height: var(--ui-tab-marker-arrow-block-size); + background: linear-gradient( + 180deg, + var(--ui-tab-marker-start), + var(--ui-tab-marker-middle) 46%, + var(--ui-tab-marker) + ); clip-path: polygon(0 0, 100% 0, 50% 100%); transform: translateX(-50%); } diff --git a/frontend-ui/src/components/ui/tabs/UiTabsTrigger.vue b/frontend-ui/src/components/ui/tabs/UiTabsTrigger.vue index 3f39e54c..0bc71d46 100644 --- a/frontend-ui/src/components/ui/tabs/UiTabsTrigger.vue +++ b/frontend-ui/src/components/ui/tabs/UiTabsTrigger.vue @@ -18,38 +18,26 @@ const delegatedProps = computed(() => { diff --git a/frontend-ui/src/components/ui/tooltip/UiTooltipContent.vue b/frontend-ui/src/components/ui/tooltip/UiTooltipContent.vue index a402bb98..9dd961cf 100644 --- a/frontend-ui/src/components/ui/tooltip/UiTooltipContent.vue +++ b/frontend-ui/src/components/ui/tooltip/UiTooltipContent.vue @@ -37,13 +37,13 @@ const forwarded = useForwardPropsEmits(delegatedProps, emits) .ui-tooltip-content { position: relative; z-index: 90; - max-width: 260px; + max-width: var(--ui-tooltip-max-inline-size); padding: 5px 7px; - border: 1px solid #6c5c3d; + border: 1px solid var(--ui-tooltip-border); border-radius: 0; - background: #020302; - box-shadow: 0 6px 14px #000; - color: #f5f5ef; + background: var(--ui-tooltip-background); + box-shadow: 0 6px 14px var(--ui-tooltip-shadow); + color: var(--ui-tooltip-foreground); font-size: 12px; line-height: 1.5; transform-origin: var(--reka-tooltip-content-transform-origin); diff --git a/frontend-ui/src/pages/ComponentCatalog/Index.vue b/frontend-ui/src/pages/ComponentCatalog/Index.vue index 29918471..6cdc7bf7 100644 --- a/frontend-ui/src/pages/ComponentCatalog/Index.vue +++ b/frontend-ui/src/pages/ComponentCatalog/Index.vue @@ -1,8 +1,10 @@