@xenide-io/the-old-ui-theme 0.10.0 → 0.11.0
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/dist/{Chip-DKT-82Lf.d.mts → Chip-Dw2g2t8D.d.mts} +14 -2
- package/dist/{Chip-DKT-82Lf.d.ts → Chip-Dw2g2t8D.d.ts} +14 -2
- package/dist/{chunk-LHE4ROON.mjs → chunk-RIPGN6FG.mjs} +412 -61
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +355 -23
- package/dist/index.mjs +11 -3
- package/dist/suite-analytics--Mf1RZbw.d.mts +29 -0
- package/dist/suite-analytics--Mf1RZbw.d.ts +29 -0
- package/dist/suite.d.mts +14 -4
- package/dist/suite.d.ts +14 -4
- package/dist/suite.js +920 -513
- package/dist/suite.mjs +898 -523
- package/dist/ui.d.mts +2 -2
- package/dist/ui.d.ts +2 -2
- package/dist/ui.js +401 -66
- package/dist/ui.mjs +3 -1
- package/package.json +1 -1
- package/src/app/globals.css +57 -1
- package/src/components/ui/BrandSpinner.tsx +256 -0
- package/src/components/ui/CodeBlock.tsx +11 -1
- package/src/components/ui/Marketing.tsx +17 -1
- package/src/components/ui/SettingsLayout.tsx +4 -0
- package/src/components/ui/Table.tsx +32 -9
- package/src/components/ui/brand-spinner.test.tsx +62 -0
- package/src/components/ui/index.ts +7 -0
- package/src/components/ui/table.test.tsx +31 -4
- package/src/styles/suite-skin.css +78 -46
- package/src/suite/components/ai-draft-editor.tsx +184 -0
- package/src/suite/components/ai-panel-workflows.test.tsx +179 -0
- package/src/suite/components/ai-panel.tsx +184 -14
- package/src/suite/components/app-switcher.tsx +13 -0
- package/src/suite/components/command-palette-host.tsx +20 -2
- package/src/suite/components/suite-notification-bell.tsx +23 -2
- package/src/suite/components/suite-notification-settings.tsx +11 -0
- package/src/suite/components/suite-settings-layout.tsx +1 -1
- package/src/suite/components/suite-settings-mobile-nav.tsx +5 -1
- package/src/suite/components/suite-sidebar.tsx +2 -2
- package/src/suite/components/theme-provider.tsx +2 -0
- package/src/suite/components/today-ui.tsx +33 -28
- package/src/suite/index.ts +8 -0
- package/src/suite/lib/suite-notifications.tsx +5 -0
package/dist/ui.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
AvatarGroup,
|
|
11
11
|
Badge,
|
|
12
12
|
BarChart,
|
|
13
|
+
BrandSpinner,
|
|
13
14
|
Button,
|
|
14
15
|
ButtonChrome,
|
|
15
16
|
Calendar,
|
|
@@ -85,7 +86,7 @@ import {
|
|
|
85
86
|
Tooltip,
|
|
86
87
|
TooltipProvider,
|
|
87
88
|
useChartTokens
|
|
88
|
-
} from "./chunk-
|
|
89
|
+
} from "./chunk-RIPGN6FG.mjs";
|
|
89
90
|
import "./chunk-FWCSY2DS.mjs";
|
|
90
91
|
export {
|
|
91
92
|
Accordion,
|
|
@@ -98,6 +99,7 @@ export {
|
|
|
98
99
|
AvatarGroup,
|
|
99
100
|
Badge,
|
|
100
101
|
BarChart,
|
|
102
|
+
BrandSpinner,
|
|
101
103
|
Button,
|
|
102
104
|
ButtonChrome,
|
|
103
105
|
Calendar,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xenide-io/the-old-ui-theme",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Props-driven React components and theme tokens inspired by classic interface design — optimized for Next.js apps and internal tools.",
|
|
5
5
|
"author": "Xenide",
|
|
6
6
|
"license": "MIT",
|
package/src/app/globals.css
CHANGED
|
@@ -112,6 +112,20 @@ body {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
@layer components {
|
|
115
|
+
.suite-sidebar-link:focus-visible {
|
|
116
|
+
outline: none;
|
|
117
|
+
box-shadow: var(--ph-focus-halo);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.ph-brand-spinner__column {
|
|
121
|
+
animation: ph-spinner-columns 0.85s ease-in-out infinite;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@keyframes ph-spinner-columns {
|
|
125
|
+
0%, 100% { opacity: 0.35; transform: translateY(2px); }
|
|
126
|
+
50% { opacity: 1; transform: translateY(-2px); }
|
|
127
|
+
}
|
|
128
|
+
|
|
115
129
|
.ph-page-title {
|
|
116
130
|
font-size: var(--ph-type-page);
|
|
117
131
|
line-height: var(--ph-leading-title);
|
|
@@ -983,6 +997,47 @@ body {
|
|
|
983
997
|
}
|
|
984
998
|
|
|
985
999
|
/* LemonTable.scss — sticky-header border Simulation + row rhythm */
|
|
1000
|
+
.ph-table-report-wrap {
|
|
1001
|
+
overflow-x: auto;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
.ph-table-report-wrap:focus-visible {
|
|
1005
|
+
outline: 3px solid color-mix(in oklab, var(--ph-focus-ring) 30%, transparent);
|
|
1006
|
+
outline-offset: 2px;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/* Preserve the existing report table geometry and sentence-case headings. */
|
|
1010
|
+
.ph-table-report {
|
|
1011
|
+
@apply w-full text-left text-sm;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.ph-table-report thead {
|
|
1015
|
+
color: var(--ph-text-secondary);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
.ph-table-report tr {
|
|
1019
|
+
border-bottom: 1px solid var(--ph-border);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
.ph-table-report th:first-child,
|
|
1023
|
+
.ph-table-report td:first-child {
|
|
1024
|
+
padding-block: 0.75rem;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.ph-table-report th:not(:last-child),
|
|
1028
|
+
.ph-table-report td:not(:last-child) {
|
|
1029
|
+
padding-right: 1rem;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
.ph-table-compact .ph-table-report {
|
|
1033
|
+
font-size: var(--ph-type-meta);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
.ph-table-compact .ph-table-report th:first-child,
|
|
1037
|
+
.ph-table-compact .ph-table-report td:first-child {
|
|
1038
|
+
padding-block: 0.375rem;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
986
1041
|
.ph-table-wrap {
|
|
987
1042
|
@apply overflow-x-auto bg-ph-surface;
|
|
988
1043
|
border-radius: var(--ph-radius-surface);
|
|
@@ -1063,7 +1118,8 @@ body {
|
|
|
1063
1118
|
font-size: var(--ph-type-meta);
|
|
1064
1119
|
}
|
|
1065
1120
|
|
|
1066
|
-
.ph-table td.ph-table-empty
|
|
1121
|
+
.ph-table td.ph-table-empty,
|
|
1122
|
+
.ph-table-report td.ph-table-empty {
|
|
1067
1123
|
@apply py-8 text-center text-sm text-ph-mutedtext;
|
|
1068
1124
|
}
|
|
1069
1125
|
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId, useSyncExternalStore, type HTMLAttributes } from "react";
|
|
4
|
+
import { cn } from "@/lib/cn";
|
|
5
|
+
|
|
6
|
+
export type BrandSpinnerVariant =
|
|
7
|
+
"ring" | "timer" | "columns" | "orbit" | "ink";
|
|
8
|
+
export type BrandSpinnerSize = "sm" | "md" | "lg";
|
|
9
|
+
|
|
10
|
+
export interface BrandSpinnerProps extends HTMLAttributes<HTMLDivElement> {
|
|
11
|
+
variant?: BrandSpinnerVariant;
|
|
12
|
+
size?: BrandSpinnerSize;
|
|
13
|
+
label?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const sizes: Record<BrandSpinnerSize, number> = { sm: 20, md: 40, lg: 56 };
|
|
17
|
+
const reducedMotionQuery = "(prefers-reduced-motion: reduce)";
|
|
18
|
+
|
|
19
|
+
function subscribeMotion(onChange: () => void): () => void {
|
|
20
|
+
const media = window.matchMedia?.(reducedMotionQuery);
|
|
21
|
+
media?.addEventListener("change", onChange);
|
|
22
|
+
return () => media?.removeEventListener("change", onChange);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readReducedMotion(): boolean {
|
|
26
|
+
return window.matchMedia?.(reducedMotionQuery).matches ?? false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function BrandSpinner({
|
|
30
|
+
variant = "ring",
|
|
31
|
+
size = "md",
|
|
32
|
+
label = "Loading",
|
|
33
|
+
className,
|
|
34
|
+
...props
|
|
35
|
+
}: BrandSpinnerProps) {
|
|
36
|
+
const gradientId = `ph-spinner-${useId().replace(/:/g, "")}`;
|
|
37
|
+
const reducedMotion = useSyncExternalStore(
|
|
38
|
+
subscribeMotion,
|
|
39
|
+
readReducedMotion,
|
|
40
|
+
() => true,
|
|
41
|
+
);
|
|
42
|
+
const dimension = sizes[size];
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
role="status"
|
|
47
|
+
aria-label={label}
|
|
48
|
+
data-test="app-spinner"
|
|
49
|
+
className={cn(
|
|
50
|
+
"inline-flex shrink-0",
|
|
51
|
+
variant !== "orbit" && "text-ph-brand",
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
<svg
|
|
57
|
+
width={dimension}
|
|
58
|
+
height={dimension}
|
|
59
|
+
viewBox={variant === "ring" ? "0 0 24 24" : "0 0 40 40"}
|
|
60
|
+
aria-hidden
|
|
61
|
+
className={
|
|
62
|
+
variant === "timer" || variant === "ring"
|
|
63
|
+
? "animate-spin motion-reduce:animate-none"
|
|
64
|
+
: undefined
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
{variant === "ring" ? (
|
|
68
|
+
<>
|
|
69
|
+
<circle
|
|
70
|
+
cx="12"
|
|
71
|
+
cy="12"
|
|
72
|
+
r="10"
|
|
73
|
+
fill="none"
|
|
74
|
+
stroke="currentColor"
|
|
75
|
+
strokeWidth="3"
|
|
76
|
+
opacity="0.2"
|
|
77
|
+
/>
|
|
78
|
+
<path
|
|
79
|
+
d="M12 2a10 10 0 0 1 10 10"
|
|
80
|
+
fill="none"
|
|
81
|
+
stroke="currentColor"
|
|
82
|
+
strokeWidth="3"
|
|
83
|
+
strokeLinecap="round"
|
|
84
|
+
/>
|
|
85
|
+
</>
|
|
86
|
+
) : variant === "timer" ? (
|
|
87
|
+
<>
|
|
88
|
+
<circle
|
|
89
|
+
cx="20"
|
|
90
|
+
cy="20"
|
|
91
|
+
r="15"
|
|
92
|
+
fill="none"
|
|
93
|
+
stroke="currentColor"
|
|
94
|
+
strokeOpacity="0.18"
|
|
95
|
+
strokeWidth="2.5"
|
|
96
|
+
/>
|
|
97
|
+
<circle
|
|
98
|
+
cx="20"
|
|
99
|
+
cy="20"
|
|
100
|
+
r="15"
|
|
101
|
+
fill="none"
|
|
102
|
+
stroke="currentColor"
|
|
103
|
+
strokeWidth="2.5"
|
|
104
|
+
strokeLinecap="round"
|
|
105
|
+
strokeDasharray="24 70"
|
|
106
|
+
/>
|
|
107
|
+
<circle cx="20" cy="5" r="2.25" fill="currentColor" />
|
|
108
|
+
</>
|
|
109
|
+
) : variant === "columns" ? (
|
|
110
|
+
<>
|
|
111
|
+
<rect
|
|
112
|
+
x="9"
|
|
113
|
+
y="16"
|
|
114
|
+
width="6"
|
|
115
|
+
height="14"
|
|
116
|
+
rx="2"
|
|
117
|
+
fill="currentColor"
|
|
118
|
+
fillOpacity="0.35"
|
|
119
|
+
className="ph-brand-spinner__column"
|
|
120
|
+
/>
|
|
121
|
+
<rect
|
|
122
|
+
x="17"
|
|
123
|
+
y="11"
|
|
124
|
+
width="6"
|
|
125
|
+
height="19"
|
|
126
|
+
rx="2"
|
|
127
|
+
fill="currentColor"
|
|
128
|
+
className="ph-brand-spinner__column"
|
|
129
|
+
style={{ animationDelay: "0.14s" }}
|
|
130
|
+
/>
|
|
131
|
+
<rect
|
|
132
|
+
x="25"
|
|
133
|
+
y="14"
|
|
134
|
+
width="6"
|
|
135
|
+
height="16"
|
|
136
|
+
rx="2"
|
|
137
|
+
fill="currentColor"
|
|
138
|
+
fillOpacity="0.55"
|
|
139
|
+
className="ph-brand-spinner__column"
|
|
140
|
+
style={{ animationDelay: "0.28s" }}
|
|
141
|
+
/>
|
|
142
|
+
</>
|
|
143
|
+
) : variant === "orbit" ? (
|
|
144
|
+
<>
|
|
145
|
+
<defs>
|
|
146
|
+
<linearGradient
|
|
147
|
+
id={gradientId}
|
|
148
|
+
x1="0%"
|
|
149
|
+
y1="0%"
|
|
150
|
+
x2="100%"
|
|
151
|
+
y2="100%"
|
|
152
|
+
>
|
|
153
|
+
<stop offset="0%" stopColor="#1877f2" />
|
|
154
|
+
<stop offset="100%" stopColor="#ebbcba" />
|
|
155
|
+
</linearGradient>
|
|
156
|
+
</defs>
|
|
157
|
+
<circle
|
|
158
|
+
cx="20"
|
|
159
|
+
cy="20"
|
|
160
|
+
r="15"
|
|
161
|
+
fill="none"
|
|
162
|
+
stroke={`url(#${gradientId})`}
|
|
163
|
+
strokeOpacity="0.25"
|
|
164
|
+
strokeWidth="2"
|
|
165
|
+
/>
|
|
166
|
+
<g className="origin-[20px_20px] animate-spin motion-reduce:animate-none">
|
|
167
|
+
<circle
|
|
168
|
+
cx="20"
|
|
169
|
+
cy="20"
|
|
170
|
+
r="15"
|
|
171
|
+
fill="none"
|
|
172
|
+
stroke={`url(#${gradientId})`}
|
|
173
|
+
strokeWidth="2.5"
|
|
174
|
+
strokeLinecap="round"
|
|
175
|
+
strokeDasharray="22 72"
|
|
176
|
+
/>
|
|
177
|
+
</g>
|
|
178
|
+
<g className="origin-[20px_20px] animate-[spin_1.6s_linear_infinite_reverse] motion-reduce:animate-none">
|
|
179
|
+
<circle
|
|
180
|
+
cx="20"
|
|
181
|
+
cy="20"
|
|
182
|
+
r="9"
|
|
183
|
+
fill="none"
|
|
184
|
+
stroke="#ebbcba"
|
|
185
|
+
strokeWidth="2"
|
|
186
|
+
strokeLinecap="round"
|
|
187
|
+
strokeDasharray="12 45"
|
|
188
|
+
strokeOpacity="0.9"
|
|
189
|
+
/>
|
|
190
|
+
</g>
|
|
191
|
+
<circle
|
|
192
|
+
cx="20"
|
|
193
|
+
cy="20"
|
|
194
|
+
r="2.5"
|
|
195
|
+
fill="#1877f2"
|
|
196
|
+
className="animate-pulse motion-reduce:animate-none"
|
|
197
|
+
/>
|
|
198
|
+
</>
|
|
199
|
+
) : (
|
|
200
|
+
<>
|
|
201
|
+
<g>
|
|
202
|
+
{!reducedMotion ? (
|
|
203
|
+
<animateTransform
|
|
204
|
+
attributeName="transform"
|
|
205
|
+
type="rotate"
|
|
206
|
+
from="0 20 20"
|
|
207
|
+
to="360 20 20"
|
|
208
|
+
dur="2.4s"
|
|
209
|
+
repeatCount="indefinite"
|
|
210
|
+
/>
|
|
211
|
+
) : null}
|
|
212
|
+
<circle
|
|
213
|
+
cx="20"
|
|
214
|
+
cy="20"
|
|
215
|
+
r="16"
|
|
216
|
+
fill="none"
|
|
217
|
+
stroke="currentColor"
|
|
218
|
+
strokeOpacity="0.2"
|
|
219
|
+
strokeWidth="1.5"
|
|
220
|
+
strokeDasharray="3 5"
|
|
221
|
+
/>
|
|
222
|
+
</g>
|
|
223
|
+
<g>
|
|
224
|
+
{!reducedMotion ? (
|
|
225
|
+
<animateTransform
|
|
226
|
+
attributeName="transform"
|
|
227
|
+
type="rotate"
|
|
228
|
+
from="360 20 20"
|
|
229
|
+
to="0 20 20"
|
|
230
|
+
dur="1.1s"
|
|
231
|
+
repeatCount="indefinite"
|
|
232
|
+
/>
|
|
233
|
+
) : null}
|
|
234
|
+
<circle
|
|
235
|
+
cx="20"
|
|
236
|
+
cy="20"
|
|
237
|
+
r="11"
|
|
238
|
+
fill="none"
|
|
239
|
+
stroke="currentColor"
|
|
240
|
+
strokeWidth="2"
|
|
241
|
+
strokeLinecap="round"
|
|
242
|
+
strokeDasharray="18 52"
|
|
243
|
+
/>
|
|
244
|
+
</g>
|
|
245
|
+
<path
|
|
246
|
+
d="M16 14h8v12a1 1 0 0 1-1.4.9L20 25.5l-2.6 1.4A1 1 0 0 1 16 26V14z"
|
|
247
|
+
fill="currentColor"
|
|
248
|
+
fillOpacity="0.85"
|
|
249
|
+
className="animate-pulse motion-reduce:animate-none"
|
|
250
|
+
/>
|
|
251
|
+
</>
|
|
252
|
+
)}
|
|
253
|
+
</svg>
|
|
254
|
+
</div>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { cn } from "@/lib/cn";
|
|
5
|
+
import { trackSuiteEvent } from "@/lib/suite-analytics";
|
|
5
6
|
|
|
6
7
|
interface CodeBlockProps {
|
|
7
8
|
code: string;
|
|
@@ -14,7 +15,16 @@ export function CodeBlock({ code, language = "tsx", filename, className }: CodeB
|
|
|
14
15
|
const [copied, setCopied] = useState(false);
|
|
15
16
|
|
|
16
17
|
const handleCopy = async () => {
|
|
17
|
-
|
|
18
|
+
try {
|
|
19
|
+
await navigator.clipboard.writeText(code);
|
|
20
|
+
} catch {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
trackSuiteEvent("Link Copied", {
|
|
24
|
+
kind: "code",
|
|
25
|
+
copy_kind: "code_block",
|
|
26
|
+
label: filename ?? language,
|
|
27
|
+
});
|
|
18
28
|
setCopied(true);
|
|
19
29
|
setTimeout(() => setCopied(false), 2000);
|
|
20
30
|
};
|
|
@@ -24,8 +24,24 @@ export function MarketingHeader({
|
|
|
24
24
|
navigation: ReactNode;
|
|
25
25
|
actions: ReactNode;
|
|
26
26
|
}) {
|
|
27
|
+
// At the very top nothing sits under the header, so it must stay transparent
|
|
28
|
+
// (the page background shows through from the first pixel). Only once content
|
|
29
|
+
// scrolls beneath it do we frost it.
|
|
30
|
+
const [scrolled, setScrolled] = useState(false);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const onScroll = () => setScrolled(window.scrollY > 4);
|
|
34
|
+
onScroll();
|
|
35
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
36
|
+
return () => window.removeEventListener("scroll", onScroll);
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
27
39
|
return (
|
|
28
|
-
<header
|
|
40
|
+
<header
|
|
41
|
+
{...props}
|
|
42
|
+
data-scrolled={scrolled ? "true" : "false"}
|
|
43
|
+
className={cn("marketing-header", className)}
|
|
44
|
+
>
|
|
29
45
|
<div className="marketing-header__inner">
|
|
30
46
|
<div className="marketing-header__brand">{brand}</div>
|
|
31
47
|
<nav
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import type { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
4
|
import { IconTuning } from "@/components/icons";
|
|
3
5
|
import { cn } from "@/lib/cn";
|
|
6
|
+
import { trackSuiteEvent } from "@/lib/suite-analytics";
|
|
4
7
|
|
|
5
8
|
export interface SettingsNavItem {
|
|
6
9
|
id: string;
|
|
@@ -50,6 +53,7 @@ export function SettingsNav({ groups, className, ...props }: SettingsNavProps) {
|
|
|
50
53
|
item.active && "ph-settings-nav__item--active",
|
|
51
54
|
)}
|
|
52
55
|
aria-current={item.active ? "page" : undefined}
|
|
56
|
+
onClick={() => trackSuiteEvent("Tab Viewed", { tab: item.id })}
|
|
53
57
|
>
|
|
54
58
|
{item.icon ? (
|
|
55
59
|
<span className="ph-settings-nav__icon">{item.icon}</span>
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import type { CSSProperties, Key, ReactNode } from "react";
|
|
4
4
|
import { cn } from "@/lib/cn";
|
|
5
5
|
|
|
6
|
+
export type TableVariant = "default" | "report";
|
|
7
|
+
|
|
6
8
|
export interface TableColumn<T> {
|
|
7
9
|
key: string;
|
|
8
10
|
header: ReactNode;
|
|
@@ -19,6 +21,8 @@ export interface TableProps<T> {
|
|
|
19
21
|
getRowStyle?: (row: T) => CSSProperties | undefined;
|
|
20
22
|
getRowKey?: (row: T, index: number) => Key;
|
|
21
23
|
className?: string;
|
|
24
|
+
tableClassName?: string;
|
|
25
|
+
variant?: TableVariant;
|
|
22
26
|
caption?: string;
|
|
23
27
|
emptyState?: ReactNode;
|
|
24
28
|
}
|
|
@@ -32,37 +36,56 @@ export function Table<T>({
|
|
|
32
36
|
getRowStyle,
|
|
33
37
|
getRowKey,
|
|
34
38
|
className,
|
|
39
|
+
tableClassName,
|
|
40
|
+
variant = "default",
|
|
35
41
|
caption,
|
|
36
42
|
emptyState = "No data available.",
|
|
37
43
|
}: TableProps<T>) {
|
|
38
44
|
return (
|
|
39
45
|
<div
|
|
40
|
-
className={cn(
|
|
46
|
+
className={cn(
|
|
47
|
+
variant === "report" ? "ph-table-report-wrap" : "ph-table-wrap",
|
|
48
|
+
compact && "ph-table-compact",
|
|
49
|
+
className,
|
|
50
|
+
)}
|
|
41
51
|
role="region"
|
|
42
52
|
aria-label={caption ?? "Data table"}
|
|
43
53
|
tabIndex={0}
|
|
44
54
|
>
|
|
45
|
-
<table
|
|
55
|
+
<table
|
|
56
|
+
className={cn(
|
|
57
|
+
variant === "report" ? "ph-table-report" : "ph-table",
|
|
58
|
+
zebra && "ph-table-zebra",
|
|
59
|
+
ribbon && "ph-table--ribbon",
|
|
60
|
+
tableClassName,
|
|
61
|
+
)}
|
|
62
|
+
>
|
|
46
63
|
{caption && <caption className="sr-only">{caption}</caption>}
|
|
47
64
|
<thead>
|
|
48
65
|
<tr>
|
|
49
66
|
{columns.map((col) => (
|
|
50
|
-
<th key={col.key} className={col.className}>
|
|
67
|
+
<th key={col.key} scope="col" className={col.className}>
|
|
68
|
+
{col.header}
|
|
69
|
+
</th>
|
|
51
70
|
))}
|
|
52
71
|
</tr>
|
|
53
72
|
</thead>
|
|
54
73
|
<tbody>
|
|
55
74
|
{data.length === 0 ? (
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
75
|
+
emptyState === null ? null : (
|
|
76
|
+
<tr>
|
|
77
|
+
<td colSpan={columns.length} className="ph-table-empty">
|
|
78
|
+
{emptyState}
|
|
79
|
+
</td>
|
|
80
|
+
</tr>
|
|
81
|
+
)
|
|
61
82
|
) : (
|
|
62
83
|
data.map((row, i) => (
|
|
63
84
|
<tr key={getRowKey?.(row, i) ?? i} style={getRowStyle?.(row)}>
|
|
64
85
|
{columns.map((col) => (
|
|
65
|
-
<td key={col.key} className={col.className}>
|
|
86
|
+
<td key={col.key} className={col.className}>
|
|
87
|
+
{col.cell(row)}
|
|
88
|
+
</td>
|
|
66
89
|
))}
|
|
67
90
|
</tr>
|
|
68
91
|
))
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { act, cleanup, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { BrandSpinner } from "./BrandSpinner";
|
|
4
|
+
|
|
5
|
+
afterEach(cleanup);
|
|
6
|
+
|
|
7
|
+
describe("BrandSpinner", () => {
|
|
8
|
+
it("keeps orbit gradient references unique when multiple loaders render", () => {
|
|
9
|
+
const { container } = render(
|
|
10
|
+
<>
|
|
11
|
+
<BrandSpinner variant="orbit" size="sm" label="Loading team" />
|
|
12
|
+
<BrandSpinner variant="orbit" size="lg" label="Loading projects" />
|
|
13
|
+
</>,
|
|
14
|
+
);
|
|
15
|
+
const gradients = container.querySelectorAll("linearGradient");
|
|
16
|
+
expect(gradients[0].id).not.toBe(gradients[1].id);
|
|
17
|
+
expect(
|
|
18
|
+
screen.getByRole("status", { name: "Loading team" }).querySelector("svg"),
|
|
19
|
+
).toHaveAttribute("width", "20");
|
|
20
|
+
expect(
|
|
21
|
+
screen
|
|
22
|
+
.getByRole("status", { name: "Loading projects" })
|
|
23
|
+
.querySelector("svg"),
|
|
24
|
+
).toHaveAttribute("width", "56");
|
|
25
|
+
expect(
|
|
26
|
+
container.querySelector(`[stroke="url(#${gradients[1].id})"]`),
|
|
27
|
+
).not.toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("stops SVG animation when reduced-motion preference changes", () => {
|
|
31
|
+
const original = window.matchMedia;
|
|
32
|
+
let notify: (() => void) | undefined;
|
|
33
|
+
const media = {
|
|
34
|
+
matches: false,
|
|
35
|
+
addEventListener: vi.fn((_event: string, listener: () => void) => {
|
|
36
|
+
notify = listener;
|
|
37
|
+
}),
|
|
38
|
+
removeEventListener: vi.fn(),
|
|
39
|
+
};
|
|
40
|
+
Object.defineProperty(window, "matchMedia", {
|
|
41
|
+
configurable: true,
|
|
42
|
+
value: vi.fn(() => media),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const { container, unmount } = render(<BrandSpinner variant="ink" />);
|
|
47
|
+
expect(container.querySelectorAll("animateTransform")).toHaveLength(2);
|
|
48
|
+
act(() => {
|
|
49
|
+
media.matches = true;
|
|
50
|
+
notify?.();
|
|
51
|
+
});
|
|
52
|
+
expect(container.querySelectorAll("animateTransform")).toHaveLength(0);
|
|
53
|
+
unmount();
|
|
54
|
+
expect(media.removeEventListener).toHaveBeenCalled();
|
|
55
|
+
} finally {
|
|
56
|
+
Object.defineProperty(window, "matchMedia", {
|
|
57
|
+
configurable: true,
|
|
58
|
+
value: original,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -134,6 +134,7 @@ export {
|
|
|
134
134
|
Table as Table,
|
|
135
135
|
type TableColumn as TableColumn,
|
|
136
136
|
type TableProps as TableProps,
|
|
137
|
+
type TableVariant as TableVariant,
|
|
137
138
|
} from "@/components/ui/Table";
|
|
138
139
|
export {
|
|
139
140
|
Stat as Stat,
|
|
@@ -260,6 +261,12 @@ export {
|
|
|
260
261
|
Spinner as Spinner,
|
|
261
262
|
type SpinnerProps as SpinnerProps,
|
|
262
263
|
} from "@/components/ui/Spinner";
|
|
264
|
+
export {
|
|
265
|
+
BrandSpinner,
|
|
266
|
+
type BrandSpinnerProps,
|
|
267
|
+
type BrandSpinnerSize,
|
|
268
|
+
type BrandSpinnerVariant,
|
|
269
|
+
} from "@/components/ui/BrandSpinner";
|
|
263
270
|
export { Dot as Dot, type DotProps as DotProps } from "@/components/ui/Dot";
|
|
264
271
|
export {
|
|
265
272
|
Chip as Chip,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
2
|
-
import { describe, expect, it } from "vitest";
|
|
1
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
3
|
|
|
4
4
|
import { Table } from "./Table";
|
|
5
5
|
|
|
6
|
+
afterEach(cleanup);
|
|
7
|
+
|
|
6
8
|
describe("Table", () => {
|
|
7
9
|
it("names its scroll region and renders a custom empty state", () => {
|
|
8
10
|
render(
|
|
@@ -14,7 +16,32 @@ describe("Table", () => {
|
|
|
14
16
|
/>,
|
|
15
17
|
);
|
|
16
18
|
|
|
17
|
-
expect(screen.getByRole("region", { name: "People" })).toHaveAttribute(
|
|
18
|
-
|
|
19
|
+
expect(screen.getByRole("region", { name: "People" })).toHaveAttribute(
|
|
20
|
+
"tabindex",
|
|
21
|
+
"0",
|
|
22
|
+
);
|
|
23
|
+
expect(
|
|
24
|
+
screen.getByRole("cell", { name: "No people yet." }),
|
|
25
|
+
).toHaveAttribute("colspan", "1");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("lets report pages own their empty state without an extra table row", () => {
|
|
29
|
+
render(
|
|
30
|
+
<Table
|
|
31
|
+
variant="report"
|
|
32
|
+
caption="Run report"
|
|
33
|
+
data={[] as Array<{ name: string }>}
|
|
34
|
+
columns={[{ key: "name", header: "Crew", cell: (row) => row.name }]}
|
|
35
|
+
emptyState={null}
|
|
36
|
+
/>,
|
|
37
|
+
);
|
|
38
|
+
expect(
|
|
39
|
+
screen.getByRole("table", { name: "Run report" }),
|
|
40
|
+
).toBeInTheDocument();
|
|
41
|
+
expect(screen.getByRole("columnheader", { name: "Crew" })).toHaveAttribute(
|
|
42
|
+
"scope",
|
|
43
|
+
"col",
|
|
44
|
+
);
|
|
45
|
+
expect(screen.queryByRole("cell")).not.toBeInTheDocument();
|
|
19
46
|
});
|
|
20
47
|
});
|