refactor: source shared design tokens from JSON

This commit is contained in:
2026-09-08 19:43:49 +03:00
parent d762ba5034
commit 0f50f15658
8 changed files with 791 additions and 93 deletions
+8 -4
View File
@@ -12,9 +12,13 @@
Для cross-slice imports используйте alias `@/` и public API, например `@/entities/portfolio`. Vite и TypeScript используют одинаковый alias.
`shared/theme/index.ts` экспортирует единую Mantine theme, семантические color
roles и CSSVariablesResolver и подключает базовые styles. App и Storybook
подключают свои MantineProvider к этому public API. Scales принимаются из Mantine,
локальные domain styles остаются у владельцев. `shared/ui` предоставляет
`shared/theme/tokens.json` владеет опубликованными токенами и описаниями ролей
в переносимом формате WealthLedger v1. `tokens.schema.ts` проверяет source,
`theme.ts` адаптирует его в Mantine. `shared/theme/index.ts` экспортирует theme,
designTokens, семантические color roles и CSSVariablesResolver и подключает
базовые styles. App и Storybook подключают свои MantineProvider к этому public API.
Внешние системы читают raw JSON напрямую, без frontend barrel и его runtime.
Неопубликованные defaults остаются у Mantine, локальные domain styles — у владельцев.
`shared/ui` предоставляет
StateView и LoadingSkeleton; их каталог не включает route-aware AppShell.
Подробнее: [дизайн-система](../../docs/development/design-system.md).
+26 -40
View File
@@ -11,21 +11,7 @@ import {
} from "@mantine/core";
import type { Meta, StoryObj } from "@storybook/react-vite";
import type { FC } from "react";
import { colorRoles } from "./index";
const roleUsage: Record<keyof typeof colorRoles, string> = {
canvas: "Фон приложения",
surface: "Поверхность Paper / формы",
subtle: "Приглушённая поверхность сводки",
text: "Основной текст страницы",
muted: "Вторичный текст; проверяйте контраст при выборе фона",
border: "Граница поверхности",
primary: "Основное действие и активная навигация",
info: "Информационное сообщение",
positive: "Положительный результат; дополните знаком и текстом",
negative: "Ошибка / отрицательный результат; дополните текстом",
warning: "Предупреждение; не мелкий текст на белом фоне",
};
import { colorRoles, designTokens } from "./index";
const ColorsView: FC = () => {
const theme = useMantineTheme();
@@ -33,8 +19,8 @@ const ColorsView: FC = () => {
<Stack>
<Title order={1}>Цвета и семантические роли</Title>
<Text>
Светлая схема WealthLedger. Роли ссылаются на палитры Mantine, а не
хранят вторую копию цветов. Цвет не заменяет подпись состояния.
Светлая схема WealthLedger. Источник цветов и ролей —
shared/theme/tokens.json. Цвет не заменяет подпись состояния.
</Text>
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }}>
{Object.entries(colorRoles).map(([role, color]) => (
@@ -47,34 +33,33 @@ const ColorsView: FC = () => {
{color} · {parseThemeColor({ color, theme }).value}
</Text>
<Text size="sm">
{roleUsage[role as keyof typeof colorRoles]}
{designTokens.color.roles[role].description}
</Text>
</Stack>
</Paper>
))}
</SimpleGrid>
{["blue", "gray", "dark", "green", "red", "yellow", "grape"].map(
(palette) => (
<Stack key={palette} gap="xs">
<Title order={2}>{palette}</Title>
<SimpleGrid cols={{ base: 2, sm: 5, lg: 10 }}>
{theme.colors[palette].map((value, shade) => (
<Paper key={value} p="xs" withBorder>
<Box h={40} bg={value} aria-hidden="true" />
<Text size="sm">
{palette}.{shade}
</Text>
<Text size="xs">{value}</Text>
</Paper>
))}
</SimpleGrid>
</Stack>
),
)}
{Object.keys(designTokens.color.palettes).map((palette) => (
<Stack key={palette} gap="xs">
<Title order={2}>{palette}</Title>
<SimpleGrid cols={{ base: 2, sm: 5, lg: 10 }}>
{theme.colors[palette].map((value, shade) => (
<Paper key={value} p="xs" withBorder>
<Box h={40} bg={value} aria-hidden="true" />
<Text size="sm">
{palette}.{shade}
</Text>
<Text size="xs">{value}</Text>
</Paper>
))}
</SimpleGrid>
</Stack>
))}
<Text>
Blue — фирменная палитра. Gray/dark — нейтрали; green/red/yellow —
статусы. Grape — дополнительный цвет графиков, его предметное значение
задаёт Product. Палитры не гарантируют контраст любых сочетаний.
статусы; orange — дополнительный акцент. Grape — цвет графиков, его
предметное значение задаёт Product. Палитры не гарантируют контраст
любых сочетаний.
</Text>
</Stack>
);
@@ -131,7 +116,8 @@ const LayoutView: FC = () => {
<Title order={1}>Интервалы, форма и адаптивность</Title>
<Text>
Используйте Mantine props gap/p/radius/shadow или --mantine-* в CSS.
Шкалы приняты из установленной Mantine без копий чисел. Default radius:
Шкалы принадлежат tokens.json; адаптер Mantine сохраняет
масштабирование. Default radius:
{theme.defaultRadius}; scale: {theme.scale}. Размеры конкретных карточек
и их особые тени остаются локальными.
</Text>
@@ -183,7 +169,7 @@ const meta = {
docs: {
description: {
component:
"Единый shared/theme приложения и каталога. Все образцы читают effective Mantine theme. Показаны используемые шкалы и роли, а не весь Mantine. Светлая схема; контраст, смысловые заголовки и отсутствие передачи статуса только цветом проверяются в конкретной композиции.",
"Единый источник shared/theme/tokens.json; theme.ts адаптирует его к Mantine. Все образцы читают effective theme; описания ролей берутся из JSON. Показаны используемые шкалы и роли, а не весь Mantine. Светлая схема; контраст, смысловые заголовки и отсутствие передачи статуса только цветом проверяются в конкретной композиции.",
},
},
},
+1 -1
View File
@@ -1,3 +1,3 @@
import "./styles.css";
export { colorRoles, cssVariablesResolver, theme } from "./theme";
export { colorRoles, cssVariablesResolver, designTokens, theme } from "./theme";
+53 -48
View File
@@ -1,69 +1,74 @@
import {
type CSSVariablesResolver,
createTheme,
DEFAULT_THEME,
getThemeColor,
rem,
rgba,
} from "@mantine/core";
import source from "./tokens.json";
import { designTokensSchema } from "./tokens.schema";
// Native Mantine color references, not a second palette. Light scheme only.
const colorRoles = {
canvas: "gray.2",
surface: "white",
subtle: "gray.0",
text: "dark.7",
muted: "gray.6",
border: "gray.3",
primary: "blue",
info: "blue.6",
positive: "green.6",
negative: "red.6",
warning: "yellow.6",
} as const;
const designTokens = designTokensSchema.parse(source);
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 fontFamily =
'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
const colorRoles = mapValues(color.roles, (role) =>
role.shade === null ? role.color : `${role.color}.${role.shade}`,
);
const theme = createTheme({
primaryColor: "blue",
defaultRadius: "md",
fontFamily,
// Explicitly adopt the installed Mantine scales without duplicating values.
fontFamilyMonospace: DEFAULT_THEME.fontFamilyMonospace,
fontSizes: DEFAULT_THEME.fontSizes,
fontWeights: DEFAULT_THEME.fontWeights,
lineHeights: DEFAULT_THEME.lineHeights,
headings: { ...DEFAULT_THEME.headings, fontFamily },
spacing: DEFAULT_THEME.spacing,
radius: DEFAULT_THEME.radius,
shadows: DEFAULT_THEME.shadows,
breakpoints: DEFAULT_THEME.breakpoints,
primaryShade: DEFAULT_THEME.primaryShade,
focusRing: DEFAULT_THEME.focusRing,
colors: {
blue: [
"#eff6ff",
"#dbeafe",
"#bfdbfe",
"#93c5fd",
"#60a5fa",
"#3b82f6",
"#2563eb",
"#1d4ed8",
"#1e40af",
"#1e3a8a",
],
primaryColor: color.roles.primary.color,
primaryShade: color.primaryShade,
defaultRadius: radius.default,
white: color.base.white,
black: color.base.black,
colors: mapValues(color.palettes, (palette) => {
// Source schema guarantees exactly ten entries before tuple conversion.
const [a, b, c, d, e, f, g, h, i, j] = palette;
return [a, b, c, d, e, f, g, h, i, j] as const;
}),
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], layer.opacity)}`,
)
.join(", "),
),
breakpoints: mapValues(
breakpoints.values,
(value) => `${value}${breakpoints.unit}`,
),
});
const cssVariablesResolver: CSSVariablesResolver = (resolvedTheme) => ({
variables: Object.fromEntries(
Object.entries(colorRoles).map(([role, color]) => [
Object.entries(colorRoles).map(([role, reference]) => [
`--wl-color-${role}`,
getThemeColor(color, resolvedTheme),
getThemeColor(reference, resolvedTheme),
]),
),
light: {},
dark: {},
});
export { colorRoles, cssVariablesResolver, theme };
export { colorRoles, cssVariablesResolver, designTokens, theme };
+377
View File
@@ -0,0 +1,377 @@
{
"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",
"#2b8a3e"
],
"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": 6,
"description": "Положительный результат; дополните знаком и текстом"
},
"negative": {
"color": "red",
"shade": 6,
"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
}
}
}
+149
View File
@@ -0,0 +1,149 @@
import { z } from "zod";
const hexColor = z.string().regex(/^#(?:[\da-f]{3}|[\da-f]{6})$/i);
const finite = z.number().finite();
const positive = finite.positive();
const shade = z.union([
z.literal(0),
z.literal(1),
z.literal(2),
z.literal(3),
z.literal(4),
z.literal(5),
z.literal(6),
z.literal(7),
z.literal(8),
z.literal(9),
]);
const scale = <T extends z.ZodTypeAny>(value: T) =>
z.object({ xs: value, sm: value, md: value, lg: value, xl: value }).strict();
const role = z
.object({
color: z.string().min(1),
shade: shade.nullable(),
description: z.string().min(1),
})
.strict();
const heading = z.object({ fontSize: positive, lineHeight: positive }).strict();
const shadow = z
.object({
offsetX: finite,
offsetY: finite,
blur: finite.nonnegative(),
spread: finite,
color: z.enum(["white", "black"]),
opacity: finite.min(0).max(1),
})
.strict();
const designTokensSchema = z
.object({
formatVersion: z.literal(1),
description: z.string().min(1),
color: z
.object({
base: z.object({ white: hexColor, black: hexColor }).strict(),
palettes: z.record(
z
.string()
.regex(/^[a-z][a-z0-9-]*$/)
.refine(
(name) => name !== "dimmed" && name !== "bright",
"Palette name is reserved by Mantine",
),
z.array(hexColor).length(10),
),
primaryShade: z.object({ light: shade, dark: shade }).strict(),
roles: z.record(z.string().regex(/^[a-z][a-z0-9-]*$/), role),
})
.strict(),
typography: z
.object({
fontFamily: z.string().min(1),
fontFamilyMonospace: z.string().min(1),
unit: z.literal("px"),
fontSizes: scale(positive),
fontWeights: z
.object({
regular: finite.min(1).max(1000),
medium: finite.min(1).max(1000),
bold: finite.min(1).max(1000),
})
.strict(),
lineHeights: scale(positive),
headings: z
.object({
h1: heading,
h2: heading,
h3: heading,
h4: heading,
h5: heading,
h6: heading,
})
.strict(),
})
.strict(),
spacing: z
.object({ unit: z.literal("px"), values: scale(finite.nonnegative()) })
.strict(),
radius: z
.object({
unit: z.literal("px"),
default: z.enum(["xs", "sm", "md", "lg", "xl"]),
values: scale(finite.nonnegative()),
})
.strict(),
shadows: z
.object({ unit: z.literal("px"), values: scale(z.array(shadow).min(1)) })
.strict(),
breakpoints: z
.object({ unit: z.literal("em"), values: scale(positive) })
.strict(),
})
.strict()
.superRefine((tokens, context) => {
const { base, palettes, roles } = tokens.color;
for (const required of ["canvas", "text", "primary"]) {
if (!Object.hasOwn(roles, required)) {
context.addIssue({
code: "custom",
path: ["color", "roles", required],
message: "Required semantic role is missing",
});
}
}
for (const [name, value] of Object.entries(roles)) {
const isPalette = Object.hasOwn(palettes, value.color);
const isBase = Object.hasOwn(base, value.color);
if ((!isPalette && !isBase) || (isBase && value.shade !== null)) {
context.addIssue({
code: "custom",
path: ["color", "roles", name],
message:
"Role must reference an existing palette shade or an unshaded base color",
});
}
}
if (
roles.primary &&
(!Object.hasOwn(palettes, roles.primary.color) ||
roles.primary.shade !== null)
) {
context.addIssue({
code: "custom",
path: ["color", "roles", "primary"],
message: "Primary must reference a palette with the default shade",
});
}
for (const name of Object.keys(base)) {
if (Object.hasOwn(palettes, name)) {
context.addIssue({
code: "custom",
path: ["color", "palettes", name],
message: "Palette name conflicts with a base color",
});
}
}
});
export { designTokensSchema };
+176
View File
@@ -0,0 +1,176 @@
import { DEFAULT_THEME, mergeMantineTheme } from "@mantine/core";
import { describe, expect, it } from "vitest";
import { colorRoles, cssVariablesResolver, theme } from "./theme";
import source from "./tokens.json";
import { designTokensSchema } from "./tokens.schema";
describe("portable design tokens", () => {
it("accepts the document, including negative shadow spread and default-shade aliases", () => {
const tokens = designTokensSchema.parse(source);
expect(tokens.shadows.values.sm[1].spread).toBe(-5);
expect(tokens.color.roles.primary.shade).toBeNull();
});
it.each([
[
"unsupported dimension unit",
{ ...source, spacing: { ...source.spacing, unit: "rem" } },
],
[
"incomplete palette",
{
...source,
color: {
...source.color,
palettes: {
...source.color.palettes,
blue: source.color.palettes.blue.slice(1),
},
},
},
],
[
"missing reference",
{
...source,
color: {
...source.color,
roles: {
...source.color.roles,
canvas: { ...source.color.roles.canvas, color: "missing" },
},
},
},
],
[
"inherited property as reference",
{
...source,
color: {
...source.color,
roles: {
...source.color.roles,
canvas: { ...source.color.roles.canvas, color: "constructor" },
},
},
},
],
[
"shade on base color",
{
...source,
color: {
...source.color,
roles: {
...source.color.roles,
surface: { ...source.color.roles.surface, shade: 2 },
},
},
},
],
[
"fixed primary shade",
{
...source,
color: {
...source.color,
roles: {
...source.color.roles,
primary: { ...source.color.roles.primary, shade: 6 },
},
},
},
],
[
"missing required roles",
{ ...source, color: { ...source.color, roles: {} } },
],
[
"ambiguous color name",
{
...source,
color: {
...source.color,
palettes: {
...source.color.palettes,
white: source.color.palettes.blue,
},
},
},
],
[
"out of range shade",
{
...source,
color: { ...source.color, primaryShade: { light: 10, dark: 8 } },
},
],
[
"invalid shadow opacity",
{
...source,
shadows: {
...source.shadows,
values: {
...source.shadows.values,
xs: [{ ...source.shadows.values.xs[0], opacity: 2 }],
},
},
},
],
])("rejects %s without fallback", (_name, document) => {
expect(designTokensSchema.safeParse(document).success).toBe(false);
});
it.each(["dimmed", "bright"])("rejects reserved palette name %s", (name) => {
const document = {
...source,
color: {
...source.color,
palettes: {
...source.color.palettes,
[name]: source.color.palettes.blue,
},
roles: {
...source.color.roles,
primary: { ...source.color.roles.primary, color: name },
},
},
};
expect(designTokensSchema.safeParse(document).success).toBe(false);
});
it("rejects weights below the CSS range", () => {
const document = {
...source,
typography: {
...source.typography,
fontWeights: { ...source.typography.fontWeights, regular: 0.5 },
},
};
expect(designTokensSchema.safeParse(document).success).toBe(false);
});
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,
);
});
});
+1
View File
@@ -13,6 +13,7 @@
"skipLibCheck": true,
"strict": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",