@slugbase/ui 0.0.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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-lint.log +4 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/dist/components/AppShell.d.ts +17 -0
- package/dist/components/AppShell.d.ts.map +1 -0
- package/dist/components/AppShell.js +11 -0
- package/dist/components/AppShell.js.map +1 -0
- package/dist/components/Badge.d.ts +13 -0
- package/dist/components/Badge.d.ts.map +1 -0
- package/dist/components/Badge.js +21 -0
- package/dist/components/Badge.js.map +1 -0
- package/dist/components/Button.d.ts +11 -0
- package/dist/components/Button.d.ts.map +1 -0
- package/dist/components/Button.js +21 -0
- package/dist/components/Button.js.map +1 -0
- package/dist/components/Checkbox.d.ts +16 -0
- package/dist/components/Checkbox.d.ts.map +1 -0
- package/dist/components/Checkbox.js +19 -0
- package/dist/components/Checkbox.js.map +1 -0
- package/dist/components/ColorPicker.d.ts +14 -0
- package/dist/components/ColorPicker.d.ts.map +1 -0
- package/dist/components/ColorPicker.js +29 -0
- package/dist/components/ColorPicker.js.map +1 -0
- package/dist/components/ConfirmDialog.d.ts +17 -0
- package/dist/components/ConfirmDialog.d.ts.map +1 -0
- package/dist/components/ConfirmDialog.js +8 -0
- package/dist/components/ConfirmDialog.js.map +1 -0
- package/dist/components/Dialog.d.ts +15 -0
- package/dist/components/Dialog.d.ts.map +1 -0
- package/dist/components/Dialog.js +12 -0
- package/dist/components/Dialog.js.map +1 -0
- package/dist/components/EmptyState.d.ts +12 -0
- package/dist/components/EmptyState.d.ts.map +1 -0
- package/dist/components/EmptyState.js +6 -0
- package/dist/components/EmptyState.js.map +1 -0
- package/dist/components/FieldError.d.ts +7 -0
- package/dist/components/FieldError.d.ts.map +1 -0
- package/dist/components/FieldError.js +5 -0
- package/dist/components/FieldError.js.map +1 -0
- package/dist/components/FolderGlyph.d.ts +8 -0
- package/dist/components/FolderGlyph.d.ts.map +1 -0
- package/dist/components/FolderGlyph.js +14 -0
- package/dist/components/FolderGlyph.js.map +1 -0
- package/dist/components/IconButton.d.ts +14 -0
- package/dist/components/IconButton.d.ts.map +1 -0
- package/dist/components/IconButton.js +15 -0
- package/dist/components/IconButton.js.map +1 -0
- package/dist/components/IconPicker.d.ts +16 -0
- package/dist/components/IconPicker.d.ts.map +1 -0
- package/dist/components/IconPicker.js +32 -0
- package/dist/components/IconPicker.js.map +1 -0
- package/dist/components/Input.d.ts +10 -0
- package/dist/components/Input.d.ts.map +1 -0
- package/dist/components/Input.js +33 -0
- package/dist/components/Input.js.map +1 -0
- package/dist/components/Kbd.d.ts +11 -0
- package/dist/components/Kbd.d.ts.map +1 -0
- package/dist/components/Kbd.js +18 -0
- package/dist/components/Kbd.js.map +1 -0
- package/dist/components/Label.d.ts +6 -0
- package/dist/components/Label.d.ts.map +1 -0
- package/dist/components/Label.js +5 -0
- package/dist/components/Label.js.map +1 -0
- package/dist/components/Popover.d.ts +14 -0
- package/dist/components/Popover.d.ts.map +1 -0
- package/dist/components/Popover.js +9 -0
- package/dist/components/Popover.js.map +1 -0
- package/dist/components/Skeleton.d.ts +22 -0
- package/dist/components/Skeleton.d.ts.map +1 -0
- package/dist/components/Skeleton.js +14 -0
- package/dist/components/Skeleton.js.map +1 -0
- package/dist/components/SlugBadge.d.ts +13 -0
- package/dist/components/SlugBadge.d.ts.map +1 -0
- package/dist/components/SlugBadge.js +17 -0
- package/dist/components/SlugBadge.js.map +1 -0
- package/dist/components/Tag.d.ts +11 -0
- package/dist/components/Tag.d.ts.map +1 -0
- package/dist/components/Tag.js +16 -0
- package/dist/components/Tag.js.map +1 -0
- package/dist/components/ThemeSwitcher.d.ts +11 -0
- package/dist/components/ThemeSwitcher.d.ts.map +1 -0
- package/dist/components/ThemeSwitcher.js +25 -0
- package/dist/components/ThemeSwitcher.js.map +1 -0
- package/dist/components/ToastProvider.d.ts +19 -0
- package/dist/components/ToastProvider.d.ts.map +1 -0
- package/dist/components/ToastProvider.js +62 -0
- package/dist/components/ToastProvider.js.map +1 -0
- package/dist/components/Tooltip.d.ts +13 -0
- package/dist/components/Tooltip.d.ts.map +1 -0
- package/dist/components/Tooltip.js +9 -0
- package/dist/components/Tooltip.js.map +1 -0
- package/dist/components/auth-kit.d.ts +59 -0
- package/dist/components/auth-kit.d.ts.map +1 -0
- package/dist/components/auth-kit.js +101 -0
- package/dist/components/auth-kit.js.map +1 -0
- package/dist/constants.d.ts +2 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -0
- package/dist/icons/folder-icon-names.d.ts +4 -0
- package/dist/icons/folder-icon-names.d.ts.map +1 -0
- package/dist/icons/folder-icon-names.js +164 -0
- package/dist/icons/folder-icon-names.js.map +1 -0
- package/dist/icons/lucide-icon.d.ts +8 -0
- package/dist/icons/lucide-icon.d.ts.map +1 -0
- package/dist/icons/lucide-icon.js +38 -0
- package/dist/icons/lucide-icon.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +31 -0
- package/dist/index.js.map +1 -0
- package/dist/tailwind/token-theme.d.ts +167 -0
- package/dist/tailwind/token-theme.d.ts.map +1 -0
- package/dist/tailwind/token-theme.js +109 -0
- package/dist/tailwind/token-theme.js.map +1 -0
- package/dist/theme/resolve-theme.d.ts +6 -0
- package/dist/theme/resolve-theme.d.ts.map +1 -0
- package/dist/theme/resolve-theme.js +24 -0
- package/dist/theme/resolve-theme.js.map +1 -0
- package/dist/theme/theme-context.d.ts +15 -0
- package/dist/theme/theme-context.d.ts.map +1 -0
- package/dist/theme/theme-context.js +51 -0
- package/dist/theme/theme-context.js.map +1 -0
- package/dist/theme/theme-types.d.ts +5 -0
- package/dist/theme/theme-types.d.ts.map +1 -0
- package/dist/theme/theme-types.js +3 -0
- package/dist/theme/theme-types.js.map +1 -0
- package/package.json +55 -0
- package/postcss.config.js +6 -0
- package/src/components/AppShell.spec.tsx +28 -0
- package/src/components/AppShell.tsx +49 -0
- package/src/components/Badge.tsx +41 -0
- package/src/components/Button.tsx +56 -0
- package/src/components/Checkbox.tsx +70 -0
- package/src/components/ColorPicker.spec.tsx +53 -0
- package/src/components/ColorPicker.tsx +87 -0
- package/src/components/ConfirmDialog.spec.tsx +44 -0
- package/src/components/ConfirmDialog.tsx +67 -0
- package/src/components/Dialog.tsx +63 -0
- package/src/components/EmptyState.spec.tsx +22 -0
- package/src/components/EmptyState.tsx +54 -0
- package/src/components/FieldError.tsx +19 -0
- package/src/components/FolderGlyph.tsx +38 -0
- package/src/components/IconButton.tsx +42 -0
- package/src/components/IconPicker.spec.tsx +54 -0
- package/src/components/IconPicker.tsx +132 -0
- package/src/components/Input.tsx +76 -0
- package/src/components/Kbd.tsx +30 -0
- package/src/components/Label.tsx +17 -0
- package/src/components/Popover.tsx +35 -0
- package/src/components/Skeleton.spec.tsx +18 -0
- package/src/components/Skeleton.tsx +94 -0
- package/src/components/SlugBadge.tsx +35 -0
- package/src/components/Tag.tsx +31 -0
- package/src/components/ThemeSwitcher.tsx +60 -0
- package/src/components/ToastProvider.spec.tsx +60 -0
- package/src/components/ToastProvider.tsx +167 -0
- package/src/components/Tooltip.tsx +33 -0
- package/src/components/__snapshots__/AppShell.spec.tsx.snap +41 -0
- package/src/components/auth-kit.spec.tsx +24 -0
- package/src/components/auth-kit.tsx +475 -0
- package/src/constants.ts +1 -0
- package/src/icons/folder-icon-names.ts +165 -0
- package/src/icons/lucide-icon.spec.ts +32 -0
- package/src/icons/lucide-icon.tsx +47 -0
- package/src/index.ts +100 -0
- package/src/styles/app.css +5 -0
- package/src/styles/colors-and-type.css +140 -0
- package/src/styles/ibm-plex-fonts.css +8 -0
- package/src/tailwind/token-theme.spec.ts +13 -0
- package/src/tailwind/token-theme.ts +114 -0
- package/src/theme/resolve-theme.spec.ts +17 -0
- package/src/theme/resolve-theme.ts +30 -0
- package/src/theme/theme-context.tsx +99 -0
- package/src/theme/theme-types.ts +7 -0
- package/tailwind.config.ts +11 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +11 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +8 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/* ── Brand rail labels (caller supplies translated strings) ── */
|
|
4
|
+
|
|
5
|
+
export type AuthShellBrandLabels = {
|
|
6
|
+
headline: string;
|
|
7
|
+
subline: string;
|
|
8
|
+
footer: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type AuthShellProps = {
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
brand: AuthShellBrandLabels;
|
|
14
|
+
/** Setup wizard omits slug rows in the brand rail (spec §8.3) */
|
|
15
|
+
showSlugRows?: boolean;
|
|
16
|
+
/** Brand icon URL (web serves `/slugbase_icon.svg`) */
|
|
17
|
+
iconSrc?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Shared two-pane auth layout (prototype AuthKit.jsx / auth.css).
|
|
22
|
+
* Left pane: brand rail (gradient wash, hero, optional slug rows).
|
|
23
|
+
* Right pane: form card at min(404px, 100%).
|
|
24
|
+
* Grid: minmax(440px, 0.9fr) 1fr; brand hidden below 920px.
|
|
25
|
+
*/
|
|
26
|
+
export function AuthShell({
|
|
27
|
+
children,
|
|
28
|
+
brand,
|
|
29
|
+
showSlugRows = true,
|
|
30
|
+
iconSrc = "/slugbase_icon.svg",
|
|
31
|
+
}: AuthShellProps) {
|
|
32
|
+
return (
|
|
33
|
+
<div
|
|
34
|
+
className="min-h-screen bg-canvas font-sans grid grid-cols-1 min-[920px]:grid-cols-[minmax(440px,0.9fr)_1fr]"
|
|
35
|
+
data-testid="auth-shell"
|
|
36
|
+
>
|
|
37
|
+
<aside
|
|
38
|
+
className="relative overflow-hidden hidden min-[920px]:flex flex-col justify-between border-r border-[color:var(--border-subtle)] bg-base"
|
|
39
|
+
style={{ padding: "var(--sp-11) var(--sp-11) var(--sp-10)", gap: "var(--sp-8)" }}
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
data-testid="auth-shell-brand-rail"
|
|
42
|
+
>
|
|
43
|
+
<div
|
|
44
|
+
className="absolute pointer-events-none"
|
|
45
|
+
style={{
|
|
46
|
+
top: "-180px",
|
|
47
|
+
left: "-140px",
|
|
48
|
+
width: "460px",
|
|
49
|
+
height: "460px",
|
|
50
|
+
background: "radial-gradient(circle, var(--accent-subtle) 0%, transparent 68%)",
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
|
|
54
|
+
<div className="relative flex items-center" style={{ gap: "var(--sp-4)" }}>
|
|
55
|
+
<img src={iconSrc} alt="" width={30} height={30} className="shrink-0" />
|
|
56
|
+
<span
|
|
57
|
+
className="text-fg"
|
|
58
|
+
style={{
|
|
59
|
+
fontWeight: "var(--weight-semi)",
|
|
60
|
+
fontSize: "var(--text-h2)",
|
|
61
|
+
lineHeight: "var(--lh-h2)",
|
|
62
|
+
letterSpacing: "var(--track-tight)",
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
SlugBase
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div
|
|
70
|
+
className="relative flex flex-col flex-1 justify-center"
|
|
71
|
+
style={{ gap: "var(--sp-8)" }}
|
|
72
|
+
>
|
|
73
|
+
<div style={{ maxWidth: "30ch" }}>
|
|
74
|
+
<h1
|
|
75
|
+
className="text-fg"
|
|
76
|
+
style={{
|
|
77
|
+
margin: 0,
|
|
78
|
+
marginBottom: "var(--sp-5)",
|
|
79
|
+
fontWeight: "var(--weight-semi)",
|
|
80
|
+
fontSize: "var(--text-display)",
|
|
81
|
+
lineHeight: "var(--lh-display)",
|
|
82
|
+
letterSpacing: "var(--track-tight)",
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
85
|
+
{brand.headline}
|
|
86
|
+
</h1>
|
|
87
|
+
<p
|
|
88
|
+
className="text-fg-muted"
|
|
89
|
+
style={{ margin: 0, fontSize: "var(--text-body-lg)", lineHeight: "var(--lh-body-lg)" }}
|
|
90
|
+
>
|
|
91
|
+
{brand.subline}
|
|
92
|
+
</p>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{showSlugRows && (
|
|
96
|
+
<div className="relative flex flex-col" style={{ gap: "var(--sp-3)" }}>
|
|
97
|
+
<BrandSlugRow src="cloud.slugbase.app/react19" dst="react.dev/blog" opacity={1} />
|
|
98
|
+
<BrandSlugRow src="cloud.slugbase.app/ddia" dst="dataintensive.net" opacity={0.55} />
|
|
99
|
+
<BrandSlugRow
|
|
100
|
+
src="cloud.slugbase.app/rustperf"
|
|
101
|
+
dst="nnethercote.github.io"
|
|
102
|
+
opacity={0.28}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<p
|
|
109
|
+
className="relative text-fg-subtle"
|
|
110
|
+
style={{ fontSize: "var(--text-small)", lineHeight: "var(--lh-small)" }}
|
|
111
|
+
>
|
|
112
|
+
{brand.footer}
|
|
113
|
+
</p>
|
|
114
|
+
</aside>
|
|
115
|
+
|
|
116
|
+
<div
|
|
117
|
+
className="grid place-items-center"
|
|
118
|
+
style={{ padding: "var(--sp-9) var(--sp-8)" }}
|
|
119
|
+
data-testid="auth-shell-form-pane"
|
|
120
|
+
>
|
|
121
|
+
<div style={{ width: "min(404px, 100%)" }}>
|
|
122
|
+
<div
|
|
123
|
+
className="flex min-[920px]:hidden items-center"
|
|
124
|
+
style={{ gap: "var(--sp-4)", marginBottom: "var(--sp-8)" }}
|
|
125
|
+
>
|
|
126
|
+
<img src={iconSrc} alt="" width={26} height={26} className="shrink-0" />
|
|
127
|
+
<span
|
|
128
|
+
className="text-fg"
|
|
129
|
+
style={{ fontWeight: "var(--weight-semi)", fontSize: "var(--text-h3)" }}
|
|
130
|
+
>
|
|
131
|
+
SlugBase
|
|
132
|
+
</span>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{children}
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function BrandSlugRow({
|
|
143
|
+
src,
|
|
144
|
+
dst,
|
|
145
|
+
opacity,
|
|
146
|
+
}: {
|
|
147
|
+
src: string;
|
|
148
|
+
dst: string;
|
|
149
|
+
opacity: number;
|
|
150
|
+
}) {
|
|
151
|
+
return (
|
|
152
|
+
<div
|
|
153
|
+
className="flex items-center border border-[color:var(--border-subtle)] bg-raised"
|
|
154
|
+
style={{
|
|
155
|
+
opacity,
|
|
156
|
+
padding: "var(--sp-4) var(--sp-5)",
|
|
157
|
+
borderRadius: "var(--r-lg)",
|
|
158
|
+
gap: "var(--sp-4)",
|
|
159
|
+
fontFamily: "var(--font-mono)",
|
|
160
|
+
fontWeight: "var(--weight-medium)",
|
|
161
|
+
fontSize: "var(--text-mono)",
|
|
162
|
+
lineHeight: 1,
|
|
163
|
+
}}
|
|
164
|
+
>
|
|
165
|
+
<span className="text-accent-text shrink-0">{src}</span>
|
|
166
|
+
<span className="text-fg-faint shrink-0">
|
|
167
|
+
<ArrowRightIcon />
|
|
168
|
+
</span>
|
|
169
|
+
<span className="text-fg-subtle overflow-hidden text-ellipsis whitespace-nowrap">{dst}</span>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function ArrowRightIcon() {
|
|
175
|
+
return (
|
|
176
|
+
<svg
|
|
177
|
+
width="13"
|
|
178
|
+
height="13"
|
|
179
|
+
viewBox="0 0 24 24"
|
|
180
|
+
fill="none"
|
|
181
|
+
stroke="currentColor"
|
|
182
|
+
strokeWidth="2"
|
|
183
|
+
strokeLinecap="round"
|
|
184
|
+
strokeLinejoin="round"
|
|
185
|
+
aria-hidden="true"
|
|
186
|
+
>
|
|
187
|
+
<path d="M5 12h14" />
|
|
188
|
+
<path d="m12 5 7 7-7 7" />
|
|
189
|
+
</svg>
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function AuthInput({
|
|
194
|
+
leadingIcon,
|
|
195
|
+
trailingSlot,
|
|
196
|
+
className,
|
|
197
|
+
style,
|
|
198
|
+
...inputProps
|
|
199
|
+
}: React.InputHTMLAttributes<HTMLInputElement> & {
|
|
200
|
+
leadingIcon?: ReactNode;
|
|
201
|
+
trailingSlot?: ReactNode;
|
|
202
|
+
}) {
|
|
203
|
+
const paddingLeft = leadingIcon ? "calc(var(--sp-4) + 16px + var(--sp-3))" : "var(--sp-5)";
|
|
204
|
+
const paddingRight = trailingSlot ? "2.5rem" : "var(--sp-5)";
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<div className="relative">
|
|
208
|
+
{leadingIcon && (
|
|
209
|
+
<span
|
|
210
|
+
className="absolute inset-y-0 left-0 flex items-center text-fg-subtle pointer-events-none"
|
|
211
|
+
style={{ paddingLeft: "var(--sp-4)" }}
|
|
212
|
+
>
|
|
213
|
+
{leadingIcon}
|
|
214
|
+
</span>
|
|
215
|
+
)}
|
|
216
|
+
<input
|
|
217
|
+
{...inputProps}
|
|
218
|
+
className={`w-full rounded-md border border-[color:var(--border)] bg-raised text-fg placeholder:text-fg-faint focus:outline-none focus:ring-2 focus:ring-accent focus:ring-offset-2 focus:ring-offset-canvas disabled:cursor-not-allowed disabled:opacity-50 ${className ?? ""}`}
|
|
219
|
+
style={{
|
|
220
|
+
height: "42px",
|
|
221
|
+
paddingLeft,
|
|
222
|
+
paddingRight,
|
|
223
|
+
fontSize: "var(--text-body-lg)",
|
|
224
|
+
lineHeight: "var(--lh-body-lg)",
|
|
225
|
+
...style,
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
{trailingSlot && (
|
|
229
|
+
<div className="absolute inset-y-0 right-0 flex items-center pr-sp-4">
|
|
230
|
+
{trailingSlot}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface OidcProviderItem {
|
|
238
|
+
id: string;
|
|
239
|
+
name: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export type SsoSectionProps = {
|
|
243
|
+
providers: OidcProviderItem[];
|
|
244
|
+
mode: "login" | "register";
|
|
245
|
+
t: (key: string, params?: Record<string, string>) => string;
|
|
246
|
+
/** Base URL for OIDC authorize links (e.g. API origin) */
|
|
247
|
+
apiBaseUrl?: string;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
export function SsoSection({ providers, mode, t, apiBaseUrl = "" }: SsoSectionProps) {
|
|
251
|
+
if (providers.length === 0) return null;
|
|
252
|
+
|
|
253
|
+
const labelKey = mode === "login" ? "auth.sso.sign_in_with" : "auth.sso.sign_up_with";
|
|
254
|
+
|
|
255
|
+
return (
|
|
256
|
+
<>
|
|
257
|
+
<div className="flex items-center" style={{ gap: "var(--sp-5)", margin: "var(--sp-2) 0" }}>
|
|
258
|
+
<span className="flex-1 h-px bg-[color:var(--border-subtle)]" aria-hidden="true" />
|
|
259
|
+
<span className="text-fg-subtle lowercase" style={{ fontSize: "var(--text-small)" }}>
|
|
260
|
+
{t("auth.sso.divider")}
|
|
261
|
+
</span>
|
|
262
|
+
<span className="flex-1 h-px bg-[color:var(--border-subtle)]" aria-hidden="true" />
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div className="flex flex-col" style={{ gap: "var(--sp-4)" }}>
|
|
266
|
+
{providers.map((p) => (
|
|
267
|
+
<a
|
|
268
|
+
key={p.id}
|
|
269
|
+
href={`${apiBaseUrl}/auth/oidc/${p.id}/authorize`}
|
|
270
|
+
className="flex items-center justify-center border border-[color:var(--border)] bg-raised text-fg font-medium transition-colors hover:bg-raised-2 hover:border-[color:var(--border-strong)]"
|
|
271
|
+
style={{
|
|
272
|
+
height: "44px",
|
|
273
|
+
borderRadius: "var(--r-md)",
|
|
274
|
+
gap: "var(--sp-4)",
|
|
275
|
+
fontSize: "var(--text-body-lg)",
|
|
276
|
+
lineHeight: 1,
|
|
277
|
+
}}
|
|
278
|
+
>
|
|
279
|
+
<ExternalProviderIcon />
|
|
280
|
+
{t(labelKey, { provider: p.name })}
|
|
281
|
+
</a>
|
|
282
|
+
))}
|
|
283
|
+
</div>
|
|
284
|
+
</>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function ExternalProviderIcon() {
|
|
289
|
+
return (
|
|
290
|
+
<svg
|
|
291
|
+
width="18"
|
|
292
|
+
height="18"
|
|
293
|
+
viewBox="0 0 24 24"
|
|
294
|
+
fill="none"
|
|
295
|
+
stroke="currentColor"
|
|
296
|
+
strokeWidth="1.5"
|
|
297
|
+
strokeLinecap="round"
|
|
298
|
+
strokeLinejoin="round"
|
|
299
|
+
aria-hidden="true"
|
|
300
|
+
>
|
|
301
|
+
<circle cx="12" cy="12" r="10" />
|
|
302
|
+
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
|
303
|
+
<path d="M2 12h20" />
|
|
304
|
+
</svg>
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function MailFieldIcon() {
|
|
309
|
+
return (
|
|
310
|
+
<svg
|
|
311
|
+
width="15"
|
|
312
|
+
height="15"
|
|
313
|
+
viewBox="0 0 24 24"
|
|
314
|
+
fill="none"
|
|
315
|
+
stroke="currentColor"
|
|
316
|
+
strokeWidth="2"
|
|
317
|
+
strokeLinecap="round"
|
|
318
|
+
strokeLinejoin="round"
|
|
319
|
+
aria-hidden="true"
|
|
320
|
+
>
|
|
321
|
+
<rect width="20" height="16" x="2" y="4" rx="2" />
|
|
322
|
+
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" />
|
|
323
|
+
</svg>
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function LockFieldIcon() {
|
|
328
|
+
return (
|
|
329
|
+
<svg
|
|
330
|
+
width="15"
|
|
331
|
+
height="15"
|
|
332
|
+
viewBox="0 0 24 24"
|
|
333
|
+
fill="none"
|
|
334
|
+
stroke="currentColor"
|
|
335
|
+
strokeWidth="2"
|
|
336
|
+
strokeLinecap="round"
|
|
337
|
+
strokeLinejoin="round"
|
|
338
|
+
aria-hidden="true"
|
|
339
|
+
>
|
|
340
|
+
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
|
|
341
|
+
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
342
|
+
</svg>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function UserFieldIcon() {
|
|
347
|
+
return (
|
|
348
|
+
<svg
|
|
349
|
+
width="15"
|
|
350
|
+
height="15"
|
|
351
|
+
viewBox="0 0 24 24"
|
|
352
|
+
fill="none"
|
|
353
|
+
stroke="currentColor"
|
|
354
|
+
strokeWidth="2"
|
|
355
|
+
strokeLinecap="round"
|
|
356
|
+
strokeLinejoin="round"
|
|
357
|
+
aria-hidden="true"
|
|
358
|
+
>
|
|
359
|
+
<circle cx="12" cy="8" r="5" />
|
|
360
|
+
<path d="M20 21a8 8 0 0 0-16 0" />
|
|
361
|
+
</svg>
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function KeyFieldIcon() {
|
|
366
|
+
return (
|
|
367
|
+
<svg
|
|
368
|
+
width="15"
|
|
369
|
+
height="15"
|
|
370
|
+
viewBox="0 0 24 24"
|
|
371
|
+
fill="none"
|
|
372
|
+
stroke="currentColor"
|
|
373
|
+
strokeWidth="2"
|
|
374
|
+
strokeLinecap="round"
|
|
375
|
+
strokeLinejoin="round"
|
|
376
|
+
aria-hidden="true"
|
|
377
|
+
>
|
|
378
|
+
<circle cx="7.5" cy="15.5" r="5.5" />
|
|
379
|
+
<path d="m21 2-9.6 9.6" />
|
|
380
|
+
<path d="m15.5 7.5 3 3L22 7l-3-3" />
|
|
381
|
+
</svg>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function AuthButton({
|
|
386
|
+
children,
|
|
387
|
+
isSubmitting,
|
|
388
|
+
disabled,
|
|
389
|
+
className,
|
|
390
|
+
style,
|
|
391
|
+
}: {
|
|
392
|
+
children: React.ReactNode;
|
|
393
|
+
isSubmitting?: boolean;
|
|
394
|
+
disabled?: boolean;
|
|
395
|
+
className?: string;
|
|
396
|
+
style?: React.CSSProperties;
|
|
397
|
+
}) {
|
|
398
|
+
return (
|
|
399
|
+
<button
|
|
400
|
+
type="submit"
|
|
401
|
+
disabled={isSubmitting || disabled}
|
|
402
|
+
className={`w-full rounded-md bg-accent font-medium text-accent-fg transition-colors hover:bg-accent-hover active:bg-accent-active disabled:cursor-not-allowed disabled:opacity-50 ${className ?? ""}`}
|
|
403
|
+
style={{ height: "44px", fontSize: "var(--text-body-lg)", lineHeight: 1, ...style }}
|
|
404
|
+
>
|
|
405
|
+
{children}
|
|
406
|
+
</button>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function AuthHeading({
|
|
411
|
+
title,
|
|
412
|
+
subtitle,
|
|
413
|
+
}: {
|
|
414
|
+
title: string;
|
|
415
|
+
subtitle?: string;
|
|
416
|
+
}) {
|
|
417
|
+
return (
|
|
418
|
+
<div style={{ marginBottom: "var(--sp-8)" }}>
|
|
419
|
+
<h2
|
|
420
|
+
className="text-fg"
|
|
421
|
+
style={{
|
|
422
|
+
margin: subtitle ? "0 0 var(--sp-4)" : 0,
|
|
423
|
+
fontWeight: "var(--weight-semi)",
|
|
424
|
+
fontSize: "var(--text-h1)",
|
|
425
|
+
lineHeight: "var(--lh-h1)",
|
|
426
|
+
letterSpacing: "var(--track-tight)",
|
|
427
|
+
}}
|
|
428
|
+
>
|
|
429
|
+
{title}
|
|
430
|
+
</h2>
|
|
431
|
+
{subtitle && (
|
|
432
|
+
<p
|
|
433
|
+
className="text-fg-muted"
|
|
434
|
+
style={{ margin: 0, fontSize: "var(--text-body-lg)", lineHeight: "var(--lh-body-lg)" }}
|
|
435
|
+
>
|
|
436
|
+
{subtitle}
|
|
437
|
+
</p>
|
|
438
|
+
)}
|
|
439
|
+
</div>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function ErrorBanner({ message }: { message: string }) {
|
|
444
|
+
return (
|
|
445
|
+
<div
|
|
446
|
+
role="alert"
|
|
447
|
+
className="flex items-start rounded-md border border-[color:var(--danger-subtle)] bg-[color:var(--danger-subtle)] px-sp-4 py-sp-3"
|
|
448
|
+
style={{ gap: "var(--sp-3)" }}
|
|
449
|
+
>
|
|
450
|
+
<p
|
|
451
|
+
className="text-danger-text"
|
|
452
|
+
style={{ fontSize: "var(--text-small)", lineHeight: "var(--lh-small)" }}
|
|
453
|
+
>
|
|
454
|
+
{message}
|
|
455
|
+
</p>
|
|
456
|
+
</div>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function SuccessBanner({ message }: { message: string }) {
|
|
461
|
+
return (
|
|
462
|
+
<div
|
|
463
|
+
role="status"
|
|
464
|
+
className="flex items-start rounded-md border border-[color:var(--success-subtle)] bg-[color:var(--success-subtle)] px-sp-4 py-sp-3"
|
|
465
|
+
style={{ gap: "var(--sp-3)" }}
|
|
466
|
+
>
|
|
467
|
+
<p
|
|
468
|
+
className="text-success-text"
|
|
469
|
+
style={{ fontSize: "var(--text-small)", lineHeight: "var(--lh-small)" }}
|
|
470
|
+
>
|
|
471
|
+
{message}
|
|
472
|
+
</p>
|
|
473
|
+
</div>
|
|
474
|
+
);
|
|
475
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const UI_PACKAGE = "@slugbase/ui" as const;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/** Curated Lucide kebab-case names for folder icons (spec §7.1 searchable icon set). */
|
|
2
|
+
export const FOLDER_ICON_NAMES = [
|
|
3
|
+
"archive",
|
|
4
|
+
"atom",
|
|
5
|
+
"award",
|
|
6
|
+
"badge",
|
|
7
|
+
"banknote",
|
|
8
|
+
"bar-chart-3",
|
|
9
|
+
"beaker",
|
|
10
|
+
"bell",
|
|
11
|
+
"bike",
|
|
12
|
+
"book",
|
|
13
|
+
"book-copy",
|
|
14
|
+
"book-marked",
|
|
15
|
+
"book-open",
|
|
16
|
+
"bookmark",
|
|
17
|
+
"bot",
|
|
18
|
+
"box",
|
|
19
|
+
"brain",
|
|
20
|
+
"briefcase",
|
|
21
|
+
"brush",
|
|
22
|
+
"bug",
|
|
23
|
+
"building",
|
|
24
|
+
"building-2",
|
|
25
|
+
"bus",
|
|
26
|
+
"cake",
|
|
27
|
+
"calculator",
|
|
28
|
+
"calendar",
|
|
29
|
+
"camera",
|
|
30
|
+
"car",
|
|
31
|
+
"cat",
|
|
32
|
+
"check-circle",
|
|
33
|
+
"chef-hat",
|
|
34
|
+
"circle",
|
|
35
|
+
"clapperboard",
|
|
36
|
+
"clipboard",
|
|
37
|
+
"clock",
|
|
38
|
+
"cloud",
|
|
39
|
+
"code",
|
|
40
|
+
"code-2",
|
|
41
|
+
"coffee",
|
|
42
|
+
"compass",
|
|
43
|
+
"contact",
|
|
44
|
+
"cpu",
|
|
45
|
+
"credit-card",
|
|
46
|
+
"crown",
|
|
47
|
+
"database",
|
|
48
|
+
"dog",
|
|
49
|
+
"dollar-sign",
|
|
50
|
+
"download",
|
|
51
|
+
"droplet",
|
|
52
|
+
"dumbbell",
|
|
53
|
+
"external-link",
|
|
54
|
+
"eye",
|
|
55
|
+
"factory",
|
|
56
|
+
"file",
|
|
57
|
+
"file-text",
|
|
58
|
+
"film",
|
|
59
|
+
"filter",
|
|
60
|
+
"flag",
|
|
61
|
+
"flame",
|
|
62
|
+
"flower-2",
|
|
63
|
+
"folder",
|
|
64
|
+
"folder-archive",
|
|
65
|
+
"folder-heart",
|
|
66
|
+
"folder-input",
|
|
67
|
+
"folder-key",
|
|
68
|
+
"folder-lock",
|
|
69
|
+
"folder-open",
|
|
70
|
+
"folder-output",
|
|
71
|
+
"folder-plus",
|
|
72
|
+
"folder-sync",
|
|
73
|
+
"folder-tree",
|
|
74
|
+
"gamepad-2",
|
|
75
|
+
"gift",
|
|
76
|
+
"git-branch",
|
|
77
|
+
"github",
|
|
78
|
+
"gitlab",
|
|
79
|
+
"globe",
|
|
80
|
+
"graduation-cap",
|
|
81
|
+
"grid-3x3",
|
|
82
|
+
"hammer",
|
|
83
|
+
"handshake",
|
|
84
|
+
"hard-drive",
|
|
85
|
+
"hash",
|
|
86
|
+
"headphones",
|
|
87
|
+
"heart",
|
|
88
|
+
"highlighter",
|
|
89
|
+
"history",
|
|
90
|
+
"home",
|
|
91
|
+
"image",
|
|
92
|
+
"inbox",
|
|
93
|
+
"key",
|
|
94
|
+
"landmark",
|
|
95
|
+
"laptop",
|
|
96
|
+
"layers",
|
|
97
|
+
"layout-grid",
|
|
98
|
+
"layout-list",
|
|
99
|
+
"leaf",
|
|
100
|
+
"library",
|
|
101
|
+
"lightbulb",
|
|
102
|
+
"link",
|
|
103
|
+
"link-2",
|
|
104
|
+
"lock",
|
|
105
|
+
"mail",
|
|
106
|
+
"map",
|
|
107
|
+
"map-pin",
|
|
108
|
+
"medal",
|
|
109
|
+
"message-circle",
|
|
110
|
+
"mic",
|
|
111
|
+
"monitor",
|
|
112
|
+
"moon",
|
|
113
|
+
"music",
|
|
114
|
+
"package",
|
|
115
|
+
"paintbrush",
|
|
116
|
+
"palette",
|
|
117
|
+
"paperclip",
|
|
118
|
+
"pen-tool",
|
|
119
|
+
"pencil",
|
|
120
|
+
"phone",
|
|
121
|
+
"pie-chart",
|
|
122
|
+
"piggy-bank",
|
|
123
|
+
"pin",
|
|
124
|
+
"plane",
|
|
125
|
+
"pizza",
|
|
126
|
+
"receipt",
|
|
127
|
+
"rocket",
|
|
128
|
+
"rss",
|
|
129
|
+
"satellite",
|
|
130
|
+
"school",
|
|
131
|
+
"search",
|
|
132
|
+
"send",
|
|
133
|
+
"server",
|
|
134
|
+
"settings",
|
|
135
|
+
"share-2",
|
|
136
|
+
"shield",
|
|
137
|
+
"ship",
|
|
138
|
+
"shopping-bag",
|
|
139
|
+
"shopping-cart",
|
|
140
|
+
"smartphone",
|
|
141
|
+
"snowflake",
|
|
142
|
+
"sparkles",
|
|
143
|
+
"star",
|
|
144
|
+
"sun",
|
|
145
|
+
"tag",
|
|
146
|
+
"tags",
|
|
147
|
+
"target",
|
|
148
|
+
"terminal",
|
|
149
|
+
"timer",
|
|
150
|
+
"train",
|
|
151
|
+
"tree-pine",
|
|
152
|
+
"trophy",
|
|
153
|
+
"truck",
|
|
154
|
+
"upload",
|
|
155
|
+
"user",
|
|
156
|
+
"user-circle",
|
|
157
|
+
"users",
|
|
158
|
+
"utensils",
|
|
159
|
+
"video",
|
|
160
|
+
"wallet",
|
|
161
|
+
"wrench",
|
|
162
|
+
"zap",
|
|
163
|
+
] as const;
|
|
164
|
+
|
|
165
|
+
export type FolderIconName = (typeof FOLDER_ICON_NAMES)[number];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { FolderIcon, FolderOpenIcon, PaletteIcon } from "lucide-react";
|
|
3
|
+
|
|
4
|
+
import { kebabToPascalCase, resolveLucideIcon } from "./lucide-icon.js";
|
|
5
|
+
|
|
6
|
+
describe("lucide-icon", () => {
|
|
7
|
+
describe("kebabToPascalCase", () => {
|
|
8
|
+
it("converts single-word names", () => {
|
|
9
|
+
expect(kebabToPascalCase("folder")).toBe("Folder");
|
|
10
|
+
expect(kebabToPascalCase("palette")).toBe("Palette");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("converts multi-segment names", () => {
|
|
14
|
+
expect(kebabToPascalCase("folder-open")).toBe("FolderOpen");
|
|
15
|
+
expect(kebabToPascalCase("book-open-text")).toBe("BookOpenText");
|
|
16
|
+
expect(kebabToPascalCase("grid-3x3")).toBe("Grid3x3");
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe("resolveLucideIcon", () => {
|
|
21
|
+
it("maps known kebab-case names to Lucide components", () => {
|
|
22
|
+
expect(resolveLucideIcon("palette")).toBe(PaletteIcon);
|
|
23
|
+
expect(resolveLucideIcon("folder-open")).toBe(FolderOpenIcon);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("falls back to FolderIcon for null, empty, or unknown names", () => {
|
|
27
|
+
expect(resolveLucideIcon(null)).toBe(FolderIcon);
|
|
28
|
+
expect(resolveLucideIcon("")).toBe(FolderIcon);
|
|
29
|
+
expect(resolveLucideIcon("not-a-real-lucide-icon-name")).toBe(FolderIcon);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|