@xenide-io/the-old-ui-theme 0.9.18 → 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-BzuNgJIa.d.mts → Chip-Dw2g2t8D.d.mts} +18 -4
- package/dist/{Chip-BzuNgJIa.d.ts → Chip-Dw2g2t8D.d.ts} +18 -4
- package/dist/{chunk-6SVJAU5K.mjs → chunk-RIPGN6FG.mjs} +1333 -526
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +490 -119
- 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 +36 -4
- package/dist/suite.d.ts +36 -4
- package/dist/suite.js +4617 -4001
- package/dist/suite.mjs +1456 -868
- package/dist/tailwind-preset.js +10 -1
- package/dist/tailwind-preset.mjs +10 -1
- package/dist/ui.d.mts +33 -4
- package/dist/ui.d.ts +33 -4
- package/dist/ui.js +1364 -571
- package/dist/ui.mjs +15 -1
- package/docs/macos-identity-audit.md +279 -0
- package/package.json +1 -1
- package/src/app/globals.css +390 -44
- package/src/components/ui/Accordion.tsx +4 -7
- package/src/components/ui/Avatar.tsx +5 -5
- package/src/components/ui/BrandSpinner.tsx +256 -0
- package/src/components/ui/Calendar.tsx +7 -7
- package/src/components/ui/CodeBlock.tsx +14 -11
- package/src/components/ui/CollapsibleSection.tsx +4 -4
- package/src/components/ui/CommandPalette.tsx +7 -10
- package/src/components/ui/EmptyState.tsx +2 -2
- package/src/components/ui/Input.tsx +1 -1
- package/src/components/ui/Marketing.tsx +144 -0
- package/src/components/ui/MarketingIllustrations.tsx +287 -0
- package/src/components/ui/Panel.tsx +1 -1
- package/src/components/ui/SegmentedControl.tsx +59 -13
- package/src/components/ui/SettingsLayout.tsx +4 -0
- package/src/components/ui/ShowcaseWrapper.tsx +3 -3
- package/src/components/ui/Table.tsx +45 -10
- package/src/components/ui/Typography.tsx +4 -4
- package/src/components/ui/avatar-stat.test.tsx +14 -1
- package/src/components/ui/brand-spinner.test.tsx +62 -0
- package/src/components/ui/index.ts +19 -11
- package/src/components/ui/interaction-primitives.test.tsx +12 -6
- package/src/components/ui/marketing.test.tsx +39 -0
- package/src/components/ui/table.test.tsx +47 -0
- package/src/styles/suite-skin.css +471 -74
- package/src/styles/themes.css +45 -45
- package/src/styles/typography.css +25 -1
- 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 +201 -31
- package/src/suite/components/app-switcher.tsx +22 -9
- package/src/suite/components/command-palette-host.tsx +20 -2
- package/src/suite/components/suite-app-layout.tsx +7 -4
- package/src/suite/components/suite-bottom-nav.tsx +1 -1
- package/src/suite/components/suite-clients-projects-directory.tsx +6 -6
- package/src/suite/components/suite-integration-picker.tsx +7 -7
- package/src/suite/components/suite-integration-rules.tsx +5 -5
- package/src/suite/components/suite-layout.test.tsx +32 -3
- package/src/suite/components/suite-layout.tsx +4 -4
- package/src/suite/components/suite-mobile-drawer.tsx +2 -2
- package/src/suite/components/suite-mobile-header.tsx +1 -1
- package/src/suite/components/suite-notification-bell.tsx +27 -6
- package/src/suite/components/suite-notification-settings.tsx +182 -0
- package/src/suite/components/suite-palette-library.test.tsx +46 -0
- package/src/suite/components/suite-palette-library.tsx +82 -15
- package/src/suite/components/suite-settings-layout.tsx +2 -2
- package/src/suite/components/suite-settings-mobile-nav.tsx +10 -10
- package/src/suite/components/suite-sidebar.test.tsx +39 -0
- package/src/suite/components/suite-sidebar.tsx +41 -19
- package/src/suite/components/suite-skeleton.tsx +6 -6
- package/src/suite/components/suite-user-menu.tsx +2 -2
- package/src/suite/components/theme-provider.tsx +2 -0
- package/src/suite/components/today-calibrating.tsx +3 -3
- package/src/suite/components/today-ui.tsx +42 -37
- package/src/suite/components/workspace-switcher.tsx +3 -3
- package/src/suite/index.ts +14 -0
- package/src/suite/lib/suite-notifications.tsx +5 -0
- package/tailwind-preset.ts +9 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { Children, type ComponentPropsWithoutRef, type ReactNode } from "react";
|
|
2
2
|
import { cn } from "@/lib/cn";
|
|
3
3
|
|
|
4
4
|
export type AvatarSize = "xs" | "sm" | "md" | "lg" | "xl";
|
|
@@ -38,7 +38,7 @@ export function Avatar({
|
|
|
38
38
|
label,
|
|
39
39
|
icon,
|
|
40
40
|
size = "md",
|
|
41
|
-
color = "bg-ph-brand text-
|
|
41
|
+
color = "bg-ph-brand text-[var(--ph-on-accent)]",
|
|
42
42
|
status,
|
|
43
43
|
badge,
|
|
44
44
|
className,
|
|
@@ -48,7 +48,7 @@ export function Avatar({
|
|
|
48
48
|
<div className={cn("relative inline-flex shrink-0", className)} {...props}>
|
|
49
49
|
<div
|
|
50
50
|
className={cn(
|
|
51
|
-
"flex items-center justify-center rounded-full border border-ph-border font-bold
|
|
51
|
+
"ph-avatar flex items-center justify-center rounded-full border border-ph-border font-bold",
|
|
52
52
|
sizeMap[size],
|
|
53
53
|
color,
|
|
54
54
|
)}
|
|
@@ -66,7 +66,7 @@ export function Avatar({
|
|
|
66
66
|
/>
|
|
67
67
|
)}
|
|
68
68
|
{badge && (
|
|
69
|
-
<span className="absolute -bottom-1 -right-1 flex min-w-[22px] items-center justify-center rounded-full bg-ph-brand px-1.5 py-px text-[10px] font-bold text-
|
|
69
|
+
<span className="absolute -bottom-1 -right-1 flex min-w-[22px] items-center justify-center rounded-full bg-ph-brand px-1.5 py-px text-[10px] font-bold text-[var(--ph-on-accent)]">
|
|
70
70
|
{badge}
|
|
71
71
|
</span>
|
|
72
72
|
)}
|
|
@@ -80,7 +80,7 @@ export interface AvatarGroupProps extends ComponentPropsWithoutRef<"div"> {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export function AvatarGroup({ children, max, className, ...props }: AvatarGroupProps) {
|
|
83
|
-
const items =
|
|
83
|
+
const items = Children.toArray(children);
|
|
84
84
|
const visible = max ? items.slice(0, max) : items;
|
|
85
85
|
const remaining = max ? items.length - max : 0;
|
|
86
86
|
|
|
@@ -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
|
+
}
|
|
@@ -157,13 +157,13 @@ export function Calendar({ value, onChange, className }: CalendarProps) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
return (
|
|
160
|
-
<div className={cn("w-[280px]
|
|
160
|
+
<div className={cn("ph-calendar w-[280px] p-4", className)}>
|
|
161
161
|
<div className="mb-4 flex items-center justify-between">
|
|
162
162
|
<button
|
|
163
163
|
type="button"
|
|
164
164
|
onClick={() => changeMonth(-1)}
|
|
165
165
|
aria-label="Previous month"
|
|
166
|
-
className="
|
|
166
|
+
className="ph-calendar__nav"
|
|
167
167
|
>
|
|
168
168
|
<IconChevronLeft className="h-5 w-5" aria-hidden />
|
|
169
169
|
</button>
|
|
@@ -174,7 +174,7 @@ export function Calendar({ value, onChange, className }: CalendarProps) {
|
|
|
174
174
|
type="button"
|
|
175
175
|
onClick={() => changeMonth(1)}
|
|
176
176
|
aria-label="Next month"
|
|
177
|
-
className="
|
|
177
|
+
className="ph-calendar__nav"
|
|
178
178
|
>
|
|
179
179
|
<IconChevronRight className="h-5 w-5" aria-hidden />
|
|
180
180
|
</button>
|
|
@@ -215,12 +215,12 @@ export function Calendar({ value, onChange, className }: CalendarProps) {
|
|
|
215
215
|
onClick={() => handleSelect(date)}
|
|
216
216
|
onKeyDown={(event) => handleDayKeyDown(event, date)}
|
|
217
217
|
className={cn(
|
|
218
|
-
"
|
|
218
|
+
"ph-calendar__day",
|
|
219
219
|
value && isSameDay(value, date)
|
|
220
|
-
? "
|
|
220
|
+
? "ph-calendar__day--selected"
|
|
221
221
|
: isSameDay(today, date)
|
|
222
|
-
? "
|
|
223
|
-
:
|
|
222
|
+
? "ph-calendar__day--today"
|
|
223
|
+
: undefined,
|
|
224
224
|
)}
|
|
225
225
|
>
|
|
226
226
|
{date.getDate()}
|
|
@@ -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,27 +15,29 @@ 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
|
};
|
|
21
31
|
|
|
22
32
|
return (
|
|
23
|
-
<div className={cn("
|
|
33
|
+
<div className={cn("ph-code-block", className)}>
|
|
24
34
|
{(filename || language) && (
|
|
25
35
|
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2">
|
|
26
|
-
<
|
|
27
|
-
<div className="flex gap-1.5">
|
|
28
|
-
<div className="h-3 w-3 rounded-full bg-red-400" />
|
|
29
|
-
<div className="h-3 w-3 rounded-full bg-amber-400" />
|
|
30
|
-
<div className="h-3 w-3 rounded-full bg-emerald-400" />
|
|
31
|
-
</div>
|
|
32
|
-
{filename && <span className="ml-2 text-xs text-white/60">{filename}</span>}
|
|
33
|
-
</div>
|
|
36
|
+
<span className="text-xs text-white/60">{filename ?? language}</span>
|
|
34
37
|
<button
|
|
35
38
|
type="button"
|
|
36
39
|
onClick={handleCopy}
|
|
37
|
-
className="
|
|
40
|
+
className="ph-code-block__copy"
|
|
38
41
|
>
|
|
39
42
|
{copied ? "Copied!" : "Copy"}
|
|
40
43
|
</button>
|
|
@@ -28,7 +28,7 @@ export function CollapsibleSection({
|
|
|
28
28
|
onClick={() => setIsOpen(!isOpen)}
|
|
29
29
|
aria-expanded={isOpen}
|
|
30
30
|
aria-controls={contentId}
|
|
31
|
-
className="
|
|
31
|
+
className="ph-collapsible-trigger mb-6"
|
|
32
32
|
>
|
|
33
33
|
<span className="text-lg font-bold text-ph-ink">{title}</span>
|
|
34
34
|
<IconChevronDown
|
|
@@ -46,11 +46,11 @@ export function CollapsibleSection({
|
|
|
46
46
|
if (node) node.toggleAttribute("inert", !isOpen);
|
|
47
47
|
}}
|
|
48
48
|
className={cn(
|
|
49
|
-
"
|
|
50
|
-
isOpen ? "
|
|
49
|
+
"grid transition-[grid-template-rows,opacity] duration-200",
|
|
50
|
+
isOpen ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
|
|
51
51
|
)}
|
|
52
52
|
>
|
|
53
|
-
{children}
|
|
53
|
+
<div className="overflow-hidden">{children}</div>
|
|
54
54
|
</div>
|
|
55
55
|
</section>
|
|
56
56
|
);
|
|
@@ -106,13 +106,10 @@ export function CommandPalette({
|
|
|
106
106
|
<Dialog.Overlay
|
|
107
107
|
id={id}
|
|
108
108
|
data-test={dataTest}
|
|
109
|
-
className="
|
|
109
|
+
className="ph-command-palette__overlay"
|
|
110
110
|
/>
|
|
111
111
|
<Dialog.Content
|
|
112
|
-
className={cn(
|
|
113
|
-
"fixed left-1/2 top-[20vh] z-50 w-full max-w-xl -translate-x-1/2 overflow-hidden rounded-xl border border-ph-border bg-ph-surface shadow-ph-md",
|
|
114
|
-
className,
|
|
115
|
-
)}
|
|
112
|
+
className={cn("ph-command-palette", className)}
|
|
116
113
|
onOpenAutoFocus={(event) => {
|
|
117
114
|
restoreFocusRef.current =
|
|
118
115
|
document.activeElement instanceof HTMLElement
|
|
@@ -127,7 +124,7 @@ export function CommandPalette({
|
|
|
127
124
|
}}
|
|
128
125
|
>
|
|
129
126
|
<Dialog.Title className="sr-only">Command palette</Dialog.Title>
|
|
130
|
-
<div className="
|
|
127
|
+
<div className="ph-command-palette__search">
|
|
131
128
|
<IconSearch className="h-5 w-5 text-ph-mutedtext" />
|
|
132
129
|
<input
|
|
133
130
|
ref={inputRef}
|
|
@@ -149,7 +146,7 @@ export function CommandPalette({
|
|
|
149
146
|
placeholder={placeholder}
|
|
150
147
|
className="flex-1 bg-transparent text-ph-ink outline-none placeholder:text-ph-mutedtext"
|
|
151
148
|
/>
|
|
152
|
-
<kbd aria-hidden="true" className="
|
|
149
|
+
<kbd aria-hidden="true" className="ph-keycap">
|
|
153
150
|
ESC
|
|
154
151
|
</kbd>
|
|
155
152
|
</div>
|
|
@@ -172,16 +169,16 @@ export function CommandPalette({
|
|
|
172
169
|
}}
|
|
173
170
|
onMouseEnter={() => setSelectedIndex(index)}
|
|
174
171
|
className={cn(
|
|
175
|
-
"
|
|
172
|
+
"ph-command-palette__option",
|
|
176
173
|
index === activeIndex
|
|
177
|
-
? "
|
|
174
|
+
? "ph-command-palette__option--selected"
|
|
178
175
|
: "text-ph-subtle",
|
|
179
176
|
)}
|
|
180
177
|
>
|
|
181
178
|
{item.icon && <span aria-hidden="true" className="h-5 w-5">{item.icon}</span>}
|
|
182
179
|
<span className="flex-1">{item.label}</span>
|
|
183
180
|
{item.shortcut && (
|
|
184
|
-
<kbd aria-hidden="true" className="
|
|
181
|
+
<kbd aria-hidden="true" className="ph-keycap text-ph-mutedtext">
|
|
185
182
|
{item.shortcut}
|
|
186
183
|
</kbd>
|
|
187
184
|
)}
|
|
@@ -14,12 +14,12 @@ export function EmptyState({ icon, title, description, action, className }: Empt
|
|
|
14
14
|
return (
|
|
15
15
|
<div
|
|
16
16
|
className={cn(
|
|
17
|
-
"
|
|
17
|
+
"ph-empty-state",
|
|
18
18
|
className
|
|
19
19
|
)}
|
|
20
20
|
>
|
|
21
21
|
{icon && (
|
|
22
|
-
<div className="
|
|
22
|
+
<div className="ph-empty-state__icon">
|
|
23
23
|
{icon}
|
|
24
24
|
</div>
|
|
25
25
|
)}
|
|
@@ -88,7 +88,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(function Input({
|
|
|
88
88
|
aria-label={revealed ? "Hide password" : "Show password"}
|
|
89
89
|
aria-pressed={revealed}
|
|
90
90
|
disabled={disabled}
|
|
91
|
-
className="
|
|
91
|
+
className="ph-field-icon-button"
|
|
92
92
|
>
|
|
93
93
|
{revealed ? (
|
|
94
94
|
<EyeClosed className="h-4 w-4" aria-hidden />
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
type AnchorHTMLAttributes,
|
|
8
|
+
type HTMLAttributes,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { Pause, Play } from "iconoir-react";
|
|
12
|
+
import { ButtonChrome } from "@/components/ui/ButtonChrome";
|
|
13
|
+
import { cn } from "@/lib/cn";
|
|
14
|
+
|
|
15
|
+
/** Pure presentation slots: every product owns its links and authentication state. */
|
|
16
|
+
export function MarketingHeader({
|
|
17
|
+
brand,
|
|
18
|
+
navigation,
|
|
19
|
+
actions,
|
|
20
|
+
className,
|
|
21
|
+
...props
|
|
22
|
+
}: HTMLAttributes<HTMLElement> & {
|
|
23
|
+
brand: ReactNode;
|
|
24
|
+
navigation: ReactNode;
|
|
25
|
+
actions: ReactNode;
|
|
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
|
+
|
|
39
|
+
return (
|
|
40
|
+
<header
|
|
41
|
+
{...props}
|
|
42
|
+
data-scrolled={scrolled ? "true" : "false"}
|
|
43
|
+
className={cn("marketing-header", className)}
|
|
44
|
+
>
|
|
45
|
+
<div className="marketing-header__inner">
|
|
46
|
+
<div className="marketing-header__brand">{brand}</div>
|
|
47
|
+
<nav
|
|
48
|
+
className="marketing-header__navigation"
|
|
49
|
+
aria-label="Product navigation"
|
|
50
|
+
>
|
|
51
|
+
{navigation}
|
|
52
|
+
</nav>
|
|
53
|
+
<div className="marketing-header__actions">{actions}</div>
|
|
54
|
+
</div>
|
|
55
|
+
</header>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function MarketingLink({
|
|
60
|
+
variant = "secondary",
|
|
61
|
+
icon,
|
|
62
|
+
sideIcon,
|
|
63
|
+
children,
|
|
64
|
+
className,
|
|
65
|
+
...props
|
|
66
|
+
}: AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
67
|
+
variant?: "primary" | "secondary" | "accent";
|
|
68
|
+
icon?: ReactNode;
|
|
69
|
+
sideIcon?: ReactNode;
|
|
70
|
+
}) {
|
|
71
|
+
return (
|
|
72
|
+
<a
|
|
73
|
+
{...props}
|
|
74
|
+
className={cn(
|
|
75
|
+
"ph-btn ph-btn-raised marketing-button",
|
|
76
|
+
`ph-btn-${variant}`,
|
|
77
|
+
className,
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
<ButtonChrome label={children} icon={icon} sideIcon={sideIcon} />
|
|
81
|
+
</a>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Keep animated artwork controllable without taking space away from the nav. */
|
|
86
|
+
export function MarketingMotionControl() {
|
|
87
|
+
const [paused, setPaused] = useState(false);
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
document.documentElement.toggleAttribute("data-marketing-paused", paused);
|
|
90
|
+
return () =>
|
|
91
|
+
document.documentElement.removeAttribute("data-marketing-paused");
|
|
92
|
+
}, [paused]);
|
|
93
|
+
return (
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
className="marketing-motion-control"
|
|
97
|
+
onClick={() => setPaused((value) => !value)}
|
|
98
|
+
aria-pressed={paused}
|
|
99
|
+
aria-label={
|
|
100
|
+
paused ? "Play landing animations" : "Pause landing animations"
|
|
101
|
+
}
|
|
102
|
+
>
|
|
103
|
+
{paused ? (
|
|
104
|
+
<Play className="h-4 w-4" aria-hidden />
|
|
105
|
+
) : (
|
|
106
|
+
<Pause className="h-4 w-4" aria-hidden />
|
|
107
|
+
)}
|
|
108
|
+
<span>{paused ? "Play motion" : "Pause motion"}</span>
|
|
109
|
+
</button>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Native scroll reveal; content remains visible when JavaScript is unavailable. */
|
|
114
|
+
export function MarketingReveal({
|
|
115
|
+
children,
|
|
116
|
+
className,
|
|
117
|
+
...props
|
|
118
|
+
}: HTMLAttributes<HTMLDivElement>) {
|
|
119
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const element = ref.current;
|
|
122
|
+
if (
|
|
123
|
+
!element ||
|
|
124
|
+
!window.IntersectionObserver ||
|
|
125
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
126
|
+
)
|
|
127
|
+
return;
|
|
128
|
+
const observer = new IntersectionObserver(
|
|
129
|
+
([entry]) => {
|
|
130
|
+
if (!entry.isIntersecting) return;
|
|
131
|
+
element.classList.add("marketing-reveal--entered");
|
|
132
|
+
observer.disconnect();
|
|
133
|
+
},
|
|
134
|
+
{ threshold: 0.1 },
|
|
135
|
+
);
|
|
136
|
+
observer.observe(element);
|
|
137
|
+
return () => observer.disconnect();
|
|
138
|
+
}, []);
|
|
139
|
+
return (
|
|
140
|
+
<div {...props} ref={ref} className={cn("marketing-reveal", className)}>
|
|
141
|
+
{children}
|
|
142
|
+
</div>
|
|
143
|
+
);
|
|
144
|
+
}
|