@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.
Files changed (79) hide show
  1. package/dist/{Chip-BzuNgJIa.d.mts → Chip-Dw2g2t8D.d.mts} +18 -4
  2. package/dist/{Chip-BzuNgJIa.d.ts → Chip-Dw2g2t8D.d.ts} +18 -4
  3. package/dist/{chunk-6SVJAU5K.mjs → chunk-RIPGN6FG.mjs} +1333 -526
  4. package/dist/index.d.mts +3 -2
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.js +490 -119
  7. package/dist/index.mjs +11 -3
  8. package/dist/suite-analytics--Mf1RZbw.d.mts +29 -0
  9. package/dist/suite-analytics--Mf1RZbw.d.ts +29 -0
  10. package/dist/suite.d.mts +36 -4
  11. package/dist/suite.d.ts +36 -4
  12. package/dist/suite.js +4617 -4001
  13. package/dist/suite.mjs +1456 -868
  14. package/dist/tailwind-preset.js +10 -1
  15. package/dist/tailwind-preset.mjs +10 -1
  16. package/dist/ui.d.mts +33 -4
  17. package/dist/ui.d.ts +33 -4
  18. package/dist/ui.js +1364 -571
  19. package/dist/ui.mjs +15 -1
  20. package/docs/macos-identity-audit.md +279 -0
  21. package/package.json +1 -1
  22. package/src/app/globals.css +390 -44
  23. package/src/components/ui/Accordion.tsx +4 -7
  24. package/src/components/ui/Avatar.tsx +5 -5
  25. package/src/components/ui/BrandSpinner.tsx +256 -0
  26. package/src/components/ui/Calendar.tsx +7 -7
  27. package/src/components/ui/CodeBlock.tsx +14 -11
  28. package/src/components/ui/CollapsibleSection.tsx +4 -4
  29. package/src/components/ui/CommandPalette.tsx +7 -10
  30. package/src/components/ui/EmptyState.tsx +2 -2
  31. package/src/components/ui/Input.tsx +1 -1
  32. package/src/components/ui/Marketing.tsx +144 -0
  33. package/src/components/ui/MarketingIllustrations.tsx +287 -0
  34. package/src/components/ui/Panel.tsx +1 -1
  35. package/src/components/ui/SegmentedControl.tsx +59 -13
  36. package/src/components/ui/SettingsLayout.tsx +4 -0
  37. package/src/components/ui/ShowcaseWrapper.tsx +3 -3
  38. package/src/components/ui/Table.tsx +45 -10
  39. package/src/components/ui/Typography.tsx +4 -4
  40. package/src/components/ui/avatar-stat.test.tsx +14 -1
  41. package/src/components/ui/brand-spinner.test.tsx +62 -0
  42. package/src/components/ui/index.ts +19 -11
  43. package/src/components/ui/interaction-primitives.test.tsx +12 -6
  44. package/src/components/ui/marketing.test.tsx +39 -0
  45. package/src/components/ui/table.test.tsx +47 -0
  46. package/src/styles/suite-skin.css +471 -74
  47. package/src/styles/themes.css +45 -45
  48. package/src/styles/typography.css +25 -1
  49. package/src/suite/components/ai-draft-editor.tsx +184 -0
  50. package/src/suite/components/ai-panel-workflows.test.tsx +179 -0
  51. package/src/suite/components/ai-panel.tsx +201 -31
  52. package/src/suite/components/app-switcher.tsx +22 -9
  53. package/src/suite/components/command-palette-host.tsx +20 -2
  54. package/src/suite/components/suite-app-layout.tsx +7 -4
  55. package/src/suite/components/suite-bottom-nav.tsx +1 -1
  56. package/src/suite/components/suite-clients-projects-directory.tsx +6 -6
  57. package/src/suite/components/suite-integration-picker.tsx +7 -7
  58. package/src/suite/components/suite-integration-rules.tsx +5 -5
  59. package/src/suite/components/suite-layout.test.tsx +32 -3
  60. package/src/suite/components/suite-layout.tsx +4 -4
  61. package/src/suite/components/suite-mobile-drawer.tsx +2 -2
  62. package/src/suite/components/suite-mobile-header.tsx +1 -1
  63. package/src/suite/components/suite-notification-bell.tsx +27 -6
  64. package/src/suite/components/suite-notification-settings.tsx +182 -0
  65. package/src/suite/components/suite-palette-library.test.tsx +46 -0
  66. package/src/suite/components/suite-palette-library.tsx +82 -15
  67. package/src/suite/components/suite-settings-layout.tsx +2 -2
  68. package/src/suite/components/suite-settings-mobile-nav.tsx +10 -10
  69. package/src/suite/components/suite-sidebar.test.tsx +39 -0
  70. package/src/suite/components/suite-sidebar.tsx +41 -19
  71. package/src/suite/components/suite-skeleton.tsx +6 -6
  72. package/src/suite/components/suite-user-menu.tsx +2 -2
  73. package/src/suite/components/theme-provider.tsx +2 -0
  74. package/src/suite/components/today-calibrating.tsx +3 -3
  75. package/src/suite/components/today-ui.tsx +42 -37
  76. package/src/suite/components/workspace-switcher.tsx +3 -3
  77. package/src/suite/index.ts +14 -0
  78. package/src/suite/lib/suite-notifications.tsx +5 -0
  79. package/tailwind-preset.ts +9 -0
@@ -1,4 +1,4 @@
1
- import type { ComponentPropsWithoutRef, ReactNode } from "react";
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-white",
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 shadow-ph",
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-white">
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 = Array.isArray(children) ? children : [children];
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] rounded-xl border border-ph-border bg-ph-surface p-4 shadow-ph", className)}>
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="rounded-lg p-1 text-ph-subtle transition hover:bg-ph-muted hover:text-ph-ink"
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="rounded-lg p-1 text-ph-subtle transition hover:bg-ph-muted hover:text-ph-ink"
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
- "flex h-full w-full items-center justify-center rounded-lg text-sm transition-colors",
218
+ "ph-calendar__day",
219
219
  value && isSameDay(value, date)
220
- ? "bg-ph-brand font-semibold text-[var(--ph-on-accent)] shadow-ph"
220
+ ? "ph-calendar__day--selected"
221
221
  : isSameDay(today, date)
222
- ? "bg-ph-muted font-semibold text-ph-brand ring-1 ring-ph-brand/30"
223
- : "text-ph-ink hover:bg-ph-muted",
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
- await navigator.clipboard.writeText(code);
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("overflow-hidden rounded-lg border border-ph-border bg-[#1a1a2e]", className)}>
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
- <div className="flex items-center gap-2">
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="text-xs text-white/60 transition hover:text-white"
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="mb-6 flex w-full items-center justify-between rounded-lg border border-ph-border bg-ph-muted px-5 py-4 text-left transition hover:bg-ph-muted/80"
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
- "overflow-hidden transition-all duration-300",
50
- isOpen ? "max-h-[50000px] opacity-100" : "max-h-0 opacity-0",
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="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm"
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="flex items-center gap-3 border-b border-ph-border px-4 py-3">
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="rounded border border-ph-border bg-ph-muted px-1.5 py-0.5 text-xs text-ph-mutedtext">
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
- "flex w-full cursor-pointer items-center gap-3 px-4 py-2.5 text-left text-sm transition-colors",
172
+ "ph-command-palette__option",
176
173
  index === activeIndex
177
- ? "bg-ph-muted text-ph-ink"
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="rounded border border-ph-border bg-ph-surface px-1.5 py-0.5 text-xs text-ph-mutedtext">
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
- "flex flex-col items-center justify-center rounded-xl border border-dashed border-ph-border bg-ph-muted/50 px-8 py-16 text-center",
17
+ "ph-empty-state",
18
18
  className
19
19
  )}
20
20
  >
21
21
  {icon && (
22
- <div className="mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-ph-muted text-ph-mutedtext">
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="absolute inset-y-0 right-0 flex w-10 items-center justify-center rounded-r-[var(--ph-radius-control)] text-ph-mutedtext transition-colors hover:text-ph-ink focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ph-focus disabled:opacity-40"
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
+ }