refactor: establish local frontend workspace boundaries
Conventional commits / commits (pull_request) Successful in 24s
Frontend security / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 5m18s
Frontend security / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 6m45s

OpenSpec change: evolve-frontend-stack
This commit was merged in pull request #34.
This commit is contained in:
2026-09-22 22:11:02 +03:00
parent c86be7bf73
commit 85fcfe3d07
101 changed files with 1487 additions and 666 deletions
+3 -3
View File
@@ -10,8 +10,8 @@ import {
useMantineTheme,
} from "@mantine/core";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { colorRoles, designTokens } from "@wealthledger/ui/theme";
import type { FC } from "react";
import { colorRoles, designTokens } from "./index";
const ColorsView: FC = () => {
const theme = useMantineTheme();
@@ -20,7 +20,7 @@ const ColorsView: FC = () => {
<Title order={1}>Цвета и семантические роли</Title>
<Text>
Светлая схема WealthLedger. Источник цветов и ролей —
shared/theme/tokens.json. Цвет не заменяет подпись состояния.
@wealthledger/tokens/tokens.json. Цвет не заменяет подпись состояния.
</Text>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }}>
{Object.entries(designTokens.color.roles).map(([role, token]) => (
@@ -168,7 +168,7 @@ const meta = {
docs: {
description: {
component:
"Единый источник shared/theme/tokens.json; theme.ts адаптирует его к Mantine. Все образцы читают effective theme; описания ролей берутся из JSON. Показаны используемые шкалы и роли, а не весь Mantine. Светлая схема; контраст, смысловые заголовки и отсутствие передачи статуса только цветом проверяются в конкретной композиции.",
"Единый источник @wealthledger/tokens/tokens.json; theme.ts адаптирует его к Mantine. Все образцы читают effective theme; описания ролей берутся из JSON. Показаны используемые шкалы и роли, а не весь Mantine. Светлая схема; контраст, смысловые заголовки и отсутствие передачи статуса только цветом проверяются в конкретной композиции.",
},
},
},
+1 -1
View File
@@ -1,5 +1,5 @@
import tokens from "@wealthledger/tokens/tokens.json";
import { describe, expect, it } from "vitest";
import tokens from "./tokens.json";
function luminance(color: string) {
const channels = [1, 3, 5]
-3
View File
@@ -1,3 +0,0 @@
import "./styles.css";
export { colorRoles, cssVariablesResolver, designTokens, theme } from "./theme";
-39
View File
@@ -1,39 +0,0 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--wl-color-canvas);
color: var(--wl-color-text);
}
a {
color: inherit;
text-decoration: none;
}
@media (prefers-reduced-motion: no-preference) {
.mantine-Modal-content {
animation: modal-appear 220ms ease-out;
}
.mantine-Modal-overlay {
animation: modal-backdrop-appear 220ms ease-out;
}
}
@keyframes modal-appear {
from {
opacity: 0;
transform: translateY(8px) scale(0.98);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes modal-backdrop-appear {
from {
opacity: 0;
}
}
-29
View File
@@ -1,29 +0,0 @@
import { DEFAULT_THEME, mergeMantineTheme } from "@mantine/core";
import { describe, expect, it } from "vitest";
import { colorRoles, cssVariablesResolver, theme } from "./theme";
import source from "./tokens.json";
describe("design theme adapter", () => {
it("preserves scale-aware sizes, unscaled breakpoints and shadow layers", () => {
expect(theme.spacing?.md).toBe("calc(1rem * var(--mantine-scale))");
expect(theme.fontSizes?.sm).toBe("calc(0.875rem * var(--mantine-scale))");
expect(theme.breakpoints?.sm).toBe("48em");
expect(theme.shadows?.sm).toContain(
"calc(-0.3125rem * var(--mantine-scale))",
);
expect(theme.shadows?.sm).toContain("rgba(0, 0, 0, 0.04)");
});
it("keeps default-shade roles attached to native filled variables", () => {
const resolved = mergeMantineTheme(DEFAULT_THEME, theme);
const variables = cssVariablesResolver(resolved).variables;
expect(colorRoles.primary).toBe("blue");
expect(variables["--wl-color-primary"]).toBe(
"var(--mantine-color-blue-filled)",
);
expect(variables["--wl-color-canvas"]).toBe("var(--mantine-color-gray-2)");
expect(Object.keys(variables)).toHaveLength(
Object.keys(source.color.roles).length,
);
});
});
-85
View File
@@ -1,85 +0,0 @@
import {
type CSSVariablesResolver,
colorsTuple,
createTheme,
getThemeColor,
type MantinePrimaryShade,
Modal,
rem,
rgba,
} from "@mantine/core";
import designTokens from "./tokens.json";
const { color, typography, spacing, radius, shadows, breakpoints } =
designTokens;
const mapValues = <T, U>(values: Record<string, T>, convert: (value: T) => U) =>
Object.fromEntries(
Object.entries(values).map(([name, value]) => [name, convert(value)]),
);
const colorRoles = mapValues(color.roles, (role) =>
role.shade === null ? role.color : `${role.color}.${role.shade}`,
);
const theme = createTheme({
respectReducedMotion: true,
components: {
Modal: Modal.extend({
defaultProps: {
overlayProps: { blur: 3 },
transitionProps: {
transition: "fade-down",
duration: 220,
exitDuration: 180,
timingFunction: "ease-out",
},
},
}),
},
primaryColor: color.roles.primary.color,
primaryShade: color.primaryShade as MantinePrimaryShade,
defaultRadius: radius.default,
white: color.base.white,
black: color.base.black,
colors: mapValues(color.palettes, colorsTuple),
fontFamily: typography.fontFamily,
fontFamilyMonospace: typography.fontFamilyMonospace,
fontSizes: mapValues(typography.fontSizes, (value) => rem(value)),
fontWeights: mapValues(typography.fontWeights, String),
lineHeights: mapValues(typography.lineHeights, String),
headings: {
fontFamily: typography.fontFamily,
fontWeight: String(typography.fontWeights.bold),
sizes: mapValues(typography.headings, (value) => ({
fontSize: rem(value.fontSize),
lineHeight: String(value.lineHeight),
})),
},
spacing: mapValues(spacing.values, (value) => rem(value)),
radius: mapValues(radius.values, (value) => rem(value)),
shadows: mapValues(shadows.values, (layers) =>
layers
.map(
(layer) =>
`${rem(layer.offsetX)} ${rem(layer.offsetY)} ${rem(layer.blur)} ${rem(layer.spread)} ${rgba(color.base[layer.color as keyof typeof color.base], layer.opacity)}`,
)
.join(", "),
),
breakpoints: mapValues(
breakpoints.values,
(value) => `${value}${breakpoints.unit}`,
),
});
const cssVariablesResolver: CSSVariablesResolver = (resolvedTheme) => ({
variables: Object.fromEntries(
Object.entries(colorRoles).map(([role, reference]) => [
`--wl-color-${role}`,
getThemeColor(reference, resolvedTheme),
]),
),
light: {},
dark: {},
});
export { colorRoles, cssVariablesResolver, designTokens, theme };
-377
View File
@@ -1,377 +0,0 @@
{
"formatVersion": 1,
"description": "Переносимые токены WealthLedger. Размеры px адаптируются к масштабу runtime; breakpoints em не масштабируются. Светлая схема; роли с shade null используют primaryShade.light для палитры или base color без оттенка.",
"color": {
"base": {
"white": "#fff",
"black": "#000"
},
"palettes": {
"blue": [
"#eff6ff",
"#dbeafe",
"#bfdbfe",
"#93c5fd",
"#60a5fa",
"#3b82f6",
"#2563eb",
"#1d4ed8",
"#1e40af",
"#1e3a8a"
],
"gray": [
"#f8f9fa",
"#f1f3f5",
"#e9ecef",
"#dee2e6",
"#ced4da",
"#adb5bd",
"#868e96",
"#495057",
"#343a40",
"#212529"
],
"dark": [
"#C9C9C9",
"#b8b8b8",
"#828282",
"#696969",
"#424242",
"#3b3b3b",
"#2e2e2e",
"#242424",
"#1f1f1f",
"#141414"
],
"green": [
"#ebfbee",
"#d3f9d8",
"#b2f2bb",
"#8ce99a",
"#69db7c",
"#51cf66",
"#40c057",
"#37b24d",
"#2f9e44",
"#237032"
],
"red": [
"#fff5f5",
"#ffe3e3",
"#ffc9c9",
"#ffa8a8",
"#ff8787",
"#ff6b6b",
"#fa5252",
"#f03e3e",
"#e03131",
"#c92a2a"
],
"yellow": [
"#fff9db",
"#fff3bf",
"#ffec99",
"#ffe066",
"#ffd43b",
"#fcc419",
"#fab005",
"#f59f00",
"#f08c00",
"#e67700"
],
"grape": [
"#f8f0fc",
"#f3d9fa",
"#eebefa",
"#e599f7",
"#da77f2",
"#cc5de8",
"#be4bdb",
"#ae3ec9",
"#9c36b5",
"#862e9c"
],
"orange": [
"#fff4e6",
"#ffe8cc",
"#ffd8a8",
"#ffc078",
"#ffa94d",
"#ff922b",
"#fd7e14",
"#f76707",
"#e8590c",
"#d9480f"
]
},
"primaryShade": {
"light": 6,
"dark": 8
},
"roles": {
"canvas": {
"color": "gray",
"shade": 2,
"description": "Фон приложения"
},
"surface": {
"color": "white",
"shade": null,
"description": "Поверхность Paper / формы"
},
"subtle": {
"color": "gray",
"shade": 0,
"description": "Приглушённая поверхность сводки"
},
"text": {
"color": "dark",
"shade": 7,
"description": "Основной текст страницы"
},
"muted": {
"color": "gray",
"shade": 6,
"description": "Вторичный текст; проверяйте контраст при выборе фона"
},
"border": {
"color": "gray",
"shade": 3,
"description": "Граница поверхности"
},
"primary": {
"color": "blue",
"shade": null,
"description": "Основное действие и активная навигация"
},
"info": {
"color": "blue",
"shade": 6,
"description": "Информационное сообщение"
},
"positive": {
"color": "green",
"shade": 9,
"description": "Положительный результат; дополните знаком и текстом"
},
"negative": {
"color": "red",
"shade": 9,
"description": "Ошибка / отрицательный результат; дополните текстом"
},
"warning": {
"color": "yellow",
"shade": 6,
"description": "Предупреждение; не мелкий текст на белом фоне"
}
}
},
"typography": {
"fontFamily": "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, sans-serif",
"fontFamilyMonospace": "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace",
"unit": "px",
"fontSizes": {
"xs": 12.0,
"sm": 14.0,
"md": 16.0,
"lg": 18.0,
"xl": 20.0
},
"fontWeights": {
"regular": 400,
"medium": 600,
"bold": 700
},
"lineHeights": {
"xs": 1.4,
"sm": 1.45,
"md": 1.55,
"lg": 1.6,
"xl": 1.65
},
"headings": {
"h1": {
"fontSize": 34.0,
"lineHeight": 1.3
},
"h2": {
"fontSize": 26.0,
"lineHeight": 1.35
},
"h3": {
"fontSize": 22.0,
"lineHeight": 1.4
},
"h4": {
"fontSize": 18.0,
"lineHeight": 1.45
},
"h5": {
"fontSize": 16.0,
"lineHeight": 1.5
},
"h6": {
"fontSize": 14.0,
"lineHeight": 1.5
}
}
},
"spacing": {
"unit": "px",
"values": {
"xs": 10.0,
"sm": 12.0,
"md": 16.0,
"lg": 20.0,
"xl": 32.0
}
},
"radius": {
"unit": "px",
"default": "md",
"values": {
"xs": 2.0,
"sm": 4.0,
"md": 8.0,
"lg": 16.0,
"xl": 32.0
}
},
"shadows": {
"unit": "px",
"values": {
"xs": [
{
"offsetX": 0.0,
"offsetY": 1.0,
"blur": 3.0,
"spread": 0,
"color": "black",
"opacity": 0.05
},
{
"offsetX": 0.0,
"offsetY": 1.0,
"blur": 2.0,
"spread": 0,
"color": "black",
"opacity": 0.1
}
],
"sm": [
{
"offsetX": 0.0,
"offsetY": 1.0,
"blur": 3.0,
"spread": 0,
"color": "black",
"opacity": 0.05
},
{
"offsetX": 0.0,
"offsetY": 10.0,
"blur": 15.0,
"spread": -5.0,
"color": "black",
"opacity": 0.05
},
{
"offsetX": 0.0,
"offsetY": 7.0,
"blur": 7.0,
"spread": -5.0,
"color": "black",
"opacity": 0.04
}
],
"md": [
{
"offsetX": 0.0,
"offsetY": 1.0,
"blur": 3.0,
"spread": 0,
"color": "black",
"opacity": 0.05
},
{
"offsetX": 0.0,
"offsetY": 20.0,
"blur": 25.0,
"spread": -5.0,
"color": "black",
"opacity": 0.05
},
{
"offsetX": 0.0,
"offsetY": 10.0,
"blur": 10.0,
"spread": -5.0,
"color": "black",
"opacity": 0.04
}
],
"lg": [
{
"offsetX": 0.0,
"offsetY": 1.0,
"blur": 3.0,
"spread": 0,
"color": "black",
"opacity": 0.05
},
{
"offsetX": 0.0,
"offsetY": 28.0,
"blur": 23.0,
"spread": -7.0,
"color": "black",
"opacity": 0.05
},
{
"offsetX": 0.0,
"offsetY": 12.0,
"blur": 12.0,
"spread": -7.0,
"color": "black",
"opacity": 0.04
}
],
"xl": [
{
"offsetX": 0.0,
"offsetY": 1.0,
"blur": 3.0,
"spread": 0,
"color": "black",
"opacity": 0.05
},
{
"offsetX": 0.0,
"offsetY": 36.0,
"blur": 28.0,
"spread": -7.0,
"color": "black",
"opacity": 0.05
},
{
"offsetX": 0.0,
"offsetY": 17.0,
"blur": 17.0,
"spread": -7.0,
"color": "black",
"opacity": 0.04
}
]
}
},
"breakpoints": {
"unit": "em",
"values": {
"xs": 36.0,
"sm": 48.0,
"md": 62.0,
"lg": 75.0,
"xl": 88.0
}
}
}