@umituz/web-dashboard 4.0.0 → 4.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -8
- package/package.json +3 -2
- package/src/domains/analytics/components/AnalyticsCard.tsx +32 -33
- package/src/domains/analytics/components/MetricCard.tsx +23 -59
- package/src/domains/settings/components/UserManagementTable.tsx +32 -50
- package/src/domains/settings/utils/index.ts +1 -0
- package/src/i18n/defaultT.ts +1 -1
- package/src/primitives/Button.tsx +8 -14
- package/src/primitives/Checkbox.tsx +7 -30
- package/src/primitives/Input.tsx +13 -9
- package/src/primitives/Skeleton.tsx +3 -23
- package/src/primitives/cn.ts +6 -11
- package/src/styles/dashboard.css +17 -331
- package/src/styles/tokens.contract.test.ts +25 -0
- package/src/styles/tokens.css +59 -67
package/README.md
CHANGED
|
@@ -14,11 +14,8 @@ Your bundler compiles it alongside your app; no prebuilt bundle ships.
|
|
|
14
14
|
|
|
15
15
|
- **Billing moved to [@umituz/web-polar-payment](https://github.com/umituz/web-polar-payment)**
|
|
16
16
|
under the `./portal` entry point. The old `./billing*` entry points are gone.
|
|
17
|
-
- **`@umituz/web-design-system` is no longer a dependency.** The primitives
|
|
18
|
-
(`Button`, `Input`, `Checkbox`, `Skeleton`, `cn`) live in `./primitives`.
|
|
19
17
|
- **Styling is shipped, not assumed.** Import `@umituz/web-dashboard/styles.css`
|
|
20
|
-
|
|
21
|
-
Tailwind, design-system tokens and utility classes are gone from the package.
|
|
18
|
+
once; all components use `udb-*` BEM classes.
|
|
22
19
|
- **No i18n runtime.** `react-i18next` is out; hosts bridge their own `t` via
|
|
23
20
|
`<DashboardI18nProvider t={i18n.t}>` (see below). Without a provider,
|
|
24
21
|
keys humanize to readable English.
|
|
@@ -30,6 +27,31 @@ Your bundler compiles it alongside your app; no prebuilt bundle ships.
|
|
|
30
27
|
texora/visora pattern (one module list drives routes + nav + guards) as
|
|
31
28
|
reusable building blocks.
|
|
32
29
|
|
|
30
|
+
## What changed in 4.1
|
|
31
|
+
|
|
32
|
+
- **The design system is back — for real this time.** 4.0 dropped
|
|
33
|
+
`@umituz/web-design-system` entirely; 4.1 makes it a runtime dependency
|
|
34
|
+
again and actually renders it:
|
|
35
|
+
- `./primitives` (`Button`, `Input`, `Checkbox`, `Skeleton`, `cn`) are thin
|
|
36
|
+
adapters that **delegate to the DS components** — the dashboard prop API
|
|
37
|
+
is unchanged, call sites need zero edits.
|
|
38
|
+
- `UserManagementTable` renders the DS `DataTable`; `MetricCard` composes
|
|
39
|
+
the DS `MetricCard`; `AnalyticsCard` is built on DS `Card` primitives.
|
|
40
|
+
- `styles.css` imports `@umituz/web-design-system/styles.css` transitively
|
|
41
|
+
(Tailwind v4 precompiled) — hosts still import only the dashboard
|
|
42
|
+
stylesheet.
|
|
43
|
+
- **`--u-*` tokens are now bridges.** `tokens.css` maps every color/radius/
|
|
44
|
+
shadow token to the DS `--color-*`/`--radius-*` variables (with the old
|
|
45
|
+
literal as fallback). Override either side; dark mode (`data-udb-theme="dark"`)
|
|
46
|
+
now sets DS variables and the bridge flows through.
|
|
47
|
+
- **Primitive base CSS is gone from `dashboard.css`.** Only dashboard-owned
|
|
48
|
+
modifiers remain (`udb-btn__spinner`, `udb-input--error/--with-toggle`,
|
|
49
|
+
`udb-skeleton--*`, `udb-card--sm/--lg`, `udb-table` for the permission
|
|
50
|
+
matrix); bases come from the DS stylesheet.
|
|
51
|
+
- **Peer footprint**: pulling the dashboard in transitively installs the DS's
|
|
52
|
+
Radix UI peers (npm 7+ handles this automatically; only imported components
|
|
53
|
+
end up in the bundle).
|
|
54
|
+
|
|
33
55
|
## Installation
|
|
34
56
|
|
|
35
57
|
```bash
|
|
@@ -38,7 +60,7 @@ import "@umituz/web-dashboard/styles.css";
|
|
|
38
60
|
```
|
|
39
61
|
|
|
40
62
|
Peers: `react` + `react-dom` (^18 or ^19) and `react-router-dom` (^6 or ^7).
|
|
41
|
-
Runtime deps: `
|
|
63
|
+
Runtime deps: `@umituz/web-design-system` + `lucide-react`. Nothing else.
|
|
42
64
|
|
|
43
65
|
### Tailwind hosts
|
|
44
66
|
|
|
@@ -48,8 +70,12 @@ resets: import the package stylesheet **after** your base layer.
|
|
|
48
70
|
|
|
49
71
|
### Theming & dark mode
|
|
50
72
|
|
|
51
|
-
|
|
52
|
-
|
|
73
|
+
`tokens.css` bridges every `--u-*` token to a design-system variable
|
|
74
|
+
(`--u-color-primary: var(--color-primary, #2563eb)`), so you can theme two
|
|
75
|
+
ways: override the `--u-*` bridge heads, or set the DS `--color-*`/`--radius-*`
|
|
76
|
+
variables (e.g. from your own Tailwind `@theme`). Dark mode is host-driven:
|
|
77
|
+
put `data-udb-theme="dark"` on any ancestor — the dashboard dark block sets
|
|
78
|
+
the DS variables and both layers switch together.
|
|
53
79
|
|
|
54
80
|
## Entry points
|
|
55
81
|
|
|
@@ -175,7 +201,7 @@ readably. The per-domain key catalogs are the string contract:
|
|
|
175
201
|
import { AUTH_KEYS } from "@umituz/web-dashboard/auth";
|
|
176
202
|
import { ANALYTICS_KEYS } from "@umituz/web-dashboard/analytics";
|
|
177
203
|
import { ONBOARDING_KEYS } from "@umituz/web-dashboard/onboarding";
|
|
178
|
-
import { SETTINGS_ADMIN_KEYS } from "@umituz/web-dashboard/settings/utils
|
|
204
|
+
import { SETTINGS_ADMIN_KEYS } from "@umituz/web-dashboard/settings/utils";
|
|
179
205
|
import { PERMISSIONS_KEYS } from "@umituz/web-dashboard/permissions";
|
|
180
206
|
import { SESSION_KEYS } from "@umituz/web-dashboard/session";
|
|
181
207
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/web-dashboard",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "Installable app skeleton for the fleet — layouts, auth, onboarding, analytics (dependency-free SVG charts), calendar, i18n injection, permissions/session/module registry; ships its own udb-* stylesheet and --u-* tokens",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -71,10 +71,11 @@
|
|
|
71
71
|
"react-router-dom": "^6.0.0 || ^7.0.0"
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"
|
|
74
|
+
"@umituz/web-design-system": "^4.0.2",
|
|
75
75
|
"lucide-react": "^0.577.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
+
"@types/node": "^22.20.5",
|
|
78
79
|
"@types/react": "^19.2.14",
|
|
79
80
|
"@types/react-dom": "^19.2.3",
|
|
80
81
|
"@typescript-eslint/eslint-plugin": "^8.57.2",
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Analytics Card
|
|
3
|
-
*
|
|
4
|
-
* Configurable analytics card with chart or metrics. Surface comes
|
|
5
|
-
* from the shared `udb-card` helper; size modifiers shrink/grow the
|
|
6
|
-
* padding ("full" shares the md default).
|
|
2
|
+
* Analytics Card — chart/metric container on the design-system Card.
|
|
3
|
+
* Size modifiers ("sm"/"lg") are dashboard-owned padding overrides.
|
|
7
4
|
*/
|
|
8
5
|
|
|
9
6
|
import { Loader2, AlertCircle } from "lucide-react";
|
|
7
|
+
import {
|
|
8
|
+
Card,
|
|
9
|
+
CardHeader,
|
|
10
|
+
CardTitle,
|
|
11
|
+
CardDescription,
|
|
12
|
+
CardContent,
|
|
13
|
+
} from "@umituz/web-design-system/organisms";
|
|
10
14
|
import { cn } from "../../../primitives";
|
|
11
15
|
import type { AnalyticsCardProps } from "../types/analytics";
|
|
12
16
|
import { MetricCard } from "./MetricCard";
|
|
13
17
|
import { AnalyticsChart } from "./AnalyticsChart";
|
|
14
18
|
|
|
15
19
|
const SURFACE_CLASS = {
|
|
16
|
-
sm:
|
|
20
|
+
sm: "udb-card--sm",
|
|
17
21
|
md: undefined,
|
|
18
|
-
lg:
|
|
22
|
+
lg: "udb-card--lg",
|
|
19
23
|
full: undefined,
|
|
20
24
|
} as const;
|
|
21
25
|
|
|
@@ -32,50 +36,45 @@ export const AnalyticsCard = ({
|
|
|
32
36
|
}: AnalyticsCardProps) => {
|
|
33
37
|
if (loading) {
|
|
34
38
|
return (
|
|
35
|
-
<
|
|
39
|
+
<Card className={cn(SURFACE_CLASS[size], className)}>
|
|
36
40
|
<div className="udb-analytics-card__status" role="status">
|
|
37
41
|
<Loader2 className="udb-spin udb-analytics-card__spinner" aria-hidden="true" />
|
|
38
42
|
</div>
|
|
39
|
-
</
|
|
43
|
+
</Card>
|
|
40
44
|
);
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
if (error) {
|
|
44
48
|
return (
|
|
45
|
-
<
|
|
49
|
+
<Card className={cn(SURFACE_CLASS[size], className)}>
|
|
46
50
|
<div className="udb-analytics-card__status udb-analytics-card__status--error" role="alert">
|
|
47
51
|
<AlertCircle className="udb-analytics-card__error-icon" aria-hidden="true" />
|
|
48
52
|
<p className="udb-analytics-card__error-text">{error}</p>
|
|
49
53
|
</div>
|
|
50
|
-
</
|
|
54
|
+
</Card>
|
|
51
55
|
);
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
return (
|
|
55
|
-
<
|
|
56
|
-
{/* Header */}
|
|
59
|
+
<Card className={cn(SURFACE_CLASS[size], className)}>
|
|
57
60
|
{(title || description) && (
|
|
58
|
-
<
|
|
59
|
-
{title && <
|
|
60
|
-
{description && <
|
|
61
|
-
</
|
|
62
|
-
)}
|
|
63
|
-
|
|
64
|
-
{/* Chart */}
|
|
65
|
-
{chart && <AnalyticsChart config={chart} />}
|
|
66
|
-
|
|
67
|
-
{/* Metrics */}
|
|
68
|
-
{metrics && metrics.length > 0 && (
|
|
69
|
-
<div className="udb-analytics-card__metrics">
|
|
70
|
-
{metrics.map((metric) => (
|
|
71
|
-
<MetricCard key={metric.id} metric={metric} size="sm" />
|
|
72
|
-
))}
|
|
73
|
-
</div>
|
|
61
|
+
<CardHeader>
|
|
62
|
+
{title && <CardTitle>{title}</CardTitle>}
|
|
63
|
+
{description && <CardDescription>{description}</CardDescription>}
|
|
64
|
+
</CardHeader>
|
|
74
65
|
)}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
66
|
+
<CardContent>
|
|
67
|
+
{chart && <AnalyticsChart config={chart} />}
|
|
68
|
+
{metrics && metrics.length > 0 && (
|
|
69
|
+
<div className="udb-analytics-card__metrics">
|
|
70
|
+
{metrics.map((metric) => (
|
|
71
|
+
<MetricCard key={metric.id} metric={metric} size="sm" />
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
{children}
|
|
76
|
+
</CardContent>
|
|
77
|
+
</Card>
|
|
79
78
|
);
|
|
80
79
|
};
|
|
81
80
|
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MetricCard
|
|
3
|
-
*
|
|
4
|
-
* Single KPI tile with a trend indicator. Colors and sizes come
|
|
5
|
-
* from `--u-*` tokens via the `udb-metric-*` classes (no hardcoded
|
|
6
|
-
* green/orange) and a11y role=button when interactive.
|
|
2
|
+
* MetricCard — dashboard logic (growth, i18n, a11y) on top of the
|
|
3
|
+
* design-system MetricCard presentation.
|
|
7
4
|
*/
|
|
8
5
|
|
|
9
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
MetricCard as DSMetricCard,
|
|
8
|
+
} from "@umituz/web-design-system/organisms";
|
|
10
9
|
import { useT } from "../../../i18n";
|
|
11
|
-
import { cn } from "../../../primitives";
|
|
12
10
|
import type { MetricCardProps } from "../types/analytics";
|
|
13
11
|
import { formatMetricValue, calculateGrowth, getTrend } from "../utils/analytics";
|
|
14
12
|
import { ANALYTICS_KEYS } from "../utils/i18nKeys";
|
|
@@ -25,61 +23,27 @@ export const MetricCard = ({
|
|
|
25
23
|
|
|
26
24
|
const hasPrevious = metric.previousValue !== undefined;
|
|
27
25
|
const growth = hasPrevious ? calculateGrowth(metric.value, metric.previousValue as number) : null;
|
|
28
|
-
const trend = growth !== null ? getTrend(growth) :
|
|
29
|
-
|
|
30
|
-
const TrendIcon = trend === 'up' ? ArrowUp : trend === 'down' ? ArrowDown : Minus;
|
|
31
|
-
|
|
32
|
-
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
33
|
-
if (!onClick) return;
|
|
34
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
35
|
-
e.preventDefault();
|
|
36
|
-
onClick();
|
|
37
|
-
}
|
|
38
|
-
};
|
|
26
|
+
const trend = growth !== null ? getTrend(growth) : "stable";
|
|
39
27
|
|
|
40
28
|
return (
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
<DSMetricCard
|
|
30
|
+
className={className}
|
|
31
|
+
title={metric.name}
|
|
32
|
+
value={formatMetricValue(metric)}
|
|
33
|
+
icon={showIcon ? metric.icon : undefined}
|
|
34
|
+
iconColor="text-primary"
|
|
35
|
+
size={size}
|
|
36
|
+
trend={
|
|
37
|
+
showTrend && growth !== null && trend !== "stable"
|
|
38
|
+
? {
|
|
39
|
+
value: growth,
|
|
40
|
+
label: t(ANALYTICS_KEYS.common.lastPeriod),
|
|
41
|
+
}
|
|
42
|
+
: undefined
|
|
43
|
+
}
|
|
44
|
+
trendValueFormatter={(v) => `${Math.abs(v).toFixed(1)}%`}
|
|
45
45
|
onClick={onClick}
|
|
46
|
-
|
|
47
|
-
className={cn(
|
|
48
|
-
'udb-metric',
|
|
49
|
-
`udb-metric--${size}`,
|
|
50
|
-
onClick && 'udb-metric--interactive',
|
|
51
|
-
className,
|
|
52
|
-
)}
|
|
53
|
-
>
|
|
54
|
-
<div className="udb-metric__head">
|
|
55
|
-
<div className="udb-metric__body">
|
|
56
|
-
<p className="udb-metric__name">{metric.name}</p>
|
|
57
|
-
|
|
58
|
-
<div className="udb-metric__value">{formatMetricValue(metric)}</div>
|
|
59
|
-
|
|
60
|
-
{showTrend && hasPrevious && growth !== null && (
|
|
61
|
-
<div
|
|
62
|
-
className={cn('udb-metric__trend', `udb-metric__trend--${trend}`)}
|
|
63
|
-
aria-label={t(ANALYTICS_KEYS.metric.change)}
|
|
64
|
-
>
|
|
65
|
-
<TrendIcon className="udb-metric__trend-icon" aria-hidden="true" />
|
|
66
|
-
<span className="udb-metric__trend-value">
|
|
67
|
-
{Math.abs(growth).toFixed(1)}%
|
|
68
|
-
</span>
|
|
69
|
-
<span className="udb-metric__trend-period">
|
|
70
|
-
{t(ANALYTICS_KEYS.common.lastPeriod)}
|
|
71
|
-
</span>
|
|
72
|
-
</div>
|
|
73
|
-
)}
|
|
74
|
-
</div>
|
|
75
|
-
|
|
76
|
-
{showIcon && metric.icon && (
|
|
77
|
-
<div className="udb-metric__icon-box">
|
|
78
|
-
<metric.icon className="udb-metric__icon" aria-hidden="true" />
|
|
79
|
-
</div>
|
|
80
|
-
)}
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
46
|
+
/>
|
|
83
47
|
);
|
|
84
48
|
};
|
|
85
49
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* UserManagementTable — admin skeleton for the users screen.
|
|
3
3
|
* Config-driven: rows and role catalog come in as props, role
|
|
4
|
-
* changes delegate out.
|
|
5
|
-
* the empty state.
|
|
4
|
+
* changes delegate out.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
import {
|
|
7
|
+
import { DataTable, type Column } from "@umituz/web-design-system/organisms";
|
|
9
8
|
import { useT } from "../../../i18n";
|
|
10
9
|
import { SETTINGS_ADMIN_KEYS } from "../utils/i18nKeys";
|
|
11
10
|
import type { RoleKey, RoleInfo } from "../../permissions/types";
|
|
@@ -32,55 +31,38 @@ export const UserManagementTable = ({
|
|
|
32
31
|
}: UserManagementTableProps) => {
|
|
33
32
|
const t = useT();
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
const columns: Column<ManagedUser>[] = [
|
|
35
|
+
{ id: "name", header: t("settings.users.columns.name"), accessor: "name" },
|
|
36
|
+
{ id: "email", header: t("settings.users.columns.email"), accessor: "email" },
|
|
37
|
+
{
|
|
38
|
+
id: "role",
|
|
39
|
+
header: t("settings.users.columns.role"),
|
|
40
|
+
accessor: (user) =>
|
|
41
|
+
onRoleChange ? (
|
|
42
|
+
<select
|
|
43
|
+
className="udb-period-select__select"
|
|
44
|
+
value={user.role}
|
|
45
|
+
aria-label={t("settings.users.columns.role")}
|
|
46
|
+
onChange={(e) => onRoleChange(user.id, e.target.value as RoleKey)}
|
|
47
|
+
>
|
|
48
|
+
{roles.map((role) => (
|
|
49
|
+
<option key={role.key} value={role.key}>
|
|
50
|
+
{role.label}
|
|
51
|
+
</option>
|
|
52
|
+
))}
|
|
53
|
+
</select>
|
|
54
|
+
) : (
|
|
55
|
+
roles.find((role) => role.key === user.role)?.label ?? user.role
|
|
56
|
+
),
|
|
57
|
+
},
|
|
58
|
+
];
|
|
45
59
|
|
|
46
60
|
return (
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
<th className="udb-table__th">{t("settings.users.columns.email")}</th>
|
|
53
|
-
<th className="udb-table__th">{t("settings.users.columns.role")}</th>
|
|
54
|
-
</tr>
|
|
55
|
-
</thead>
|
|
56
|
-
<tbody>
|
|
57
|
-
{users.map((user) => (
|
|
58
|
-
<tr key={user.id} className="udb-table__row">
|
|
59
|
-
<td className="udb-table__td">{user.name}</td>
|
|
60
|
-
<td className="udb-table__td">{user.email}</td>
|
|
61
|
-
<td className="udb-table__td">
|
|
62
|
-
{onRoleChange ? (
|
|
63
|
-
<select
|
|
64
|
-
className="udb-period-select__select"
|
|
65
|
-
value={user.role}
|
|
66
|
-
aria-label={t("settings.users.columns.role")}
|
|
67
|
-
onChange={(e) => onRoleChange(user.id, e.target.value)}
|
|
68
|
-
>
|
|
69
|
-
{roles.map((role) => (
|
|
70
|
-
<option key={role.key} value={role.key}>
|
|
71
|
-
{role.label}
|
|
72
|
-
</option>
|
|
73
|
-
))}
|
|
74
|
-
</select>
|
|
75
|
-
) : (
|
|
76
|
-
roles.find((role) => role.key === user.role)?.label ?? user.role
|
|
77
|
-
)}
|
|
78
|
-
</td>
|
|
79
|
-
</tr>
|
|
80
|
-
))}
|
|
81
|
-
</tbody>
|
|
82
|
-
</table>
|
|
83
|
-
</div>
|
|
61
|
+
<DataTable
|
|
62
|
+
data={users}
|
|
63
|
+
columns={columns}
|
|
64
|
+
emptyState={{ title: t(SETTINGS_ADMIN_KEYS.users.empty) }}
|
|
65
|
+
/>
|
|
84
66
|
);
|
|
85
67
|
};
|
|
86
68
|
|
package/src/i18n/defaultT.ts
CHANGED
|
@@ -29,6 +29,6 @@ const interpolate = (text: string, params?: Record<string, unknown>): string =>
|
|
|
29
29
|
|
|
30
30
|
export const defaultT: TranslateFn = (key, params) => {
|
|
31
31
|
const segments = key.split(".").filter(Boolean);
|
|
32
|
-
const source = segments
|
|
32
|
+
const source = segments[segments.length - 1] ?? key;
|
|
33
33
|
return interpolate(humanizeSegment(source), params);
|
|
34
34
|
};
|
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Button —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* default/destructive/outline/ghost, sizes default/sm/icon. No `asChild`
|
|
6
|
-
* (radix-free), no variant library — classes come from `styles.css`.
|
|
2
|
+
* Button — delegates to the design system.
|
|
3
|
+
* Dashboard surface: variants default/destructive/outline/ghost,
|
|
4
|
+
* sizes default/sm/icon. Call-site API unchanged.
|
|
7
5
|
*/
|
|
8
6
|
|
|
9
|
-
import { forwardRef
|
|
10
|
-
import {
|
|
7
|
+
import { forwardRef } from 'react';
|
|
8
|
+
import { Button as DSButton } from '@umituz/web-design-system/atoms';
|
|
11
9
|
|
|
12
10
|
export type ButtonVariant = 'default' | 'destructive' | 'outline' | 'ghost';
|
|
13
11
|
export type ButtonSize = 'default' | 'sm' | 'icon';
|
|
14
12
|
|
|
15
|
-
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
13
|
+
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
16
14
|
variant?: ButtonVariant;
|
|
17
15
|
size?: ButtonSize;
|
|
18
16
|
}
|
|
19
17
|
|
|
20
18
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
21
|
-
({
|
|
22
|
-
<
|
|
23
|
-
ref={ref}
|
|
24
|
-
className={cn('udb-btn', `udb-btn--${variant}`, `udb-btn--${size}`, className)}
|
|
25
|
-
{...props}
|
|
26
|
-
/>
|
|
19
|
+
({ variant = 'default', size = 'default', ...props }, ref) => (
|
|
20
|
+
<DSButton ref={ref} variant={variant} size={size} {...props} />
|
|
27
21
|
),
|
|
28
22
|
);
|
|
29
23
|
|
|
@@ -1,40 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Checkbox —
|
|
3
|
-
*
|
|
4
|
-
* Keeps the `onCheckedChange` convenience this package relies on; every
|
|
5
|
-
* other native attribute passes straight through.
|
|
2
|
+
* Checkbox — delegates to the design system (native + onCheckedChange).
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
|
-
import { forwardRef
|
|
9
|
-
import {
|
|
5
|
+
import { forwardRef } from 'react';
|
|
6
|
+
import { Checkbox as DSCheckbox } from '@umituz/web-design-system/atoms';
|
|
10
7
|
|
|
11
|
-
export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
|
8
|
+
export interface CheckboxProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
|
12
9
|
checked?: boolean;
|
|
13
10
|
onCheckedChange?: (checked: boolean) => void;
|
|
14
11
|
}
|
|
15
12
|
|
|
16
|
-
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const handleChange = useCallback(
|
|
20
|
-
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
21
|
-
onCheckedChange?.(e.target.checked);
|
|
22
|
-
onChange?.(e);
|
|
23
|
-
},
|
|
24
|
-
[onCheckedChange, onChange],
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
return (
|
|
28
|
-
<input
|
|
29
|
-
ref={ref}
|
|
30
|
-
type="checkbox"
|
|
31
|
-
checked={checked}
|
|
32
|
-
onChange={handleChange}
|
|
33
|
-
className={cn('udb-checkbox', className)}
|
|
34
|
-
{...props}
|
|
35
|
-
/>
|
|
36
|
-
);
|
|
37
|
-
},
|
|
38
|
-
);
|
|
13
|
+
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>((props, ref) => (
|
|
14
|
+
<DSCheckbox ref={ref} {...props} />
|
|
15
|
+
));
|
|
39
16
|
|
|
40
17
|
Checkbox.displayName = 'Checkbox';
|
package/src/primitives/Input.tsx
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Input —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* token-driven styling. Everything else passes through untouched.
|
|
2
|
+
* Input — delegates to the design system.
|
|
3
|
+
* Native `size` (character width) has no DS counterpart; it maps to a
|
|
4
|
+
* `ch`-based width so the original prop API keeps working.
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
import { forwardRef
|
|
9
|
-
import {
|
|
7
|
+
import { forwardRef } from 'react';
|
|
8
|
+
import { Input as DSInput } from '@umituz/web-design-system/atoms';
|
|
10
9
|
|
|
11
|
-
export type InputProps = InputHTMLAttributes<HTMLInputElement>;
|
|
10
|
+
export type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
|
12
11
|
|
|
13
12
|
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|
14
|
-
({
|
|
15
|
-
<
|
|
13
|
+
({ type = 'text', size: nativeSize, style, ...props }, ref) => (
|
|
14
|
+
<DSInput
|
|
15
|
+
ref={ref}
|
|
16
|
+
type={type}
|
|
17
|
+
{...props}
|
|
18
|
+
style={nativeSize != null ? { width: `${nativeSize}ch`, ...style } : style}
|
|
19
|
+
/>
|
|
16
20
|
),
|
|
17
21
|
);
|
|
18
22
|
|
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skeleton —
|
|
3
|
-
*
|
|
4
|
-
* The pulse animation is defined in `styles.css` (a CSS file can ship
|
|
5
|
-
* keyframes; inline styles cannot).
|
|
2
|
+
* Skeleton — re-exported from the design system (variant API identical).
|
|
6
3
|
*/
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
|
|
12
|
-
variant?: 'text' | 'circular' | 'rectangular';
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
|
|
16
|
-
({ className, variant = 'rectangular', ...props }, ref) => (
|
|
17
|
-
<div
|
|
18
|
-
ref={ref}
|
|
19
|
-
className={cn('udb-skeleton', `udb-skeleton--${variant}`, className)}
|
|
20
|
-
role="status"
|
|
21
|
-
{...props}
|
|
22
|
-
/>
|
|
23
|
-
),
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
Skeleton.displayName = 'Skeleton';
|
|
5
|
+
export { Skeleton } from '@umituz/web-design-system/atoms';
|
|
6
|
+
export type { SkeletonProps } from '@umituz/web-design-system/atoms';
|
package/src/primitives/cn.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* cn — conditional className join
|
|
2
|
+
* cn — conditional className join, delegated to the design system.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* `udb-*`
|
|
6
|
-
*
|
|
4
|
+
* The design system's `cn` (clsx + tailwind-merge) understands both the
|
|
5
|
+
* `udb-*` vocabulary (unknown classes pass through untouched) and real
|
|
6
|
+
* Tailwind utilities a host may pass, resolving conflicts by last-wins.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export type { ClassValue };
|
|
12
|
-
|
|
13
|
-
export function cn(...inputs: ClassValue[]): string {
|
|
14
|
-
return clsx(inputs);
|
|
15
|
-
}
|
|
9
|
+
export { cn } from '@umituz/web-design-system/utils/cn';
|
|
10
|
+
export type { ClassValue } from '@umituz/web-design-system/utils/cn';
|
package/src/styles/dashboard.css
CHANGED
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
* tokens (see `tokens.css`); override them for theming and dark mode.
|
|
11
11
|
*
|
|
12
12
|
* Sections:
|
|
13
|
-
* 1. Scope & reset 5.
|
|
14
|
-
* 2.
|
|
15
|
-
* 3.
|
|
16
|
-
* 4.
|
|
13
|
+
* 1. Scope & reset 5. Header/menus 9. Settings
|
|
14
|
+
* 2. Layout helpers 6. Auth 10. Analytics
|
|
15
|
+
* 3. App shell 7. Forms (auth) 11. Shared blocks
|
|
16
|
+
* 4. Sidebar 8. Onboarding
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
@import "@umituz/web-design-system/styles.css";
|
|
20
|
+
@import "./tokens.css";
|
|
21
|
+
|
|
19
22
|
/* ============================================================
|
|
20
23
|
* 1. Scope & reset
|
|
21
24
|
* ============================================================ */
|
|
@@ -50,191 +53,7 @@
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
/* ============================================================
|
|
53
|
-
* 2.
|
|
54
|
-
* ============================================================ */
|
|
55
|
-
|
|
56
|
-
.udb-btn {
|
|
57
|
-
display: inline-flex;
|
|
58
|
-
align-items: center;
|
|
59
|
-
justify-content: center;
|
|
60
|
-
gap: var(--u-space-2);
|
|
61
|
-
white-space: nowrap;
|
|
62
|
-
border: 1px solid transparent;
|
|
63
|
-
border-radius: var(--u-radius-md);
|
|
64
|
-
font-family: inherit;
|
|
65
|
-
font-size: var(--u-font-size-md);
|
|
66
|
-
font-weight: var(--u-font-weight-medium);
|
|
67
|
-
line-height: 1;
|
|
68
|
-
cursor: pointer;
|
|
69
|
-
text-decoration: none;
|
|
70
|
-
transition: background-color 0.15s ease, color 0.15s ease,
|
|
71
|
-
border-color 0.15s ease, transform 0.1s ease;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
.udb-btn:focus-visible {
|
|
75
|
-
outline: none;
|
|
76
|
-
box-shadow: 0 0 0 2px var(--u-color-surface), 0 0 0 4px var(--u-color-ring);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.udb-btn:disabled {
|
|
80
|
-
pointer-events: none;
|
|
81
|
-
opacity: 0.55;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.udb-btn:active:not(:disabled) {
|
|
85
|
-
transform: scale(0.97);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
.udb-btn svg {
|
|
89
|
-
pointer-events: none;
|
|
90
|
-
width: 1em;
|
|
91
|
-
height: 1em;
|
|
92
|
-
flex-shrink: 0;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/* Variants */
|
|
96
|
-
.udb-btn--default {
|
|
97
|
-
background: var(--u-color-primary);
|
|
98
|
-
color: var(--u-color-primary-contrast);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
.udb-btn--default:hover:not(:disabled) {
|
|
102
|
-
background: var(--u-color-primary-hover);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.udb-btn--destructive {
|
|
106
|
-
background: var(--u-color-danger);
|
|
107
|
-
color: var(--u-color-danger-contrast);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
.udb-btn--destructive:hover:not(:disabled) {
|
|
111
|
-
background: var(--u-color-danger-hover);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
.udb-btn--outline {
|
|
115
|
-
background: var(--u-color-surface);
|
|
116
|
-
color: var(--u-color-text);
|
|
117
|
-
border-color: var(--u-color-border-strong);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
.udb-btn--outline:hover:not(:disabled) {
|
|
121
|
-
background: var(--u-color-surface-muted);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
.udb-btn--ghost {
|
|
125
|
-
background: transparent;
|
|
126
|
-
color: var(--u-color-text);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.udb-btn--ghost:hover:not(:disabled) {
|
|
130
|
-
background: var(--u-color-surface-muted);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/* Sizes */
|
|
134
|
-
.udb-btn--default {
|
|
135
|
-
height: 40px;
|
|
136
|
-
padding: 0 var(--u-space-4);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
.udb-btn--sm {
|
|
140
|
-
height: 32px;
|
|
141
|
-
padding: 0 var(--u-space-3);
|
|
142
|
-
font-size: var(--u-font-size-sm);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
.udb-btn--icon {
|
|
146
|
-
height: 36px;
|
|
147
|
-
width: 36px;
|
|
148
|
-
padding: 0;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/* ============================================================
|
|
152
|
-
* 2. Primitives — Input / Checkbox / Skeleton
|
|
153
|
-
* ============================================================ */
|
|
154
|
-
|
|
155
|
-
.udb-input {
|
|
156
|
-
display: block;
|
|
157
|
-
width: 100%;
|
|
158
|
-
height: 40px;
|
|
159
|
-
padding: 0 var(--u-space-3);
|
|
160
|
-
background: var(--u-color-surface);
|
|
161
|
-
border: 1px solid var(--u-color-border-strong);
|
|
162
|
-
border-radius: var(--u-radius-md);
|
|
163
|
-
color: var(--u-color-text);
|
|
164
|
-
font-family: inherit;
|
|
165
|
-
font-size: var(--u-font-size-md);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
.udb-input::placeholder {
|
|
169
|
-
color: var(--u-color-muted);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
.udb-input:focus-visible {
|
|
173
|
-
outline: none;
|
|
174
|
-
border-color: var(--u-color-primary);
|
|
175
|
-
box-shadow: 0 0 0 3px var(--u-color-ring);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
.udb-input:disabled {
|
|
179
|
-
cursor: not-allowed;
|
|
180
|
-
opacity: 0.55;
|
|
181
|
-
background: var(--u-color-surface-muted);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
.udb-checkbox {
|
|
185
|
-
width: 18px;
|
|
186
|
-
height: 18px;
|
|
187
|
-
margin: 0;
|
|
188
|
-
accent-color: var(--u-color-primary);
|
|
189
|
-
cursor: pointer;
|
|
190
|
-
flex-shrink: 0;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
.udb-checkbox:disabled {
|
|
194
|
-
cursor: not-allowed;
|
|
195
|
-
opacity: 0.55;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
.udb-checkbox:focus-visible {
|
|
199
|
-
outline: none;
|
|
200
|
-
box-shadow: 0 0 0 3px var(--u-color-ring);
|
|
201
|
-
border-radius: var(--u-radius-sm);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
.udb-skeleton {
|
|
205
|
-
background: var(--u-color-surface-muted);
|
|
206
|
-
animation: udb-pulse 1.6s ease-in-out infinite;
|
|
207
|
-
border-radius: var(--u-radius-md);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
.udb-skeleton--text {
|
|
211
|
-
height: 16px;
|
|
212
|
-
width: 100%;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
.udb-skeleton--circular {
|
|
216
|
-
height: 48px;
|
|
217
|
-
width: 48px;
|
|
218
|
-
border-radius: 50%;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
.udb-skeleton--rectangular {
|
|
222
|
-
height: 96px;
|
|
223
|
-
width: 100%;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
@keyframes udb-pulse {
|
|
227
|
-
0%,
|
|
228
|
-
100% {
|
|
229
|
-
opacity: 1;
|
|
230
|
-
}
|
|
231
|
-
50% {
|
|
232
|
-
opacity: 0.55;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/* ============================================================
|
|
237
|
-
* 3. Layout helpers
|
|
56
|
+
* 2. Layout helpers
|
|
238
57
|
* ============================================================ */
|
|
239
58
|
|
|
240
59
|
.udb-stack {
|
|
@@ -254,14 +73,6 @@
|
|
|
254
73
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
|
255
74
|
}
|
|
256
75
|
|
|
257
|
-
.udb-card {
|
|
258
|
-
background: var(--u-color-surface);
|
|
259
|
-
border: 1px solid var(--u-color-border);
|
|
260
|
-
border-radius: var(--u-radius-lg);
|
|
261
|
-
padding: var(--u-space-6);
|
|
262
|
-
box-shadow: var(--u-shadow-sm);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
76
|
.udb-card--sm {
|
|
266
77
|
padding: var(--u-space-4);
|
|
267
78
|
}
|
|
@@ -271,7 +82,7 @@
|
|
|
271
82
|
}
|
|
272
83
|
|
|
273
84
|
/* ============================================================
|
|
274
|
-
*
|
|
85
|
+
* 3. App shell
|
|
275
86
|
* ============================================================ */
|
|
276
87
|
|
|
277
88
|
.udb-layout {
|
|
@@ -376,7 +187,7 @@
|
|
|
376
187
|
}
|
|
377
188
|
|
|
378
189
|
/* ============================================================
|
|
379
|
-
*
|
|
190
|
+
* 4. Sidebar
|
|
380
191
|
* ============================================================ */
|
|
381
192
|
|
|
382
193
|
.udb-sidebar {
|
|
@@ -589,7 +400,7 @@
|
|
|
589
400
|
}
|
|
590
401
|
|
|
591
402
|
/* ============================================================
|
|
592
|
-
*
|
|
403
|
+
* 5. Header / menus / notifications / profile
|
|
593
404
|
* ============================================================ */
|
|
594
405
|
|
|
595
406
|
.udb-header {
|
|
@@ -1015,7 +826,7 @@
|
|
|
1015
826
|
}
|
|
1016
827
|
|
|
1017
828
|
/* ============================================================
|
|
1018
|
-
*
|
|
829
|
+
* 6. Auth pages
|
|
1019
830
|
* ============================================================ */
|
|
1020
831
|
|
|
1021
832
|
.udb-auth {
|
|
@@ -1208,7 +1019,7 @@
|
|
|
1208
1019
|
}
|
|
1209
1020
|
|
|
1210
1021
|
/* ============================================================
|
|
1211
|
-
*
|
|
1022
|
+
* 7. Forms (auth + generic fields)
|
|
1212
1023
|
* ============================================================ */
|
|
1213
1024
|
|
|
1214
1025
|
/* Shared spinner (also used by onboarding/analytics icons). */
|
|
@@ -1551,7 +1362,7 @@
|
|
|
1551
1362
|
}
|
|
1552
1363
|
|
|
1553
1364
|
/* ============================================================
|
|
1554
|
-
*
|
|
1365
|
+
* 8. Onboarding wizard
|
|
1555
1366
|
* ============================================================ */
|
|
1556
1367
|
|
|
1557
1368
|
.udb-onboarding {
|
|
@@ -2202,7 +2013,7 @@
|
|
|
2202
2013
|
}
|
|
2203
2014
|
|
|
2204
2015
|
/* ============================================================
|
|
2205
|
-
*
|
|
2016
|
+
* 9. Settings
|
|
2206
2017
|
* ============================================================ */
|
|
2207
2018
|
|
|
2208
2019
|
.udb-settings {
|
|
@@ -2452,7 +2263,7 @@
|
|
|
2452
2263
|
}
|
|
2453
2264
|
|
|
2454
2265
|
/* ============================================================
|
|
2455
|
-
*
|
|
2266
|
+
* 10. Analytics
|
|
2456
2267
|
* ============================================================ */
|
|
2457
2268
|
|
|
2458
2269
|
.udb-analytics {
|
|
@@ -2572,131 +2383,6 @@
|
|
|
2572
2383
|
cursor: pointer;
|
|
2573
2384
|
}
|
|
2574
2385
|
|
|
2575
|
-
/* KPI tile. */
|
|
2576
|
-
.udb-metric {
|
|
2577
|
-
background: var(--u-color-surface);
|
|
2578
|
-
border: 1px solid var(--u-color-border);
|
|
2579
|
-
border-radius: var(--u-radius-lg);
|
|
2580
|
-
padding: var(--u-space-6);
|
|
2581
|
-
box-shadow: var(--u-shadow-sm);
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
|
-
.udb-metric--sm {
|
|
2585
|
-
padding: var(--u-space-4);
|
|
2586
|
-
}
|
|
2587
|
-
|
|
2588
|
-
.udb-metric--lg {
|
|
2589
|
-
padding: var(--u-space-8);
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
.udb-metric--interactive {
|
|
2593
|
-
cursor: pointer;
|
|
2594
|
-
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
2595
|
-
}
|
|
2596
|
-
|
|
2597
|
-
.udb-metric--interactive:hover {
|
|
2598
|
-
border-color: var(--u-color-primary);
|
|
2599
|
-
}
|
|
2600
|
-
|
|
2601
|
-
.udb-metric--interactive:focus-visible {
|
|
2602
|
-
outline: none;
|
|
2603
|
-
box-shadow: 0 0 0 3px var(--u-color-ring);
|
|
2604
|
-
}
|
|
2605
|
-
|
|
2606
|
-
.udb-metric__head {
|
|
2607
|
-
display: flex;
|
|
2608
|
-
align-items: flex-start;
|
|
2609
|
-
justify-content: space-between;
|
|
2610
|
-
gap: var(--u-space-3);
|
|
2611
|
-
}
|
|
2612
|
-
|
|
2613
|
-
.udb-metric__body {
|
|
2614
|
-
flex: 1;
|
|
2615
|
-
min-width: 0;
|
|
2616
|
-
}
|
|
2617
|
-
|
|
2618
|
-
.udb-metric__name {
|
|
2619
|
-
margin: 0 0 var(--u-space-1);
|
|
2620
|
-
font-size: var(--u-font-size-md);
|
|
2621
|
-
font-weight: var(--u-font-weight-medium);
|
|
2622
|
-
color: var(--u-color-muted);
|
|
2623
|
-
}
|
|
2624
|
-
|
|
2625
|
-
.udb-metric__value {
|
|
2626
|
-
font-size: var(--u-font-size-3xl);
|
|
2627
|
-
font-weight: var(--u-font-weight-bold);
|
|
2628
|
-
letter-spacing: -0.02em;
|
|
2629
|
-
line-height: 1.2;
|
|
2630
|
-
color: var(--u-color-text);
|
|
2631
|
-
}
|
|
2632
|
-
|
|
2633
|
-
.udb-metric--sm .udb-metric__name {
|
|
2634
|
-
font-size: var(--u-font-size-sm);
|
|
2635
|
-
}
|
|
2636
|
-
|
|
2637
|
-
.udb-metric--sm .udb-metric__value {
|
|
2638
|
-
font-size: var(--u-font-size-xl);
|
|
2639
|
-
}
|
|
2640
|
-
|
|
2641
|
-
.udb-metric--lg .udb-metric__name {
|
|
2642
|
-
font-size: var(--u-font-size-lg);
|
|
2643
|
-
}
|
|
2644
|
-
|
|
2645
|
-
.udb-metric--lg .udb-metric__value {
|
|
2646
|
-
font-size: var(--u-font-size-4xl);
|
|
2647
|
-
}
|
|
2648
|
-
|
|
2649
|
-
/* Trend row: one modifier tones the icon + value together;
|
|
2650
|
-
* the "vs last period" label stays muted. */
|
|
2651
|
-
.udb-metric__trend {
|
|
2652
|
-
display: flex;
|
|
2653
|
-
align-items: center;
|
|
2654
|
-
gap: var(--u-space-1);
|
|
2655
|
-
margin-top: var(--u-space-2);
|
|
2656
|
-
color: var(--u-color-muted);
|
|
2657
|
-
}
|
|
2658
|
-
|
|
2659
|
-
.udb-metric__trend--up {
|
|
2660
|
-
color: var(--u-color-success);
|
|
2661
|
-
}
|
|
2662
|
-
|
|
2663
|
-
.udb-metric__trend--down {
|
|
2664
|
-
color: var(--u-color-danger);
|
|
2665
|
-
}
|
|
2666
|
-
|
|
2667
|
-
.udb-metric__trend-icon {
|
|
2668
|
-
width: 16px;
|
|
2669
|
-
height: 16px;
|
|
2670
|
-
flex-shrink: 0;
|
|
2671
|
-
}
|
|
2672
|
-
|
|
2673
|
-
.udb-metric__trend-value {
|
|
2674
|
-
font-size: var(--u-font-size-md);
|
|
2675
|
-
font-weight: var(--u-font-weight-medium);
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
.udb-metric__trend-period {
|
|
2679
|
-
font-size: var(--u-font-size-md);
|
|
2680
|
-
color: var(--u-color-muted);
|
|
2681
|
-
}
|
|
2682
|
-
|
|
2683
|
-
.udb-metric__icon-box {
|
|
2684
|
-
display: flex;
|
|
2685
|
-
align-items: center;
|
|
2686
|
-
justify-content: center;
|
|
2687
|
-
width: 48px;
|
|
2688
|
-
height: 48px;
|
|
2689
|
-
flex-shrink: 0;
|
|
2690
|
-
border-radius: var(--u-radius-md);
|
|
2691
|
-
background: var(--u-color-primary-soft);
|
|
2692
|
-
}
|
|
2693
|
-
|
|
2694
|
-
.udb-metric__icon {
|
|
2695
|
-
width: 24px;
|
|
2696
|
-
height: 24px;
|
|
2697
|
-
color: var(--u-color-primary);
|
|
2698
|
-
}
|
|
2699
|
-
|
|
2700
2386
|
/* Chart card internals. */
|
|
2701
2387
|
.udb-analytics-card__status {
|
|
2702
2388
|
display: flex;
|
|
@@ -2810,7 +2496,7 @@
|
|
|
2810
2496
|
}
|
|
2811
2497
|
|
|
2812
2498
|
/* ============================================================
|
|
2813
|
-
*
|
|
2499
|
+
* 11. Shared blocks — empty state, tables, org switcher, matrix,
|
|
2814
2500
|
* audit list (used by guards, session and settings skeletons)
|
|
2815
2501
|
* ============================================================ */
|
|
2816
2502
|
|
|
@@ -63,4 +63,29 @@ describe("dashboard stylesheet token contract", () => {
|
|
|
63
63
|
it("provides dark-mode overrides", () => {
|
|
64
64
|
expect(tokensCss).toContain('[data-udb-theme="dark"]');
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
it("bridges core tokens to design-system variables with fallbacks", () => {
|
|
68
|
+
const BRIDGED = [
|
|
69
|
+
["--u-color-primary", "var(--color-primary, #2563eb)"],
|
|
70
|
+
["--u-color-bg", "var(--color-background, #f8fafc)"],
|
|
71
|
+
["--u-color-danger", "var(--color-destructive, #dc2626)"],
|
|
72
|
+
["--u-radius-md", "var(--radius-md, 8px)"],
|
|
73
|
+
["--u-shadow-lg", "var(--shadow-lg,"],
|
|
74
|
+
["--u-font-sans", "var(--font-sans,"],
|
|
75
|
+
] as const;
|
|
76
|
+
for (const [token, bridge] of BRIDGED) {
|
|
77
|
+
expect(tokensCss).toContain(`${token}: ${bridge}`);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("derives hover/soft shades from the bridged base color", () => {
|
|
82
|
+
expect(tokensCss).toMatch(/--u-color-primary-hover: var\(--color-primary-hover, color-mix\(/);
|
|
83
|
+
expect(tokensCss).toMatch(/--u-color-primary-soft: var\(--color-primary-soft, color-mix\(/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("pulls the design-system stylesheet transitively", () => {
|
|
87
|
+
const imports = [...dashboardCss.matchAll(/@import\s+"([^"]+)"/g)].map((m) => m[1]);
|
|
88
|
+
expect(imports[0]).toBe("@umituz/web-design-system/styles.css");
|
|
89
|
+
expect(imports).toContain("./tokens.css");
|
|
90
|
+
});
|
|
66
91
|
});
|
package/src/styles/tokens.css
CHANGED
|
@@ -1,53 +1,51 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @umituz/web-dashboard — design tokens (`--u-*`)
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* <html> or the layout root). The package never switches themes on
|
|
10
|
-
* its own — the host decides.
|
|
4
|
+
* Colors, radii, fonts and shadows are bridged to the design system's
|
|
5
|
+
* `--color-*` / `--radius-*` / `--font-*` / `--shadow-*` variables; each
|
|
6
|
+
* bridge keeps its previous value as fallback. Dark mode: add
|
|
7
|
+
* `data-udb-theme="dark"` on any ancestor — the dark block re-sets the
|
|
8
|
+
* design-system variables and every bridge follows automatically.
|
|
11
9
|
*/
|
|
12
10
|
|
|
13
11
|
:root {
|
|
14
|
-
/*
|
|
15
|
-
--u-color-bg: #f8fafc;
|
|
16
|
-
--u-color-surface: #ffffff;
|
|
17
|
-
--u-color-surface-muted: #f1f5f9;
|
|
18
|
-
--u-color-text: #0f172a;
|
|
19
|
-
--u-color-muted: #64748b;
|
|
20
|
-
--u-color-border: #e2e8f0;
|
|
12
|
+
/* Surfaces & text */
|
|
13
|
+
--u-color-bg: var(--color-background, #f8fafc);
|
|
14
|
+
--u-color-surface: var(--color-card, #ffffff);
|
|
15
|
+
--u-color-surface-muted: var(--color-muted, #f1f5f9);
|
|
16
|
+
--u-color-text: var(--color-foreground, #0f172a);
|
|
17
|
+
--u-color-muted: var(--color-muted-foreground, #64748b);
|
|
18
|
+
--u-color-border: var(--color-border, #e2e8f0);
|
|
21
19
|
--u-color-border-strong: #cbd5e1;
|
|
22
20
|
|
|
23
|
-
/*
|
|
24
|
-
--u-color-primary: #2563eb;
|
|
25
|
-
--u-color-primary-hover: #
|
|
26
|
-
--u-color-primary-contrast: #ffffff;
|
|
27
|
-
--u-color-primary-soft:
|
|
28
|
-
--u-color-secondary: #8b5cf6;
|
|
29
|
-
|
|
30
|
-
/*
|
|
31
|
-
--u-color-danger: #dc2626;
|
|
32
|
-
--u-color-danger-hover: #
|
|
33
|
-
--u-color-danger-contrast: #ffffff;
|
|
34
|
-
--u-color-danger-soft:
|
|
35
|
-
--u-color-success: #16a34a;
|
|
36
|
-
--u-color-success-soft:
|
|
37
|
-
--u-color-warning: #d97706;
|
|
38
|
-
--u-color-warning-soft:
|
|
21
|
+
/* Brand (hover/soft derive from the base color) */
|
|
22
|
+
--u-color-primary: var(--color-primary, #2563eb);
|
|
23
|
+
--u-color-primary-hover: var(--color-primary-hover, color-mix(in oklab, var(--color-primary, #2563eb) 86%, #000));
|
|
24
|
+
--u-color-primary-contrast: var(--color-primary-foreground, #ffffff);
|
|
25
|
+
--u-color-primary-soft: var(--color-primary-soft, color-mix(in srgb, var(--color-primary, #2563eb) 10%, transparent));
|
|
26
|
+
--u-color-secondary: var(--color-secondary, #8b5cf6);
|
|
27
|
+
|
|
28
|
+
/* Semantic */
|
|
29
|
+
--u-color-danger: var(--color-destructive, #dc2626);
|
|
30
|
+
--u-color-danger-hover: var(--color-destructive-hover, color-mix(in oklab, var(--color-destructive, #dc2626) 85%, #000));
|
|
31
|
+
--u-color-danger-contrast: var(--color-destructive-foreground, #ffffff);
|
|
32
|
+
--u-color-danger-soft: var(--color-destructive-soft, color-mix(in srgb, var(--color-destructive, #dc2626) 10%, transparent));
|
|
33
|
+
--u-color-success: var(--color-success, #16a34a);
|
|
34
|
+
--u-color-success-soft: var(--color-success-soft, color-mix(in srgb, var(--color-success, #16a34a) 12%, transparent));
|
|
35
|
+
--u-color-warning: var(--color-warning, #d97706);
|
|
36
|
+
--u-color-warning-soft: var(--color-warning-soft, color-mix(in srgb, var(--color-warning, #d97706) 15%, transparent));
|
|
39
37
|
|
|
40
38
|
/* Focus */
|
|
41
|
-
--u-color-ring: rgba(37, 99, 235, 0.45);
|
|
39
|
+
--u-color-ring: var(--color-ring, rgba(37, 99, 235, 0.45));
|
|
42
40
|
|
|
43
41
|
/* Radii */
|
|
44
|
-
--u-radius-sm: 6px;
|
|
45
|
-
--u-radius-md: 8px;
|
|
46
|
-
--u-radius-lg: 12px;
|
|
47
|
-
--u-radius-xl: 16px;
|
|
42
|
+
--u-radius-sm: var(--radius-sm, 6px);
|
|
43
|
+
--u-radius-md: var(--radius-md, 8px);
|
|
44
|
+
--u-radius-lg: var(--radius-lg, 12px);
|
|
45
|
+
--u-radius-xl: var(--radius-xl, 16px);
|
|
48
46
|
--u-radius-pill: 9999px;
|
|
49
47
|
|
|
50
|
-
/* Spacing
|
|
48
|
+
/* Spacing (4px base) */
|
|
51
49
|
--u-space-1: 4px;
|
|
52
50
|
--u-space-2: 8px;
|
|
53
51
|
--u-space-3: 12px;
|
|
@@ -58,8 +56,8 @@
|
|
|
58
56
|
--u-space-8: 40px;
|
|
59
57
|
|
|
60
58
|
/* Typography */
|
|
61
|
-
--u-font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
|
|
62
|
-
"Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
|
59
|
+
--u-font-sans: var(--font-sans, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto,
|
|
60
|
+
"Helvetica Neue", Arial, "Noto Sans", sans-serif);
|
|
63
61
|
--u-font-size-xs: 11px;
|
|
64
62
|
--u-font-size-sm: 12px;
|
|
65
63
|
--u-font-size-md: 14px;
|
|
@@ -72,17 +70,17 @@
|
|
|
72
70
|
--u-font-weight-medium: 500;
|
|
73
71
|
--u-font-weight-bold: 700;
|
|
74
72
|
|
|
75
|
-
/* Layout
|
|
73
|
+
/* Layout */
|
|
76
74
|
--u-sidebar-width: 240px;
|
|
77
75
|
--u-sidebar-width-collapsed: 64px;
|
|
78
76
|
--u-header-height: 56px;
|
|
79
77
|
|
|
80
78
|
/* Elevation */
|
|
81
|
-
--u-shadow-sm: 0 1px 2px rgba(15, 23, 42, 0.06);
|
|
82
|
-
--u-shadow-md: 0 4px 12px rgba(15, 23, 42, 0.08);
|
|
83
|
-
--u-shadow-lg: 0 12px 32px rgba(15, 23, 42, 0.14);
|
|
79
|
+
--u-shadow-sm: var(--shadow-sm, 0 1px 2px rgba(15, 23, 42, 0.06));
|
|
80
|
+
--u-shadow-md: var(--shadow-md, 0 4px 12px rgba(15, 23, 42, 0.08));
|
|
81
|
+
--u-shadow-lg: var(--shadow-lg, 0 12px 32px rgba(15, 23, 42, 0.14));
|
|
84
82
|
|
|
85
|
-
/* Sidebar palette
|
|
83
|
+
/* Sidebar palette */
|
|
86
84
|
--u-sidebar-bg: var(--u-color-surface);
|
|
87
85
|
--u-sidebar-text: var(--u-color-text);
|
|
88
86
|
--u-sidebar-muted: var(--u-color-muted);
|
|
@@ -92,33 +90,27 @@
|
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
[data-udb-theme="dark"] {
|
|
95
|
-
--
|
|
96
|
-
--
|
|
97
|
-
--
|
|
98
|
-
--
|
|
99
|
-
--
|
|
100
|
-
--
|
|
101
|
-
--u-color-border-strong: #334155;
|
|
93
|
+
--color-background: #0b1120;
|
|
94
|
+
--color-card: #111827;
|
|
95
|
+
--color-muted: #1e293b;
|
|
96
|
+
--color-foreground: #f1f5f9;
|
|
97
|
+
--color-muted-foreground: #94a3b8;
|
|
98
|
+
--color-border: #1f2a3c;
|
|
102
99
|
|
|
103
|
-
--
|
|
104
|
-
--
|
|
105
|
-
--
|
|
106
|
-
--u-color-primary-soft: rgba(59, 130, 246, 0.16);
|
|
100
|
+
--color-primary: #3b82f6;
|
|
101
|
+
--color-primary-foreground: #ffffff;
|
|
102
|
+
--color-secondary: #a78bfa;
|
|
107
103
|
|
|
108
|
-
--
|
|
104
|
+
--color-destructive: #ef4444;
|
|
105
|
+
--color-destructive-foreground: #ffffff;
|
|
106
|
+
--color-success: #22c55e;
|
|
107
|
+
--color-warning: #f59e0b;
|
|
109
108
|
|
|
110
|
-
--
|
|
111
|
-
--u-color-danger-hover: #f87171;
|
|
112
|
-
--u-color-danger-contrast: #ffffff;
|
|
113
|
-
--u-color-danger-soft: rgba(239, 68, 68, 0.16);
|
|
114
|
-
--u-color-success: #22c55e;
|
|
115
|
-
--u-color-success-soft: rgba(34, 197, 94, 0.16);
|
|
116
|
-
--u-color-warning: #f59e0b;
|
|
117
|
-
--u-color-warning-soft: rgba(245, 158, 11, 0.16);
|
|
109
|
+
--color-ring: rgba(96, 165, 250, 0.5);
|
|
118
110
|
|
|
119
|
-
--
|
|
111
|
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
112
|
+
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45);
|
|
113
|
+
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.55);
|
|
120
114
|
|
|
121
|
-
--u-
|
|
122
|
-
--u-shadow-md: 0 4px 12px rgba(0, 0, 0, 0.45);
|
|
123
|
-
--u-shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.55);
|
|
115
|
+
--u-color-border-strong: #334155;
|
|
124
116
|
}
|