@xenide-io/the-old-ui-theme 0.3.2 → 0.4.3
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/chunk-54ZR4VL5.mjs +4641 -0
- package/dist/chunk-G53YLHVE.mjs +1023 -0
- package/dist/{index-BgG8Homu.d.mts → index-B5NOyoKS.d.mts} +100 -100
- package/dist/{index-BgG8Homu.d.ts → index-B5NOyoKS.d.ts} +100 -100
- package/dist/index.d.mts +9 -145
- package/dist/index.d.ts +9 -145
- package/dist/index.js +653 -1111
- package/dist/index.mjs +86 -342
- package/dist/suite.d.mts +760 -0
- package/dist/suite.d.ts +760 -0
- package/dist/suite.js +2800 -0
- package/dist/suite.mjs +2441 -0
- package/dist/ui.d.mts +1 -2
- package/dist/ui.d.ts +1 -2
- package/dist/ui.js +14 -10
- package/dist/ui.mjs +13 -11
- package/package.json +43 -24
- package/src/components/icons/icons.tsx +0 -2
- package/src/components/icons/index.ts +0 -3
- package/src/components/sections/IconShowcase.tsx +1 -110
- package/src/components/sections/NewComponentsShowcase.tsx +4 -4
- package/src/components/sections/SidebarShowcase.tsx +3 -3
- package/src/components/sections/SuiteShowcase.tsx +669 -0
- package/src/components/ui/ComponentDocs.tsx +2 -2
- package/src/components/ui/ContextMenu.tsx +1 -1
- package/src/components/ui/Menubar.tsx +1 -1
- package/src/components/ui/Sidebar.tsx +10 -8
- package/src/styles/suite-skin.css +255 -0
- package/src/suite/components/ai-panel.tsx +202 -0
- package/src/suite/components/app-switcher.tsx +196 -0
- package/src/suite/components/command-palette-host.tsx +62 -0
- package/src/suite/components/deferred-chrome.tsx +12 -0
- package/src/suite/components/suite-app-layout.tsx +69 -0
- package/src/suite/components/suite-bottom-nav.tsx +113 -0
- package/src/suite/components/suite-layout.tsx +285 -0
- package/src/suite/components/suite-mobile-drawer.tsx +150 -0
- package/src/suite/components/suite-mobile-header.tsx +71 -0
- package/src/suite/components/suite-notification-bell.tsx +227 -0
- package/src/suite/components/suite-settings-mobile-nav.tsx +83 -0
- package/src/suite/components/suite-sidebar.tsx +198 -0
- package/src/suite/components/suite-skeleton.tsx +191 -0
- package/src/suite/components/suite-user-menu.tsx +109 -0
- package/src/suite/components/theme-provider.tsx +174 -0
- package/src/suite/components/today-calibrating.tsx +95 -0
- package/src/suite/components/today-page-frame.tsx +28 -0
- package/src/suite/components/today-ui.tsx +370 -0
- package/src/suite/icons/app-accents.ts +75 -0
- package/src/suite/icons/glyph-parts.tsx +67 -0
- package/src/suite/icons/glyphs.ts +203 -0
- package/src/suite/icons/index.ts +12 -0
- package/src/suite/icons/suite-app-icon.tsx +68 -0
- package/src/suite/icons/suite-icon.tsx +93 -0
- package/src/suite/index.ts +108 -0
- package/src/suite/lib/apps.ts +75 -0
- package/src/suite/lib/cn.ts +6 -0
- package/src/suite/lib/injected.ts +54 -0
- package/src/suite/lib/motion.tsx +48 -0
- package/src/suite/lib/use-idle-mount.ts +25 -0
- package/dist/chunk-5HSOBJ4F.mjs +0 -5968
- package/src/components/icons/lemon-brand-icons.tsx +0 -16
- package/src/components/icons/lemon-category-icons.tsx +0 -72
- package/src/components/icons/lemon-icons.tsx +0 -57
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Refresh as RefreshCw } from 'iconoir-react';
|
|
4
|
+
import type { ReactNode } from 'react';
|
|
5
|
+
import { cn } from '../lib/cn';
|
|
6
|
+
import { SuiteAppIcon } from '../icons/suite-app-icon';
|
|
7
|
+
import { SuiteIcon } from '../icons/suite-icon';
|
|
8
|
+
import type { SuiteIconName } from '../icons/glyphs';
|
|
9
|
+
type LucideIcon = import('react').ComponentType<import('react').SVGProps<SVGSVGElement>>;
|
|
10
|
+
|
|
11
|
+
export type SuiteAppSlug = 'shellstack' | 'tides' | 'turtletime' | 'kraken';
|
|
12
|
+
|
|
13
|
+
const APP_META: Record<SuiteAppSlug, { name: string }> = {
|
|
14
|
+
shellstack: { name: 'ShellStack' },
|
|
15
|
+
tides: { name: 'Tides' },
|
|
16
|
+
turtletime: { name: 'TurtleTime' },
|
|
17
|
+
kraken: { name: 'Kraken' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const SUITE_APPS: SuiteAppSlug[] = [
|
|
21
|
+
'shellstack',
|
|
22
|
+
'tides',
|
|
23
|
+
'turtletime',
|
|
24
|
+
'kraken',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function sourceToSuiteApp(source: string): SuiteAppSlug {
|
|
28
|
+
if (source === 'tides' || source === 'turtletime' || source === 'kraken') {
|
|
29
|
+
return source;
|
|
30
|
+
}
|
|
31
|
+
return 'shellstack';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function suiteAppLabel(source: string): string {
|
|
35
|
+
return APP_META[sourceToSuiteApp(source)].name;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const SOURCE_APP_GLYPH: Record<SuiteAppSlug, SuiteIconName> = {
|
|
39
|
+
shellstack: 'stack-hex',
|
|
40
|
+
tides: 'kanban-wave',
|
|
41
|
+
turtletime: 'timer-shell',
|
|
42
|
+
kraken: 'squid-doc',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Small line icon for list rows — the app's suite glyph with its brand
|
|
47
|
+
* accent on a muted base. Not the branded app tile (see SuiteAppIcon).
|
|
48
|
+
*/
|
|
49
|
+
export function SourceAppGlyph({
|
|
50
|
+
source,
|
|
51
|
+
className,
|
|
52
|
+
}: {
|
|
53
|
+
source: string;
|
|
54
|
+
className?: string;
|
|
55
|
+
}) {
|
|
56
|
+
const app = sourceToSuiteApp(source);
|
|
57
|
+
return (
|
|
58
|
+
<SuiteIcon
|
|
59
|
+
name={SOURCE_APP_GLYPH[app]}
|
|
60
|
+
accent={app}
|
|
61
|
+
className={cn('h-4 w-4 shrink-0 text-ph-subtle', className)}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatLongDate(date = new Date()): string {
|
|
67
|
+
return new Intl.DateTimeFormat(undefined, {
|
|
68
|
+
month: 'long',
|
|
69
|
+
day: 'numeric',
|
|
70
|
+
}).format(date);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function SuiteAppStrip({
|
|
74
|
+
className,
|
|
75
|
+
icons,
|
|
76
|
+
}: {
|
|
77
|
+
className?: string;
|
|
78
|
+
icons?: Partial<Record<SuiteAppSlug, ReactNode>>;
|
|
79
|
+
}) {
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
className={cn(
|
|
83
|
+
'flex flex-wrap items-center justify-center gap-2.5',
|
|
84
|
+
className,
|
|
85
|
+
)}
|
|
86
|
+
aria-label="ShellStack suite apps"
|
|
87
|
+
>
|
|
88
|
+
{SUITE_APPS.map((app) => (
|
|
89
|
+
<div
|
|
90
|
+
key={app}
|
|
91
|
+
className="flex items-center gap-2 rounded-full border border-ph-border bg-ph-surface px-2.5 py-1.5 shadow-sm"
|
|
92
|
+
>
|
|
93
|
+
{icons?.[app] ?? <SuiteAppIcon app={app} size="sm" />}
|
|
94
|
+
<span className="text-xs font-medium text-ph-subtle">
|
|
95
|
+
{APP_META[app].name}
|
|
96
|
+
</span>
|
|
97
|
+
</div>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function AnimatedSun({ className }: { className?: string }) {
|
|
104
|
+
return (
|
|
105
|
+
<svg
|
|
106
|
+
width="40"
|
|
107
|
+
height="40"
|
|
108
|
+
viewBox="0 0 40 40"
|
|
109
|
+
fill="none"
|
|
110
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
111
|
+
className={cn(className)}
|
|
112
|
+
aria-hidden="true"
|
|
113
|
+
>
|
|
114
|
+
<defs>
|
|
115
|
+
<linearGradient id="animatedSunCenter" x1="0" y1="0" x2="40" y2="40">
|
|
116
|
+
<stop stopColor="#f59e0b" />
|
|
117
|
+
<stop offset="1" stopColor="#fb923c" />
|
|
118
|
+
</linearGradient>
|
|
119
|
+
</defs>
|
|
120
|
+
<g className="origin-center motion-safe:animate-[spin_20s_linear_infinite] motion-reduce:animate-none">
|
|
121
|
+
{[0, 45, 90, 135, 180, 225, 270, 315].map((angle) => (
|
|
122
|
+
<line
|
|
123
|
+
key={angle}
|
|
124
|
+
x1="20"
|
|
125
|
+
y1="4"
|
|
126
|
+
x2="20"
|
|
127
|
+
y2="9"
|
|
128
|
+
stroke="#fbbf24"
|
|
129
|
+
strokeWidth="2"
|
|
130
|
+
strokeLinecap="round"
|
|
131
|
+
transform={`rotate(${angle} 20 20)`}
|
|
132
|
+
/>
|
|
133
|
+
))}
|
|
134
|
+
</g>
|
|
135
|
+
<circle cx="20" cy="20" r="9" fill="url(#animatedSunCenter)" />
|
|
136
|
+
</svg>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function AnimatedMoon({ className }: { className?: string }) {
|
|
141
|
+
return (
|
|
142
|
+
<svg
|
|
143
|
+
width="40"
|
|
144
|
+
height="40"
|
|
145
|
+
viewBox="0 0 40 40"
|
|
146
|
+
fill="none"
|
|
147
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
148
|
+
className={cn(className)}
|
|
149
|
+
aria-hidden="true"
|
|
150
|
+
>
|
|
151
|
+
<defs>
|
|
152
|
+
<linearGradient id="animatedMoonBody" x1="0" y1="0" x2="40" y2="40">
|
|
153
|
+
<stop stopColor="#a5b4fc" />
|
|
154
|
+
<stop offset="1" stopColor="#818cf8" />
|
|
155
|
+
</linearGradient>
|
|
156
|
+
</defs>
|
|
157
|
+
<path
|
|
158
|
+
d="M27.5 25.5A11 11 0 1 1 14.5 9.6a9 9 0 1 0 13 15.9Z"
|
|
159
|
+
fill="url(#animatedMoonBody)"
|
|
160
|
+
/>
|
|
161
|
+
<g className="motion-safe:animate-[pulse_4s_ease-in-out_infinite] motion-reduce:animate-none">
|
|
162
|
+
<circle cx="29" cy="10" r="1.4" fill="#c7d2fe" />
|
|
163
|
+
<circle cx="34" cy="16" r="1" fill="#c7d2fe" />
|
|
164
|
+
<circle cx="25" cy="5.5" r="0.9" fill="#c7d2fe" />
|
|
165
|
+
</g>
|
|
166
|
+
</svg>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Sun through the day, moon from 18:00 to 06:00. */
|
|
171
|
+
export function TodayTimeIcon({ className }: { className?: string }) {
|
|
172
|
+
const hour = new Date().getHours();
|
|
173
|
+
const isNight = hour >= 18 || hour < 6;
|
|
174
|
+
return isNight ? (
|
|
175
|
+
<AnimatedMoon className={className} />
|
|
176
|
+
) : (
|
|
177
|
+
<AnimatedSun className={className} />
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
export function TodayTopBar({
|
|
181
|
+
refreshing,
|
|
182
|
+
loading,
|
|
183
|
+
onRefresh,
|
|
184
|
+
appIcon,
|
|
185
|
+
appName,
|
|
186
|
+
}: {
|
|
187
|
+
refreshing?: boolean;
|
|
188
|
+
loading?: boolean;
|
|
189
|
+
onRefresh: () => void;
|
|
190
|
+
appIcon?: ReactNode;
|
|
191
|
+
appName?: string;
|
|
192
|
+
}) {
|
|
193
|
+
const icon = appIcon ?? <TodayTimeIcon className="h-6 w-6 sm:h-7 sm:w-7 text-amber-500" />;
|
|
194
|
+
return (
|
|
195
|
+
<header className="flex items-center justify-between gap-4">
|
|
196
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
197
|
+
<div className="hidden shrink-0 sm:block" aria-hidden="true">
|
|
198
|
+
{icon}
|
|
199
|
+
</div>
|
|
200
|
+
<div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
|
201
|
+
<h1 className="font-display text-[1.625rem] font-bold leading-tight tracking-tight text-ph-ink sm:text-[2.25rem]">
|
|
202
|
+
Today
|
|
203
|
+
</h1>
|
|
204
|
+
{appName ? (
|
|
205
|
+
<span className="rounded-md bg-ph-muted px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-ph-subtle">
|
|
206
|
+
{appName}
|
|
207
|
+
</span>
|
|
208
|
+
) : null}
|
|
209
|
+
<span className="text-sm text-ph-subtle">{formatLongDate()}</span>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="flex shrink-0 items-center">
|
|
213
|
+
<button
|
|
214
|
+
type="button"
|
|
215
|
+
onClick={onRefresh}
|
|
216
|
+
disabled={refreshing || loading}
|
|
217
|
+
className="press-subtle inline-flex h-10 w-10 items-center justify-center rounded-lg text-ph-subtle transition hover:bg-ph-surface hover:text-ph-ink disabled:opacity-50"
|
|
218
|
+
data-test="today-refresh"
|
|
219
|
+
aria-label="Refresh today"
|
|
220
|
+
>
|
|
221
|
+
<RefreshCw
|
|
222
|
+
className={cn('h-4 w-4', refreshing && 'animate-spin')}
|
|
223
|
+
aria-hidden
|
|
224
|
+
/>
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
</header>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function getTodayGreeting(
|
|
232
|
+
user?: { first_name?: string; name?: string; email?: string },
|
|
233
|
+
stats?: { taskCount?: number; overdueCount?: number; hours?: number },
|
|
234
|
+
): string {
|
|
235
|
+
const hour = new Date().getHours();
|
|
236
|
+
let period = 'morning';
|
|
237
|
+
if (hour >= 12 && hour < 17) period = 'afternoon';
|
|
238
|
+
else if (hour >= 17 && hour < 21) period = 'evening';
|
|
239
|
+
else if (hour >= 21 || hour < 5) period = 'night';
|
|
240
|
+
|
|
241
|
+
const name =
|
|
242
|
+
user?.first_name ||
|
|
243
|
+
user?.name?.split(' ')[0] ||
|
|
244
|
+
user?.email?.split('@')[0] ||
|
|
245
|
+
null;
|
|
246
|
+
|
|
247
|
+
let greeting = `Good ${period}`;
|
|
248
|
+
if (name) greeting += `, ${name}`;
|
|
249
|
+
|
|
250
|
+
const taskCount = stats?.taskCount ?? 0;
|
|
251
|
+
const overdueCount = stats?.overdueCount ?? 0;
|
|
252
|
+
const hours = stats?.hours ?? 0;
|
|
253
|
+
|
|
254
|
+
if (overdueCount > 0) {
|
|
255
|
+
return `${greeting}. You have ${taskCount} task${taskCount === 1 ? '' : 's'} due today, ${overdueCount} overdue.`;
|
|
256
|
+
}
|
|
257
|
+
if (taskCount > 0) {
|
|
258
|
+
return `${greeting}. You have ${taskCount} task${taskCount === 1 ? '' : 's'} due today.`;
|
|
259
|
+
}
|
|
260
|
+
if (hours > 0) {
|
|
261
|
+
return `${greeting}. You logged ${hours} hour${hours === 1 ? '' : 's'} today.`;
|
|
262
|
+
}
|
|
263
|
+
if (period === 'evening' || period === 'night') {
|
|
264
|
+
return `${greeting}. Time to wrap up.`;
|
|
265
|
+
}
|
|
266
|
+
return `${greeting}. Nothing overdue — take a nice, deep breath.`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function TodayHero({
|
|
270
|
+
message = 'Take a nice, deep breath.',
|
|
271
|
+
}: {
|
|
272
|
+
message?: ReactNode;
|
|
273
|
+
}) {
|
|
274
|
+
return (
|
|
275
|
+
<div className="mt-8 text-center sm:mt-10">
|
|
276
|
+
<p
|
|
277
|
+
key={typeof message === 'string' ? message : 'hero'}
|
|
278
|
+
className="today-hero font-display text-2xl font-bold tracking-tight text-ph-ink sm:text-[1.75rem] sm:leading-tight"
|
|
279
|
+
>
|
|
280
|
+
{message}
|
|
281
|
+
</p>
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function TodaySection({
|
|
287
|
+
icon: Icon,
|
|
288
|
+
iconClassName,
|
|
289
|
+
title,
|
|
290
|
+
description,
|
|
291
|
+
action,
|
|
292
|
+
testId,
|
|
293
|
+
children,
|
|
294
|
+
}: {
|
|
295
|
+
icon: LucideIcon;
|
|
296
|
+
iconClassName?: string;
|
|
297
|
+
title: string;
|
|
298
|
+
description?: string;
|
|
299
|
+
action?: ReactNode;
|
|
300
|
+
testId?: string;
|
|
301
|
+
children: ReactNode;
|
|
302
|
+
}) {
|
|
303
|
+
return (
|
|
304
|
+
<section data-test={testId} className="space-y-4">
|
|
305
|
+
<div className="flex items-start justify-between gap-4">
|
|
306
|
+
<div className="flex min-w-0 items-start gap-3.5">
|
|
307
|
+
<span
|
|
308
|
+
className={cn(
|
|
309
|
+
'inline-flex h-10 w-10 sm:h-11 sm:w-11 shrink-0 items-center justify-center rounded-2xl shadow-sm ring-1 ring-black/[0.06]',
|
|
310
|
+
iconClassName ?? 'bg-ph-muted text-ph-ink',
|
|
311
|
+
)}
|
|
312
|
+
>
|
|
313
|
+
<Icon className="h-5 w-5" aria-hidden />
|
|
314
|
+
</span>
|
|
315
|
+
<div className="min-w-0 pt-0.5">
|
|
316
|
+
<h2 className="font-display text-lg font-bold tracking-tight text-ph-ink">
|
|
317
|
+
{title}
|
|
318
|
+
</h2>
|
|
319
|
+
{description ? (
|
|
320
|
+
<p className="mt-1 max-w-xl text-sm leading-relaxed text-ph-subtle">
|
|
321
|
+
{description}
|
|
322
|
+
</p>
|
|
323
|
+
) : null}
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
{action}
|
|
327
|
+
</div>
|
|
328
|
+
{children}
|
|
329
|
+
</section>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function TodayPanel({
|
|
334
|
+
children,
|
|
335
|
+
className,
|
|
336
|
+
}: {
|
|
337
|
+
children: ReactNode;
|
|
338
|
+
className?: string;
|
|
339
|
+
}) {
|
|
340
|
+
return (
|
|
341
|
+
<div
|
|
342
|
+
className={cn(
|
|
343
|
+
'border-ph-border/70 rounded-2xl border bg-ph-surface p-6 shadow-[0_2px_28px_-10px_rgba(15,23,42,0.14)] transition-all duration-200 ease-out sm:p-6',
|
|
344
|
+
'hover:shadow-[0_8px_32px_-12px_rgba(15,23,42,0.18)] hover:-translate-y-0.5 motion-reduce:transition-none motion-reduce:hover:transform-none',
|
|
345
|
+
className,
|
|
346
|
+
)}
|
|
347
|
+
>
|
|
348
|
+
{children}
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function TodayEmptyState({
|
|
354
|
+
title,
|
|
355
|
+
description,
|
|
356
|
+
}: {
|
|
357
|
+
title: string;
|
|
358
|
+
description?: string;
|
|
359
|
+
}) {
|
|
360
|
+
return (
|
|
361
|
+
<div className="border-ph-border/80 rounded-xl border border-dashed px-5 py-8 text-center">
|
|
362
|
+
<p className="text-sm font-semibold text-ph-ink">{title}</p>
|
|
363
|
+
{description ? (
|
|
364
|
+
<p className="mt-1.5 text-sm leading-relaxed text-ph-subtle">
|
|
365
|
+
{description}
|
|
366
|
+
</p>
|
|
367
|
+
) : null}
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { SuiteIconName } from './glyphs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Per-app brand colours for the suite icon system — the single place hexes
|
|
5
|
+
* live. `accent` tints a glyph's accent layer; `tile`/`onTile`/
|
|
6
|
+
* `onTileAccent` drive `<SuiteAppIcon>` (the programmatic favicon-style
|
|
7
|
+
* tile + glyph mark).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type SuiteAccentSlug =
|
|
11
|
+
| 'turtletime'
|
|
12
|
+
| 'tides'
|
|
13
|
+
| 'kraken'
|
|
14
|
+
| 'shellstack'
|
|
15
|
+
| 'crew';
|
|
16
|
+
|
|
17
|
+
export interface SuiteAppAccent {
|
|
18
|
+
/** Display name (default aria label for app marks). */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Brand accent — tints glyph accent layers via `--suite-icon-accent`. */
|
|
21
|
+
accent: string;
|
|
22
|
+
/** App tile background. */
|
|
23
|
+
tile: string;
|
|
24
|
+
/** Base glyph colour on the tile. */
|
|
25
|
+
onTile: string;
|
|
26
|
+
/** Accent-layer glyph colour on the tile. */
|
|
27
|
+
onTileAccent: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const APP_ACCENTS: Record<SuiteAccentSlug, SuiteAppAccent> = {
|
|
31
|
+
turtletime: {
|
|
32
|
+
label: 'TurtleTime',
|
|
33
|
+
accent: '#3f8f57',
|
|
34
|
+
tile: '#173521',
|
|
35
|
+
onTile: '#f7ead0',
|
|
36
|
+
onTileAccent: '#f3c24e',
|
|
37
|
+
},
|
|
38
|
+
tides: {
|
|
39
|
+
label: 'Tides',
|
|
40
|
+
accent: '#a63bb5',
|
|
41
|
+
tile: '#8f2d96',
|
|
42
|
+
onTile: '#ffffff',
|
|
43
|
+
onTileAccent: '#bfe3ff',
|
|
44
|
+
},
|
|
45
|
+
kraken: {
|
|
46
|
+
label: 'Kraken',
|
|
47
|
+
accent: '#737373',
|
|
48
|
+
tile: '#191919',
|
|
49
|
+
onTile: '#ffffff',
|
|
50
|
+
onTileAccent: '#d4d4d4',
|
|
51
|
+
},
|
|
52
|
+
shellstack: {
|
|
53
|
+
label: 'ShellStack',
|
|
54
|
+
accent: '#5e5893',
|
|
55
|
+
tile: '#5e5893',
|
|
56
|
+
onTile: '#ffffff',
|
|
57
|
+
onTileAccent: '#c4a7e7',
|
|
58
|
+
},
|
|
59
|
+
crew: {
|
|
60
|
+
label: 'Crew',
|
|
61
|
+
accent: '#0891b2',
|
|
62
|
+
tile: '#155e75',
|
|
63
|
+
onTile: '#ffffff',
|
|
64
|
+
onTileAccent: '#a5f3fc',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Which glyph represents each app. */
|
|
69
|
+
export const APP_GLYPHS: Record<SuiteAccentSlug, SuiteIconName> = {
|
|
70
|
+
turtletime: 'timer-shell',
|
|
71
|
+
tides: 'kanban-wave',
|
|
72
|
+
kraken: 'squid-doc',
|
|
73
|
+
shellstack: 'stack-hex',
|
|
74
|
+
crew: 'crew-bot',
|
|
75
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { SuiteGlyphElement } from './glyphs';
|
|
4
|
+
|
|
5
|
+
function GlyphElement({ element }: { element: SuiteGlyphElement }) {
|
|
6
|
+
const fill = element.filled ? 'currentColor' : 'none';
|
|
7
|
+
const stroke = element.filled ? 'none' : undefined;
|
|
8
|
+
switch (element.kind) {
|
|
9
|
+
case 'path':
|
|
10
|
+
return <path d={element.d} fill={fill} stroke={stroke} />;
|
|
11
|
+
case 'circle':
|
|
12
|
+
return (
|
|
13
|
+
<circle
|
|
14
|
+
cx={element.cx}
|
|
15
|
+
cy={element.cy}
|
|
16
|
+
r={element.r}
|
|
17
|
+
fill={fill}
|
|
18
|
+
stroke={stroke}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
case 'rect':
|
|
22
|
+
return (
|
|
23
|
+
<rect
|
|
24
|
+
x={element.x}
|
|
25
|
+
y={element.y}
|
|
26
|
+
width={element.width}
|
|
27
|
+
height={element.height}
|
|
28
|
+
rx={element.rx}
|
|
29
|
+
fill={fill}
|
|
30
|
+
stroke={stroke}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Renders a glyph's elements. Accent-flagged elements are wrapped in a group
|
|
38
|
+
* coloured by `var(--suite-icon-accent, currentColor)` — callers set the
|
|
39
|
+
* variable on an ancestor (inline style) to tint the accent layer.
|
|
40
|
+
*/
|
|
41
|
+
export function GlyphParts({
|
|
42
|
+
elements,
|
|
43
|
+
accentClassName,
|
|
44
|
+
}: {
|
|
45
|
+
elements: SuiteGlyphElement[];
|
|
46
|
+
accentClassName?: string;
|
|
47
|
+
}) {
|
|
48
|
+
const base = elements.filter((el) => !el.accent);
|
|
49
|
+
const accented = elements.filter((el) => el.accent);
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
{base.map((el, i) => (
|
|
53
|
+
<GlyphElement key={i} element={el} />
|
|
54
|
+
))}
|
|
55
|
+
{accented.length > 0 ? (
|
|
56
|
+
<g
|
|
57
|
+
className={accentClassName}
|
|
58
|
+
style={{ color: 'var(--suite-icon-accent, currentColor)' }}
|
|
59
|
+
>
|
|
60
|
+
{accented.map((el, i) => (
|
|
61
|
+
<GlyphElement key={i} element={el} />
|
|
62
|
+
))}
|
|
63
|
+
</g>
|
|
64
|
+
) : null}
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
}
|