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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ emptyText }}
+
+ {{ option.label }}
+ ◆
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+ {{ title }}
+ {{ open ? expandedLabel : collapsedLabel }}
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+
{{ row.reinforce }}
+
{{ row.amplify }}
+
{{ row.name }}
+
+
+
+
{{ row.status }}
+
{{ row.owner }}
+
+
+
+
+
+
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 @@