@xenide-io/the-old-ui-theme 0.9.17 → 0.10.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-DKT-82Lf.d.mts} +5 -3
- package/dist/{Chip-BzuNgJIa.d.ts → Chip-DKT-82Lf.d.ts} +5 -3
- package/dist/{chunk-6SVJAU5K.mjs → chunk-LHE4ROON.mjs} +977 -521
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +138 -99
- package/dist/index.mjs +1 -1
- package/dist/suite.d.mts +23 -1
- package/dist/suite.d.ts +23 -1
- package/dist/suite.js +4250 -4041
- package/dist/suite.mjs +1008 -795
- 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 +1024 -566
- package/dist/ui.mjs +13 -1
- package/docs/macos-identity-audit.md +279 -0
- package/package.json +1 -1
- package/src/app/globals.css +334 -44
- package/src/components/ui/Accordion.tsx +4 -7
- package/src/components/ui/Avatar.tsx +5 -5
- package/src/components/ui/Calendar.tsx +7 -7
- package/src/components/ui/CodeBlock.tsx +3 -10
- 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 +128 -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/ShowcaseWrapper.tsx +3 -3
- package/src/components/ui/Table.tsx +18 -6
- package/src/components/ui/Typography.tsx +4 -4
- package/src/components/ui/avatar-stat.test.tsx +14 -1
- package/src/components/ui/index.ts +13 -12
- 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 +20 -0
- package/src/styles/suite-skin.css +405 -40
- package/src/styles/themes.css +45 -45
- package/src/styles/typography.css +25 -1
- package/src/suite/components/ai-panel.tsx +17 -17
- package/src/suite/components/app-switcher.tsx +9 -9
- 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-entity-icon.tsx +3 -3
- 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 +4 -4
- package/src/suite/components/suite-notification-settings.tsx +171 -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 +5 -9
- 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/today-calibrating.tsx +3 -3
- package/src/suite/components/today-ui.tsx +12 -12
- package/src/suite/components/workspace-switcher.tsx +3 -3
- package/src/suite/index.ts +6 -0
- package/tailwind-preset.ts +9 -0
|
@@ -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()}
|
|
@@ -20,21 +20,14 @@ export function CodeBlock({ code, language = "tsx", filename, className }: CodeB
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
return (
|
|
23
|
-
<div className={cn("
|
|
23
|
+
<div className={cn("ph-code-block", className)}>
|
|
24
24
|
{(filename || language) && (
|
|
25
25
|
<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>
|
|
26
|
+
<span className="text-xs text-white/60">{filename ?? language}</span>
|
|
34
27
|
<button
|
|
35
28
|
type="button"
|
|
36
29
|
onClick={handleCopy}
|
|
37
|
-
className="
|
|
30
|
+
className="ph-code-block__copy"
|
|
38
31
|
>
|
|
39
32
|
{copied ? "Copied!" : "Copy"}
|
|
40
33
|
</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,128 @@
|
|
|
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
|
+
return (
|
|
28
|
+
<header {...props} className={cn("marketing-header", className)}>
|
|
29
|
+
<div className="marketing-header__inner">
|
|
30
|
+
<div className="marketing-header__brand">{brand}</div>
|
|
31
|
+
<nav
|
|
32
|
+
className="marketing-header__navigation"
|
|
33
|
+
aria-label="Product navigation"
|
|
34
|
+
>
|
|
35
|
+
{navigation}
|
|
36
|
+
</nav>
|
|
37
|
+
<div className="marketing-header__actions">{actions}</div>
|
|
38
|
+
</div>
|
|
39
|
+
</header>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function MarketingLink({
|
|
44
|
+
variant = "secondary",
|
|
45
|
+
icon,
|
|
46
|
+
sideIcon,
|
|
47
|
+
children,
|
|
48
|
+
className,
|
|
49
|
+
...props
|
|
50
|
+
}: AnchorHTMLAttributes<HTMLAnchorElement> & {
|
|
51
|
+
variant?: "primary" | "secondary" | "accent";
|
|
52
|
+
icon?: ReactNode;
|
|
53
|
+
sideIcon?: ReactNode;
|
|
54
|
+
}) {
|
|
55
|
+
return (
|
|
56
|
+
<a
|
|
57
|
+
{...props}
|
|
58
|
+
className={cn(
|
|
59
|
+
"ph-btn ph-btn-raised marketing-button",
|
|
60
|
+
`ph-btn-${variant}`,
|
|
61
|
+
className,
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<ButtonChrome label={children} icon={icon} sideIcon={sideIcon} />
|
|
65
|
+
</a>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Keep animated artwork controllable without taking space away from the nav. */
|
|
70
|
+
export function MarketingMotionControl() {
|
|
71
|
+
const [paused, setPaused] = useState(false);
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
document.documentElement.toggleAttribute("data-marketing-paused", paused);
|
|
74
|
+
return () =>
|
|
75
|
+
document.documentElement.removeAttribute("data-marketing-paused");
|
|
76
|
+
}, [paused]);
|
|
77
|
+
return (
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
className="marketing-motion-control"
|
|
81
|
+
onClick={() => setPaused((value) => !value)}
|
|
82
|
+
aria-pressed={paused}
|
|
83
|
+
aria-label={
|
|
84
|
+
paused ? "Play landing animations" : "Pause landing animations"
|
|
85
|
+
}
|
|
86
|
+
>
|
|
87
|
+
{paused ? (
|
|
88
|
+
<Play className="h-4 w-4" aria-hidden />
|
|
89
|
+
) : (
|
|
90
|
+
<Pause className="h-4 w-4" aria-hidden />
|
|
91
|
+
)}
|
|
92
|
+
<span>{paused ? "Play motion" : "Pause motion"}</span>
|
|
93
|
+
</button>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Native scroll reveal; content remains visible when JavaScript is unavailable. */
|
|
98
|
+
export function MarketingReveal({
|
|
99
|
+
children,
|
|
100
|
+
className,
|
|
101
|
+
...props
|
|
102
|
+
}: HTMLAttributes<HTMLDivElement>) {
|
|
103
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
const element = ref.current;
|
|
106
|
+
if (
|
|
107
|
+
!element ||
|
|
108
|
+
!window.IntersectionObserver ||
|
|
109
|
+
window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
110
|
+
)
|
|
111
|
+
return;
|
|
112
|
+
const observer = new IntersectionObserver(
|
|
113
|
+
([entry]) => {
|
|
114
|
+
if (!entry.isIntersecting) return;
|
|
115
|
+
element.classList.add("marketing-reveal--entered");
|
|
116
|
+
observer.disconnect();
|
|
117
|
+
},
|
|
118
|
+
{ threshold: 0.1 },
|
|
119
|
+
);
|
|
120
|
+
observer.observe(element);
|
|
121
|
+
return () => observer.disconnect();
|
|
122
|
+
}, []);
|
|
123
|
+
return (
|
|
124
|
+
<div {...props} ref={ref} className={cn("marketing-reveal", className)}>
|
|
125
|
+
{children}
|
|
126
|
+
</div>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useId } from "react";
|
|
4
|
+
|
|
5
|
+
export function AiWritingIllustration({
|
|
6
|
+
title,
|
|
7
|
+
prompt,
|
|
8
|
+
before,
|
|
9
|
+
after,
|
|
10
|
+
}: {
|
|
11
|
+
title: string;
|
|
12
|
+
prompt: string;
|
|
13
|
+
before: string[];
|
|
14
|
+
after: string[];
|
|
15
|
+
}) {
|
|
16
|
+
const id = useId().replaceAll(":", "");
|
|
17
|
+
return (
|
|
18
|
+
<svg
|
|
19
|
+
viewBox="0 0 560 370"
|
|
20
|
+
className="h-auto w-full"
|
|
21
|
+
role="img"
|
|
22
|
+
aria-label={`${title}: use /ai and Shelly AI to draft and polish writing`}
|
|
23
|
+
data-marketing-art
|
|
24
|
+
>
|
|
25
|
+
<defs>
|
|
26
|
+
<radialGradient id={`${id}-glow`}>
|
|
27
|
+
<stop stopColor="var(--ph-accent)" stopOpacity=".2" />
|
|
28
|
+
<stop offset="1" stopColor="var(--ph-accent)" stopOpacity="0" />
|
|
29
|
+
</radialGradient>
|
|
30
|
+
<linearGradient id={`${id}-paper`} x2="1" y2="1">
|
|
31
|
+
<stop stopColor="var(--ph-surface)" />
|
|
32
|
+
<stop offset="1" stopColor="var(--ph-muted)" />
|
|
33
|
+
</linearGradient>
|
|
34
|
+
</defs>
|
|
35
|
+
<ellipse cx="280" cy="198" rx="260" ry="168" fill={`url(#${id}-glow)`} />
|
|
36
|
+
<g className="marketing-svg-float">
|
|
37
|
+
<rect
|
|
38
|
+
x="18"
|
|
39
|
+
y="28"
|
|
40
|
+
width="524"
|
|
41
|
+
height="300"
|
|
42
|
+
rx="20"
|
|
43
|
+
fill={`url(#${id}-paper)`}
|
|
44
|
+
stroke="var(--ph-border)"
|
|
45
|
+
/>
|
|
46
|
+
<path d="M18 76h524" stroke="var(--ph-border)" />
|
|
47
|
+
<image href="/shelly-icon.svg" x="36" y="40" width="24" height="24" />
|
|
48
|
+
<text
|
|
49
|
+
x="70"
|
|
50
|
+
y="57"
|
|
51
|
+
fontSize="16"
|
|
52
|
+
fontWeight="700"
|
|
53
|
+
fill="var(--ph-text-primary)"
|
|
54
|
+
>
|
|
55
|
+
{title}
|
|
56
|
+
</text>
|
|
57
|
+
<text x="434" y="57" fontSize="12" fill="var(--ph-text-secondary)">
|
|
58
|
+
Shelly AI
|
|
59
|
+
</text>
|
|
60
|
+
<rect
|
|
61
|
+
x="38"
|
|
62
|
+
y="94"
|
|
63
|
+
width="484"
|
|
64
|
+
height="42"
|
|
65
|
+
rx="10"
|
|
66
|
+
fill="var(--ph-canvas)"
|
|
67
|
+
stroke="var(--ph-selected-border)"
|
|
68
|
+
/>
|
|
69
|
+
<text x="54" y="121" fontSize="16" fill="var(--ph-text-primary)">
|
|
70
|
+
{prompt}
|
|
71
|
+
</text>
|
|
72
|
+
<g className="marketing-svg-draft">
|
|
73
|
+
<text
|
|
74
|
+
x="46"
|
|
75
|
+
y="171"
|
|
76
|
+
fontSize="12"
|
|
77
|
+
fontWeight="700"
|
|
78
|
+
fill="var(--ph-text-secondary)"
|
|
79
|
+
>
|
|
80
|
+
YOUR DRAFT
|
|
81
|
+
</text>
|
|
82
|
+
{before.map((line, index) => (
|
|
83
|
+
<text
|
|
84
|
+
key={index}
|
|
85
|
+
x="46"
|
|
86
|
+
y={205 + index * 26}
|
|
87
|
+
fontSize="16"
|
|
88
|
+
fill="var(--ph-text-secondary)"
|
|
89
|
+
>
|
|
90
|
+
{line}
|
|
91
|
+
</text>
|
|
92
|
+
))}
|
|
93
|
+
</g>
|
|
94
|
+
<path
|
|
95
|
+
className="marketing-svg-thread"
|
|
96
|
+
d="M265 175v102"
|
|
97
|
+
stroke="var(--ph-accent)"
|
|
98
|
+
strokeWidth="2"
|
|
99
|
+
strokeDasharray="4 7"
|
|
100
|
+
/>
|
|
101
|
+
<g className="marketing-svg-polished">
|
|
102
|
+
<text
|
|
103
|
+
x="294"
|
|
104
|
+
y="171"
|
|
105
|
+
fontSize="12"
|
|
106
|
+
fontWeight="700"
|
|
107
|
+
fill="var(--ph-accent)"
|
|
108
|
+
>
|
|
109
|
+
POLISHED WITH SHELLY
|
|
110
|
+
</text>
|
|
111
|
+
{after.map((line, index) => (
|
|
112
|
+
<text
|
|
113
|
+
key={index}
|
|
114
|
+
x="294"
|
|
115
|
+
y={205 + index * 26}
|
|
116
|
+
fontSize="16"
|
|
117
|
+
fill="var(--ph-text-primary)"
|
|
118
|
+
>
|
|
119
|
+
{line}
|
|
120
|
+
</text>
|
|
121
|
+
))}
|
|
122
|
+
<path
|
|
123
|
+
d="m302 294 5 5 10-11"
|
|
124
|
+
fill="none"
|
|
125
|
+
stroke="var(--ph-success)"
|
|
126
|
+
strokeWidth="3"
|
|
127
|
+
strokeLinecap="round"
|
|
128
|
+
/>
|
|
129
|
+
<text x="328" y="300" fontSize="12" fill="var(--ph-text-secondary)">
|
|
130
|
+
Ready to keep working
|
|
131
|
+
</text>
|
|
132
|
+
</g>
|
|
133
|
+
</g>
|
|
134
|
+
<g className="marketing-svg-spark" fill="var(--ph-accent)">
|
|
135
|
+
<path d="m520 12 3 10 10 3-10 3-3 10-3-10-10-3 10-3z" />
|
|
136
|
+
<path d="m25 310 2 7 7 2-7 2-2 7-2-7-7-2 7-2z" />
|
|
137
|
+
</g>
|
|
138
|
+
</svg>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function IntegrationBridgeIllustration({
|
|
143
|
+
appName,
|
|
144
|
+
appIcon,
|
|
145
|
+
recordLabel,
|
|
146
|
+
calendar = false,
|
|
147
|
+
}: {
|
|
148
|
+
appName: string;
|
|
149
|
+
appIcon: string;
|
|
150
|
+
recordLabel: string;
|
|
151
|
+
calendar?: boolean;
|
|
152
|
+
}) {
|
|
153
|
+
const id = useId().replaceAll(":", "");
|
|
154
|
+
const rows = calendar
|
|
155
|
+
? ["Client catch-up", "10:00 – 11:00", "Meeting notes"]
|
|
156
|
+
: ["Client", "Project", recordLabel];
|
|
157
|
+
return (
|
|
158
|
+
<svg
|
|
159
|
+
viewBox="0 0 600 350"
|
|
160
|
+
className="h-auto w-full"
|
|
161
|
+
role="img"
|
|
162
|
+
aria-label={
|
|
163
|
+
calendar
|
|
164
|
+
? "Choose a calendar event to prefill a TurtleTime entry"
|
|
165
|
+
: `Shelly Bridge matches connected records with ${appName}`
|
|
166
|
+
}
|
|
167
|
+
data-marketing-art
|
|
168
|
+
>
|
|
169
|
+
<defs>
|
|
170
|
+
<radialGradient id={`${id}-bridge`}>
|
|
171
|
+
<stop stopColor="var(--ph-blue)" stopOpacity=".17" />
|
|
172
|
+
<stop offset="1" stopColor="var(--ph-blue)" stopOpacity="0" />
|
|
173
|
+
</radialGradient>
|
|
174
|
+
</defs>
|
|
175
|
+
<ellipse
|
|
176
|
+
cx="300"
|
|
177
|
+
cy="185"
|
|
178
|
+
rx="280"
|
|
179
|
+
ry="160"
|
|
180
|
+
fill={`url(#${id}-bridge)`}
|
|
181
|
+
/>
|
|
182
|
+
<g className="marketing-svg-float">
|
|
183
|
+
<rect
|
|
184
|
+
x="18"
|
|
185
|
+
y="55"
|
|
186
|
+
width="230"
|
|
187
|
+
height="240"
|
|
188
|
+
rx="18"
|
|
189
|
+
fill="var(--ph-surface)"
|
|
190
|
+
stroke="var(--ph-border)"
|
|
191
|
+
/>
|
|
192
|
+
<rect
|
|
193
|
+
x="352"
|
|
194
|
+
y="55"
|
|
195
|
+
width="230"
|
|
196
|
+
height="240"
|
|
197
|
+
rx="18"
|
|
198
|
+
fill="var(--ph-surface)"
|
|
199
|
+
stroke="var(--ph-border)"
|
|
200
|
+
/>
|
|
201
|
+
<text
|
|
202
|
+
x="38"
|
|
203
|
+
y="92"
|
|
204
|
+
fontSize="17"
|
|
205
|
+
fontWeight="700"
|
|
206
|
+
fill="var(--ph-text-primary)"
|
|
207
|
+
>
|
|
208
|
+
{calendar ? "Calendar events" : "Connected tools"}
|
|
209
|
+
</text>
|
|
210
|
+
<image href={appIcon} x="370" y="72" width="28" height="28" />
|
|
211
|
+
<text
|
|
212
|
+
x="410"
|
|
213
|
+
y="93"
|
|
214
|
+
fontSize="17"
|
|
215
|
+
fontWeight="700"
|
|
216
|
+
fill="var(--ph-text-primary)"
|
|
217
|
+
>
|
|
218
|
+
{appName}
|
|
219
|
+
</text>
|
|
220
|
+
{rows.map((row, index) => (
|
|
221
|
+
<g key={row}>
|
|
222
|
+
<rect
|
|
223
|
+
x="36"
|
|
224
|
+
y={119 + index * 48}
|
|
225
|
+
width="194"
|
|
226
|
+
height="36"
|
|
227
|
+
rx="8"
|
|
228
|
+
fill="var(--ph-muted)"
|
|
229
|
+
/>
|
|
230
|
+
<text
|
|
231
|
+
x="50"
|
|
232
|
+
y={142 + index * 48}
|
|
233
|
+
fontSize="14"
|
|
234
|
+
fill="var(--ph-text-secondary)"
|
|
235
|
+
>
|
|
236
|
+
{row}
|
|
237
|
+
</text>
|
|
238
|
+
<rect
|
|
239
|
+
x="370"
|
|
240
|
+
y={119 + index * 48}
|
|
241
|
+
width="194"
|
|
242
|
+
height="36"
|
|
243
|
+
rx="8"
|
|
244
|
+
fill="var(--ph-muted)"
|
|
245
|
+
/>
|
|
246
|
+
<text
|
|
247
|
+
x="384"
|
|
248
|
+
y={142 + index * 48}
|
|
249
|
+
fontSize="14"
|
|
250
|
+
fill="var(--ph-text-primary)"
|
|
251
|
+
>
|
|
252
|
+
{calendar && index === 1 ? "1 hour, ready to review" : row}
|
|
253
|
+
</text>
|
|
254
|
+
<path
|
|
255
|
+
className="marketing-svg-thread"
|
|
256
|
+
d={`M248 ${137 + index * 48}h104`}
|
|
257
|
+
stroke="var(--ph-accent)"
|
|
258
|
+
strokeWidth="2"
|
|
259
|
+
strokeDasharray="5 8"
|
|
260
|
+
/>
|
|
261
|
+
</g>
|
|
262
|
+
))}
|
|
263
|
+
</g>
|
|
264
|
+
<g className="marketing-svg-polished">
|
|
265
|
+
<circle
|
|
266
|
+
cx="300"
|
|
267
|
+
cy="185"
|
|
268
|
+
r="27"
|
|
269
|
+
fill="var(--ph-canvas)"
|
|
270
|
+
stroke="var(--ph-selected-border)"
|
|
271
|
+
/>
|
|
272
|
+
<image href="/shelly-icon.svg" x="280" y="165" width="40" height="40" />
|
|
273
|
+
<text
|
|
274
|
+
x="300"
|
|
275
|
+
y="331"
|
|
276
|
+
textAnchor="middle"
|
|
277
|
+
fontSize="13"
|
|
278
|
+
fill="var(--ph-text-secondary)"
|
|
279
|
+
>
|
|
280
|
+
{calendar
|
|
281
|
+
? "Prefill details · review · save the entry"
|
|
282
|
+
: "Shelly Bridge · match the records you want to connect"}
|
|
283
|
+
</text>
|
|
284
|
+
</g>
|
|
285
|
+
</svg>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
@@ -10,7 +10,7 @@ export interface PanelProps extends Omit<ComponentPropsWithoutRef<"div">, "title
|
|
|
10
10
|
export function Panel({ title, description, children, className, ...props }: PanelProps) {
|
|
11
11
|
return (
|
|
12
12
|
<div className={cn("ph-panel", className)} {...props}>
|
|
13
|
-
{title && <h3 className="text-
|
|
13
|
+
{title && <h3 className="text-lg font-semibold tracking-tight text-ph-ink">{title}</h3>}
|
|
14
14
|
{description && <p className="text-sm text-ph-subtle">{description}</p>}
|
|
15
15
|
{children}
|
|
16
16
|
</div>
|