@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.
Files changed (181) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-lint.log +4 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/dist/components/AppShell.d.ts +17 -0
  5. package/dist/components/AppShell.d.ts.map +1 -0
  6. package/dist/components/AppShell.js +11 -0
  7. package/dist/components/AppShell.js.map +1 -0
  8. package/dist/components/Badge.d.ts +13 -0
  9. package/dist/components/Badge.d.ts.map +1 -0
  10. package/dist/components/Badge.js +21 -0
  11. package/dist/components/Badge.js.map +1 -0
  12. package/dist/components/Button.d.ts +11 -0
  13. package/dist/components/Button.d.ts.map +1 -0
  14. package/dist/components/Button.js +21 -0
  15. package/dist/components/Button.js.map +1 -0
  16. package/dist/components/Checkbox.d.ts +16 -0
  17. package/dist/components/Checkbox.d.ts.map +1 -0
  18. package/dist/components/Checkbox.js +19 -0
  19. package/dist/components/Checkbox.js.map +1 -0
  20. package/dist/components/ColorPicker.d.ts +14 -0
  21. package/dist/components/ColorPicker.d.ts.map +1 -0
  22. package/dist/components/ColorPicker.js +29 -0
  23. package/dist/components/ColorPicker.js.map +1 -0
  24. package/dist/components/ConfirmDialog.d.ts +17 -0
  25. package/dist/components/ConfirmDialog.d.ts.map +1 -0
  26. package/dist/components/ConfirmDialog.js +8 -0
  27. package/dist/components/ConfirmDialog.js.map +1 -0
  28. package/dist/components/Dialog.d.ts +15 -0
  29. package/dist/components/Dialog.d.ts.map +1 -0
  30. package/dist/components/Dialog.js +12 -0
  31. package/dist/components/Dialog.js.map +1 -0
  32. package/dist/components/EmptyState.d.ts +12 -0
  33. package/dist/components/EmptyState.d.ts.map +1 -0
  34. package/dist/components/EmptyState.js +6 -0
  35. package/dist/components/EmptyState.js.map +1 -0
  36. package/dist/components/FieldError.d.ts +7 -0
  37. package/dist/components/FieldError.d.ts.map +1 -0
  38. package/dist/components/FieldError.js +5 -0
  39. package/dist/components/FieldError.js.map +1 -0
  40. package/dist/components/FolderGlyph.d.ts +8 -0
  41. package/dist/components/FolderGlyph.d.ts.map +1 -0
  42. package/dist/components/FolderGlyph.js +14 -0
  43. package/dist/components/FolderGlyph.js.map +1 -0
  44. package/dist/components/IconButton.d.ts +14 -0
  45. package/dist/components/IconButton.d.ts.map +1 -0
  46. package/dist/components/IconButton.js +15 -0
  47. package/dist/components/IconButton.js.map +1 -0
  48. package/dist/components/IconPicker.d.ts +16 -0
  49. package/dist/components/IconPicker.d.ts.map +1 -0
  50. package/dist/components/IconPicker.js +32 -0
  51. package/dist/components/IconPicker.js.map +1 -0
  52. package/dist/components/Input.d.ts +10 -0
  53. package/dist/components/Input.d.ts.map +1 -0
  54. package/dist/components/Input.js +33 -0
  55. package/dist/components/Input.js.map +1 -0
  56. package/dist/components/Kbd.d.ts +11 -0
  57. package/dist/components/Kbd.d.ts.map +1 -0
  58. package/dist/components/Kbd.js +18 -0
  59. package/dist/components/Kbd.js.map +1 -0
  60. package/dist/components/Label.d.ts +6 -0
  61. package/dist/components/Label.d.ts.map +1 -0
  62. package/dist/components/Label.js +5 -0
  63. package/dist/components/Label.js.map +1 -0
  64. package/dist/components/Popover.d.ts +14 -0
  65. package/dist/components/Popover.d.ts.map +1 -0
  66. package/dist/components/Popover.js +9 -0
  67. package/dist/components/Popover.js.map +1 -0
  68. package/dist/components/Skeleton.d.ts +22 -0
  69. package/dist/components/Skeleton.d.ts.map +1 -0
  70. package/dist/components/Skeleton.js +14 -0
  71. package/dist/components/Skeleton.js.map +1 -0
  72. package/dist/components/SlugBadge.d.ts +13 -0
  73. package/dist/components/SlugBadge.d.ts.map +1 -0
  74. package/dist/components/SlugBadge.js +17 -0
  75. package/dist/components/SlugBadge.js.map +1 -0
  76. package/dist/components/Tag.d.ts +11 -0
  77. package/dist/components/Tag.d.ts.map +1 -0
  78. package/dist/components/Tag.js +16 -0
  79. package/dist/components/Tag.js.map +1 -0
  80. package/dist/components/ThemeSwitcher.d.ts +11 -0
  81. package/dist/components/ThemeSwitcher.d.ts.map +1 -0
  82. package/dist/components/ThemeSwitcher.js +25 -0
  83. package/dist/components/ThemeSwitcher.js.map +1 -0
  84. package/dist/components/ToastProvider.d.ts +19 -0
  85. package/dist/components/ToastProvider.d.ts.map +1 -0
  86. package/dist/components/ToastProvider.js +62 -0
  87. package/dist/components/ToastProvider.js.map +1 -0
  88. package/dist/components/Tooltip.d.ts +13 -0
  89. package/dist/components/Tooltip.d.ts.map +1 -0
  90. package/dist/components/Tooltip.js +9 -0
  91. package/dist/components/Tooltip.js.map +1 -0
  92. package/dist/components/auth-kit.d.ts +59 -0
  93. package/dist/components/auth-kit.d.ts.map +1 -0
  94. package/dist/components/auth-kit.js +101 -0
  95. package/dist/components/auth-kit.js.map +1 -0
  96. package/dist/constants.d.ts +2 -0
  97. package/dist/constants.d.ts.map +1 -0
  98. package/dist/constants.js +2 -0
  99. package/dist/constants.js.map +1 -0
  100. package/dist/icons/folder-icon-names.d.ts +4 -0
  101. package/dist/icons/folder-icon-names.d.ts.map +1 -0
  102. package/dist/icons/folder-icon-names.js +164 -0
  103. package/dist/icons/folder-icon-names.js.map +1 -0
  104. package/dist/icons/lucide-icon.d.ts +8 -0
  105. package/dist/icons/lucide-icon.d.ts.map +1 -0
  106. package/dist/icons/lucide-icon.js +38 -0
  107. package/dist/icons/lucide-icon.js.map +1 -0
  108. package/dist/index.d.ts +32 -0
  109. package/dist/index.d.ts.map +1 -0
  110. package/dist/index.js +31 -0
  111. package/dist/index.js.map +1 -0
  112. package/dist/tailwind/token-theme.d.ts +167 -0
  113. package/dist/tailwind/token-theme.d.ts.map +1 -0
  114. package/dist/tailwind/token-theme.js +109 -0
  115. package/dist/tailwind/token-theme.js.map +1 -0
  116. package/dist/theme/resolve-theme.d.ts +6 -0
  117. package/dist/theme/resolve-theme.d.ts.map +1 -0
  118. package/dist/theme/resolve-theme.js +24 -0
  119. package/dist/theme/resolve-theme.js.map +1 -0
  120. package/dist/theme/theme-context.d.ts +15 -0
  121. package/dist/theme/theme-context.d.ts.map +1 -0
  122. package/dist/theme/theme-context.js +51 -0
  123. package/dist/theme/theme-context.js.map +1 -0
  124. package/dist/theme/theme-types.d.ts +5 -0
  125. package/dist/theme/theme-types.d.ts.map +1 -0
  126. package/dist/theme/theme-types.js +3 -0
  127. package/dist/theme/theme-types.js.map +1 -0
  128. package/package.json +55 -0
  129. package/postcss.config.js +6 -0
  130. package/src/components/AppShell.spec.tsx +28 -0
  131. package/src/components/AppShell.tsx +49 -0
  132. package/src/components/Badge.tsx +41 -0
  133. package/src/components/Button.tsx +56 -0
  134. package/src/components/Checkbox.tsx +70 -0
  135. package/src/components/ColorPicker.spec.tsx +53 -0
  136. package/src/components/ColorPicker.tsx +87 -0
  137. package/src/components/ConfirmDialog.spec.tsx +44 -0
  138. package/src/components/ConfirmDialog.tsx +67 -0
  139. package/src/components/Dialog.tsx +63 -0
  140. package/src/components/EmptyState.spec.tsx +22 -0
  141. package/src/components/EmptyState.tsx +54 -0
  142. package/src/components/FieldError.tsx +19 -0
  143. package/src/components/FolderGlyph.tsx +38 -0
  144. package/src/components/IconButton.tsx +42 -0
  145. package/src/components/IconPicker.spec.tsx +54 -0
  146. package/src/components/IconPicker.tsx +132 -0
  147. package/src/components/Input.tsx +76 -0
  148. package/src/components/Kbd.tsx +30 -0
  149. package/src/components/Label.tsx +17 -0
  150. package/src/components/Popover.tsx +35 -0
  151. package/src/components/Skeleton.spec.tsx +18 -0
  152. package/src/components/Skeleton.tsx +94 -0
  153. package/src/components/SlugBadge.tsx +35 -0
  154. package/src/components/Tag.tsx +31 -0
  155. package/src/components/ThemeSwitcher.tsx +60 -0
  156. package/src/components/ToastProvider.spec.tsx +60 -0
  157. package/src/components/ToastProvider.tsx +167 -0
  158. package/src/components/Tooltip.tsx +33 -0
  159. package/src/components/__snapshots__/AppShell.spec.tsx.snap +41 -0
  160. package/src/components/auth-kit.spec.tsx +24 -0
  161. package/src/components/auth-kit.tsx +475 -0
  162. package/src/constants.ts +1 -0
  163. package/src/icons/folder-icon-names.ts +165 -0
  164. package/src/icons/lucide-icon.spec.ts +32 -0
  165. package/src/icons/lucide-icon.tsx +47 -0
  166. package/src/index.ts +100 -0
  167. package/src/styles/app.css +5 -0
  168. package/src/styles/colors-and-type.css +140 -0
  169. package/src/styles/ibm-plex-fonts.css +8 -0
  170. package/src/tailwind/token-theme.spec.ts +13 -0
  171. package/src/tailwind/token-theme.ts +114 -0
  172. package/src/theme/resolve-theme.spec.ts +17 -0
  173. package/src/theme/resolve-theme.ts +30 -0
  174. package/src/theme/theme-context.tsx +99 -0
  175. package/src/theme/theme-types.ts +7 -0
  176. package/tailwind.config.ts +11 -0
  177. package/tsconfig.build.json +4 -0
  178. package/tsconfig.build.tsbuildinfo +1 -0
  179. package/tsconfig.json +11 -0
  180. package/tsconfig.tsbuildinfo +1 -0
  181. 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
+ }
@@ -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
+ });