@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,47 @@
|
|
|
1
|
+
import type { LucideProps } from "lucide-react";
|
|
2
|
+
import * as LucideIcons from "lucide-react";
|
|
3
|
+
import { FolderIcon } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
export function kebabToPascalCase(name: string): string {
|
|
6
|
+
return name
|
|
7
|
+
.split("-")
|
|
8
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
9
|
+
.join("");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isLucideComponent(
|
|
13
|
+
value: unknown,
|
|
14
|
+
): value is typeof FolderIcon {
|
|
15
|
+
if (typeof value === "function") return true;
|
|
16
|
+
return (
|
|
17
|
+
typeof value === "object" &&
|
|
18
|
+
value !== null &&
|
|
19
|
+
"render" in value &&
|
|
20
|
+
typeof (value as { render?: unknown }).render === "function"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolveLucideIcon(name: string | null | undefined) {
|
|
25
|
+
if (!name) return FolderIcon;
|
|
26
|
+
const pascal = kebabToPascalCase(name);
|
|
27
|
+
const iconKey = `${pascal}Icon` as keyof typeof LucideIcons;
|
|
28
|
+
const candidate = LucideIcons[iconKey];
|
|
29
|
+
if (isLucideComponent(candidate)) {
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
const legacyKey = pascal as keyof typeof LucideIcons;
|
|
33
|
+
const legacy = LucideIcons[legacyKey];
|
|
34
|
+
if (isLucideComponent(legacy)) {
|
|
35
|
+
return legacy;
|
|
36
|
+
}
|
|
37
|
+
return FolderIcon;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type LucideIconProps = LucideProps & {
|
|
41
|
+
name: string | null | undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function LucideIcon({ name, ...props }: LucideIconProps) {
|
|
45
|
+
const Icon = resolveLucideIcon(name);
|
|
46
|
+
return <Icon {...props} />;
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export { UI_PACKAGE } from "./constants.js";
|
|
2
|
+
export { slugbaseTailwindPreset, resolveTokenColor, slugbaseCssVarColors } from "./tailwind/token-theme.js";
|
|
3
|
+
export type { SlugbaseTokenColor } from "./tailwind/token-theme.js";
|
|
4
|
+
export {
|
|
5
|
+
DEFAULT_THEME_PREFERENCE,
|
|
6
|
+
THEME_STORAGE_KEY,
|
|
7
|
+
type ResolvedTheme,
|
|
8
|
+
type ThemePreference,
|
|
9
|
+
} from "./theme/theme-types.js";
|
|
10
|
+
export { resolveTheme, readStoredThemePreference, persistThemePreference } from "./theme/resolve-theme.js";
|
|
11
|
+
export { ThemeProvider, useTheme, type ThemeContextValue, type ThemeProviderProps } from "./theme/theme-context.js";
|
|
12
|
+
export { AppShell, type AppShellProps } from "./components/AppShell.js";
|
|
13
|
+
export {
|
|
14
|
+
AuthShell,
|
|
15
|
+
AuthInput,
|
|
16
|
+
AuthButton,
|
|
17
|
+
AuthHeading,
|
|
18
|
+
ErrorBanner,
|
|
19
|
+
SuccessBanner,
|
|
20
|
+
SsoSection,
|
|
21
|
+
MailFieldIcon,
|
|
22
|
+
LockFieldIcon,
|
|
23
|
+
UserFieldIcon,
|
|
24
|
+
KeyFieldIcon,
|
|
25
|
+
type AuthShellProps,
|
|
26
|
+
type AuthShellBrandLabels,
|
|
27
|
+
type OidcProviderItem,
|
|
28
|
+
type SsoSectionProps,
|
|
29
|
+
} from "./components/auth-kit.js";
|
|
30
|
+
export { Kbd, type KbdProps } from "./components/Kbd.js";
|
|
31
|
+
export { Button, type ButtonProps, type ButtonVariant, type ButtonSize } from "./components/Button.js";
|
|
32
|
+
export { Input, type InputProps, type InputSize } from "./components/Input.js";
|
|
33
|
+
export { Label, type LabelProps } from "./components/Label.js";
|
|
34
|
+
export { FieldError, type FieldErrorProps } from "./components/FieldError.js";
|
|
35
|
+
export {
|
|
36
|
+
Dialog,
|
|
37
|
+
DialogClose,
|
|
38
|
+
DialogContent,
|
|
39
|
+
DialogOverlay,
|
|
40
|
+
DialogTrigger,
|
|
41
|
+
type DialogContentProps,
|
|
42
|
+
} from "./components/Dialog.js";
|
|
43
|
+
export {
|
|
44
|
+
Skeleton,
|
|
45
|
+
SkeletonCardGrid,
|
|
46
|
+
SkeletonList,
|
|
47
|
+
type SkeletonCardGridProps,
|
|
48
|
+
type SkeletonListProps,
|
|
49
|
+
type SkeletonProps,
|
|
50
|
+
} from "./components/Skeleton.js";
|
|
51
|
+
export { EmptyState, type EmptyStateProps } from "./components/EmptyState.js";
|
|
52
|
+
export {
|
|
53
|
+
ToastProvider,
|
|
54
|
+
useToast,
|
|
55
|
+
type ToastContextValue,
|
|
56
|
+
type ToastInput,
|
|
57
|
+
type ToastKind,
|
|
58
|
+
type ToastProviderProps,
|
|
59
|
+
} from "./components/ToastProvider.js";
|
|
60
|
+
export { ConfirmDialog, type ConfirmDialogProps } from "./components/ConfirmDialog.js";
|
|
61
|
+
export { Badge, type BadgeProps, type BadgeVariant } from "./components/Badge.js";
|
|
62
|
+
export {
|
|
63
|
+
Tooltip,
|
|
64
|
+
TooltipContent,
|
|
65
|
+
TooltipProvider,
|
|
66
|
+
TooltipTrigger,
|
|
67
|
+
type TooltipContentProps,
|
|
68
|
+
} from "./components/Tooltip.js";
|
|
69
|
+
export {
|
|
70
|
+
Popover,
|
|
71
|
+
PopoverClose,
|
|
72
|
+
PopoverContent,
|
|
73
|
+
PopoverTrigger,
|
|
74
|
+
type PopoverContentProps,
|
|
75
|
+
} from "./components/Popover.js";
|
|
76
|
+
export { Tag, type TagProps } from "./components/Tag.js";
|
|
77
|
+
export { SlugBadge, type SlugBadgeProps } from "./components/SlugBadge.js";
|
|
78
|
+
export { Checkbox, type CheckboxProps } from "./components/Checkbox.js";
|
|
79
|
+
export { IconButton, type IconButtonProps, type IconButtonSize } from "./components/IconButton.js";
|
|
80
|
+
export { ThemeSwitcher, type ThemeSwitcherLabels, type ThemeSwitcherProps } from "./components/ThemeSwitcher.js";
|
|
81
|
+
export { FolderGlyph, type FolderGlyphProps } from "./components/FolderGlyph.js";
|
|
82
|
+
export {
|
|
83
|
+
IconPicker,
|
|
84
|
+
filterIconNames,
|
|
85
|
+
type IconPickerLabels,
|
|
86
|
+
type IconPickerProps,
|
|
87
|
+
} from "./components/IconPicker.js";
|
|
88
|
+
export {
|
|
89
|
+
ColorPicker,
|
|
90
|
+
PRESET_COLORS,
|
|
91
|
+
type ColorPickerLabels,
|
|
92
|
+
type ColorPickerProps,
|
|
93
|
+
} from "./components/ColorPicker.js";
|
|
94
|
+
export {
|
|
95
|
+
LucideIcon,
|
|
96
|
+
resolveLucideIcon,
|
|
97
|
+
kebabToPascalCase,
|
|
98
|
+
type LucideIconProps,
|
|
99
|
+
} from "./icons/lucide-icon.js";
|
|
100
|
+
export { FOLDER_ICON_NAMES, type FolderIconName } from "./icons/folder-icon-names.js";
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
SlugBase Design System - Color & Type Foundations
|
|
3
|
+
Source: docs/design-prototype/V1/colors_and_type.css
|
|
4
|
+
Accent: periwinkle. Dark-first, developer-tool density.
|
|
5
|
+
============================================================ */
|
|
6
|
+
|
|
7
|
+
@import './ibm-plex-fonts.css';
|
|
8
|
+
|
|
9
|
+
:root,
|
|
10
|
+
[data-theme="dark"] {
|
|
11
|
+
--accent: #7782f7;
|
|
12
|
+
--accent-hover: #8d96f9;
|
|
13
|
+
--accent-active: #6571ec;
|
|
14
|
+
--accent-fg: #0b0c14;
|
|
15
|
+
--accent-subtle: rgba(119, 130, 247, 0.13);
|
|
16
|
+
--accent-subtle-hover: rgba(119, 130, 247, 0.20);
|
|
17
|
+
--accent-border: rgba(119, 130, 247, 0.40);
|
|
18
|
+
--accent-text: #9aa2fa;
|
|
19
|
+
|
|
20
|
+
--canvas: #0a0b10;
|
|
21
|
+
--base: #101220;
|
|
22
|
+
--raised: #171a2b;
|
|
23
|
+
--raised-2: #1e2236;
|
|
24
|
+
--overlay: #1b1f31;
|
|
25
|
+
--overlay-scrim: rgba(6, 7, 12, 0.62);
|
|
26
|
+
|
|
27
|
+
--border-subtle: rgba(255, 255, 255, 0.055);
|
|
28
|
+
--border: rgba(255, 255, 255, 0.10);
|
|
29
|
+
--border-strong: rgba(255, 255, 255, 0.16);
|
|
30
|
+
|
|
31
|
+
--fg: #eceef6;
|
|
32
|
+
--fg-muted: #a7adc2;
|
|
33
|
+
--fg-subtle: #6e7489;
|
|
34
|
+
--fg-faint: #4b5067;
|
|
35
|
+
|
|
36
|
+
--success: #45c98a;
|
|
37
|
+
--success-fg: #07140d;
|
|
38
|
+
--success-subtle: rgba(69, 201, 138, 0.14);
|
|
39
|
+
--success-text: #5fd6a0;
|
|
40
|
+
|
|
41
|
+
--warning: #e6b24e;
|
|
42
|
+
--warning-fg: #1a1304;
|
|
43
|
+
--warning-subtle: rgba(230, 178, 78, 0.14);
|
|
44
|
+
--warning-text: #efc878;
|
|
45
|
+
|
|
46
|
+
--danger: #f0686b;
|
|
47
|
+
--danger-hover: #f47e80;
|
|
48
|
+
--danger-fg: #1a0708;
|
|
49
|
+
--danger-subtle: rgba(240, 104, 107, 0.14);
|
|
50
|
+
--danger-text: #f48a8c;
|
|
51
|
+
|
|
52
|
+
--ring: 0 0 0 2px var(--canvas), 0 0 0 4px var(--accent);
|
|
53
|
+
--ring-inset: inset 0 0 0 1px var(--accent-border);
|
|
54
|
+
|
|
55
|
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.40);
|
|
56
|
+
--shadow-raised: 0 2px 8px rgba(0, 0, 0, 0.35);
|
|
57
|
+
--shadow-overlay: 0 18px 50px -12px rgba(0, 0, 0, 0.70), 0 0 0 1px var(--border);
|
|
58
|
+
--shadow-popover: 0 10px 32px -8px rgba(0, 0, 0, 0.62), 0 0 0 1px var(--border);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
[data-theme="light"] {
|
|
62
|
+
--accent: #5b66e8;
|
|
63
|
+
--accent-hover: #4a55e0;
|
|
64
|
+
--accent-active: #3f49d4;
|
|
65
|
+
--accent-fg: #ffffff;
|
|
66
|
+
--accent-subtle: rgba(91, 102, 232, 0.10);
|
|
67
|
+
--accent-subtle-hover: rgba(91, 102, 232, 0.16);
|
|
68
|
+
--accent-border: rgba(91, 102, 232, 0.32);
|
|
69
|
+
--accent-text: #4d58df;
|
|
70
|
+
|
|
71
|
+
--canvas: #f4f5f9;
|
|
72
|
+
--base: #ffffff;
|
|
73
|
+
--raised: #ffffff;
|
|
74
|
+
--raised-2: #f1f2f7;
|
|
75
|
+
--overlay: #ffffff;
|
|
76
|
+
--overlay-scrim: rgba(20, 22, 35, 0.30);
|
|
77
|
+
|
|
78
|
+
--border-subtle: rgba(16, 18, 32, 0.06);
|
|
79
|
+
--border: rgba(16, 18, 32, 0.11);
|
|
80
|
+
--border-strong: rgba(16, 18, 32, 0.18);
|
|
81
|
+
|
|
82
|
+
--fg: #14161f;
|
|
83
|
+
--fg-muted: #4a4f63;
|
|
84
|
+
--fg-subtle: #71768a;
|
|
85
|
+
--fg-faint: #a2a7ba;
|
|
86
|
+
|
|
87
|
+
--success-text: #1f9d68;
|
|
88
|
+
--warning-text: #b8861f;
|
|
89
|
+
--danger-text: #d6494d;
|
|
90
|
+
|
|
91
|
+
--shadow-sm: 0 1px 2px rgba(16, 18, 32, 0.08);
|
|
92
|
+
--shadow-raised: 0 1px 3px rgba(16, 18, 32, 0.10), 0 1px 2px rgba(16,18,32,0.06);
|
|
93
|
+
--shadow-overlay: 0 24px 56px -16px rgba(16, 18, 32, 0.30), 0 0 0 1px var(--border);
|
|
94
|
+
--shadow-popover: 0 12px 32px -10px rgba(16, 18, 32, 0.22), 0 0 0 1px var(--border);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
:root {
|
|
98
|
+
--font-sans: 'IBM Plex Sans', ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
99
|
+
--font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace;
|
|
100
|
+
|
|
101
|
+
--text-display: 28px; --lh-display: 34px;
|
|
102
|
+
--text-h1: 22px; --lh-h1: 28px;
|
|
103
|
+
--text-h2: 18px; --lh-h2: 24px;
|
|
104
|
+
--text-h3: 15px; --lh-h3: 20px;
|
|
105
|
+
--text-body-lg: 14px; --lh-body-lg: 20px;
|
|
106
|
+
--text-body: 13px; --lh-body: 18px;
|
|
107
|
+
--text-small: 12px; --lh-small: 16px;
|
|
108
|
+
--text-micro: 11px; --lh-micro: 14px;
|
|
109
|
+
--text-mono: 12.5px; --lh-mono: 18px;
|
|
110
|
+
|
|
111
|
+
--weight-regular: 400;
|
|
112
|
+
--weight-medium: 500;
|
|
113
|
+
--weight-semi: 600;
|
|
114
|
+
--weight-bold: 700;
|
|
115
|
+
|
|
116
|
+
--track-tight: -0.01em;
|
|
117
|
+
--track-wide: 0.06em;
|
|
118
|
+
|
|
119
|
+
--sp-1: 2px; --sp-2: 4px; --sp-3: 6px; --sp-4: 8px;
|
|
120
|
+
--sp-5: 12px; --sp-6: 16px; --sp-7: 20px; --sp-8: 24px;
|
|
121
|
+
--sp-9: 32px; --sp-10: 40px; --sp-11: 48px; --sp-12: 64px;
|
|
122
|
+
|
|
123
|
+
--r-sm: 4px; --r-md: 6px; --r-lg: 8px; --r-xl: 12px; --r-full: 999px;
|
|
124
|
+
|
|
125
|
+
--ease: cubic-bezier(0.2, 0, 0, 1);
|
|
126
|
+
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
|
127
|
+
--dur-micro: 110ms;
|
|
128
|
+
--dur: 170ms;
|
|
129
|
+
--dur-overlay: 230ms;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.t-display { font: var(--weight-semi) var(--text-display)/var(--lh-display) var(--font-sans); letter-spacing: var(--track-tight); color: var(--fg); }
|
|
133
|
+
.t-h1 { font: var(--weight-semi) var(--text-h1)/var(--lh-h1) var(--font-sans); letter-spacing: var(--track-tight); color: var(--fg); }
|
|
134
|
+
.t-h2 { font: var(--weight-semi) var(--text-h2)/var(--lh-h2) var(--font-sans); color: var(--fg); }
|
|
135
|
+
.t-h3 { font: var(--weight-semi) var(--text-h3)/var(--lh-h3) var(--font-sans); color: var(--fg); }
|
|
136
|
+
.t-body-lg { font: var(--weight-regular) var(--text-body-lg)/var(--lh-body-lg) var(--font-sans); color: var(--fg); }
|
|
137
|
+
.t-body { font: var(--weight-regular) var(--text-body)/var(--lh-body) var(--font-sans); color: var(--fg); }
|
|
138
|
+
.t-small { font: var(--weight-regular) var(--text-small)/var(--lh-small) var(--font-sans); color: var(--fg-muted); }
|
|
139
|
+
.t-label { font: var(--weight-medium) var(--text-micro)/var(--lh-micro) var(--font-sans); letter-spacing: var(--track-wide); text-transform: uppercase; color: var(--fg-subtle); }
|
|
140
|
+
.t-mono { font: var(--weight-regular) var(--text-mono)/var(--lh-mono) var(--font-mono); color: var(--fg-muted); }
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/* Self-hosted IBM Plex (SIL OFL) — replaces Google Fonts CDN for CSP font-src 'self'. */
|
|
2
|
+
@import '@fontsource/ibm-plex-sans/400.css';
|
|
3
|
+
@import '@fontsource/ibm-plex-sans/500.css';
|
|
4
|
+
@import '@fontsource/ibm-plex-sans/600.css';
|
|
5
|
+
@import '@fontsource/ibm-plex-sans/700.css';
|
|
6
|
+
@import '@fontsource/ibm-plex-mono/400.css';
|
|
7
|
+
@import '@fontsource/ibm-plex-mono/500.css';
|
|
8
|
+
@import '@fontsource/ibm-plex-mono/600.css';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveTokenColor, slugbaseCssVarColors } from "./token-theme.js";
|
|
3
|
+
|
|
4
|
+
describe("resolveTokenColor", () => {
|
|
5
|
+
it("maps accent to the design-token CSS variable", () => {
|
|
6
|
+
expect(resolveTokenColor("accent")).toBe("var(--accent)");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("exposes canvas and fg tokens bridged from colors-and-type.css", () => {
|
|
10
|
+
expect(slugbaseCssVarColors.canvas).toBe("var(--canvas)");
|
|
11
|
+
expect(slugbaseCssVarColors.fg).toBe("var(--fg)");
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { Config } from "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/** CSS custom properties bridged from colors-and-type.css - no hard-coded hex in Tailwind theme. */
|
|
4
|
+
export const slugbaseCssVarColors = {
|
|
5
|
+
accent: "var(--accent)",
|
|
6
|
+
"accent-hover": "var(--accent-hover)",
|
|
7
|
+
"accent-active": "var(--accent-active)",
|
|
8
|
+
"accent-fg": "var(--accent-fg)",
|
|
9
|
+
"accent-subtle": "var(--accent-subtle)",
|
|
10
|
+
"accent-subtle-hover": "var(--accent-subtle-hover)",
|
|
11
|
+
"accent-border": "var(--accent-border)",
|
|
12
|
+
"accent-text": "var(--accent-text)",
|
|
13
|
+
canvas: "var(--canvas)",
|
|
14
|
+
base: "var(--base)",
|
|
15
|
+
raised: "var(--raised)",
|
|
16
|
+
"raised-2": "var(--raised-2)",
|
|
17
|
+
overlay: "var(--overlay)",
|
|
18
|
+
fg: "var(--fg)",
|
|
19
|
+
"fg-muted": "var(--fg-muted)",
|
|
20
|
+
"fg-subtle": "var(--fg-subtle)",
|
|
21
|
+
"fg-faint": "var(--fg-faint)",
|
|
22
|
+
success: "var(--success)",
|
|
23
|
+
"success-fg": "var(--success-fg)",
|
|
24
|
+
"success-subtle": "var(--success-subtle)",
|
|
25
|
+
"success-text": "var(--success-text)",
|
|
26
|
+
warning: "var(--warning)",
|
|
27
|
+
"warning-fg": "var(--warning-fg)",
|
|
28
|
+
"warning-subtle": "var(--warning-subtle)",
|
|
29
|
+
"warning-text": "var(--warning-text)",
|
|
30
|
+
danger: "var(--danger)",
|
|
31
|
+
"danger-hover": "var(--danger-hover)",
|
|
32
|
+
"danger-fg": "var(--danger-fg)",
|
|
33
|
+
"danger-subtle": "var(--danger-subtle)",
|
|
34
|
+
"danger-text": "var(--danger-text)",
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export type SlugbaseTokenColor = keyof typeof slugbaseCssVarColors;
|
|
38
|
+
|
|
39
|
+
/** Resolves a design-token color name to its CSS variable reference. */
|
|
40
|
+
export function resolveTokenColor(token: SlugbaseTokenColor): string {
|
|
41
|
+
return slugbaseCssVarColors[token];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const slugbaseTailwindPreset = {
|
|
45
|
+
theme: {
|
|
46
|
+
extend: {
|
|
47
|
+
colors: slugbaseCssVarColors,
|
|
48
|
+
fontFamily: {
|
|
49
|
+
sans: ["var(--font-sans)"],
|
|
50
|
+
mono: ["var(--font-mono)"],
|
|
51
|
+
},
|
|
52
|
+
fontSize: {
|
|
53
|
+
display: ["var(--text-display)", { lineHeight: "var(--lh-display)" }],
|
|
54
|
+
h1: ["var(--text-h1)", { lineHeight: "var(--lh-h1)" }],
|
|
55
|
+
h2: ["var(--text-h2)", { lineHeight: "var(--lh-h2)" }],
|
|
56
|
+
h3: ["var(--text-h3)", { lineHeight: "var(--lh-h3)" }],
|
|
57
|
+
"body-lg": ["var(--text-body-lg)", { lineHeight: "var(--lh-body-lg)" }],
|
|
58
|
+
body: ["var(--text-body)", { lineHeight: "var(--lh-body)" }],
|
|
59
|
+
small: ["var(--text-small)", { lineHeight: "var(--lh-small)" }],
|
|
60
|
+
micro: ["var(--text-micro)", { lineHeight: "var(--lh-micro)" }],
|
|
61
|
+
mono: ["var(--text-mono)", { lineHeight: "var(--lh-mono)" }],
|
|
62
|
+
},
|
|
63
|
+
borderRadius: {
|
|
64
|
+
sm: "var(--r-sm)",
|
|
65
|
+
md: "var(--r-md)",
|
|
66
|
+
lg: "var(--r-lg)",
|
|
67
|
+
xl: "var(--r-xl)",
|
|
68
|
+
full: "var(--r-full)",
|
|
69
|
+
},
|
|
70
|
+
spacing: {
|
|
71
|
+
"sp-1": "var(--sp-1)",
|
|
72
|
+
"sp-2": "var(--sp-2)",
|
|
73
|
+
"sp-3": "var(--sp-3)",
|
|
74
|
+
"sp-4": "var(--sp-4)",
|
|
75
|
+
"sp-5": "var(--sp-5)",
|
|
76
|
+
"sp-6": "var(--sp-6)",
|
|
77
|
+
"sp-7": "var(--sp-7)",
|
|
78
|
+
"sp-8": "var(--sp-8)",
|
|
79
|
+
"sp-9": "var(--sp-9)",
|
|
80
|
+
"sp-10": "var(--sp-10)",
|
|
81
|
+
"sp-11": "var(--sp-11)",
|
|
82
|
+
"sp-12": "var(--sp-12)",
|
|
83
|
+
},
|
|
84
|
+
boxShadow: {
|
|
85
|
+
raised: "var(--shadow-raised)",
|
|
86
|
+
overlay: "var(--shadow-overlay)",
|
|
87
|
+
popover: "var(--shadow-popover)",
|
|
88
|
+
},
|
|
89
|
+
transitionDuration: {
|
|
90
|
+
micro: "var(--dur-micro)",
|
|
91
|
+
DEFAULT: "var(--dur)",
|
|
92
|
+
overlay: "var(--dur-overlay)",
|
|
93
|
+
},
|
|
94
|
+
transitionTimingFunction: {
|
|
95
|
+
DEFAULT: "var(--ease)",
|
|
96
|
+
"in-out": "var(--ease-in-out)",
|
|
97
|
+
},
|
|
98
|
+
keyframes: {
|
|
99
|
+
shimmer: {
|
|
100
|
+
from: { backgroundPosition: "200% 0" },
|
|
101
|
+
to: { backgroundPosition: "-200% 0" },
|
|
102
|
+
},
|
|
103
|
+
"toast-in": {
|
|
104
|
+
from: { opacity: "0", transform: "translateX(16px)" },
|
|
105
|
+
to: { opacity: "1", transform: "translateX(0)" },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
animation: {
|
|
109
|
+
shimmer: "shimmer 1.3s ease-in-out infinite",
|
|
110
|
+
"toast-in": "toast-in var(--dur) var(--ease)",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
} satisfies Pick<Config, "theme">;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { resolveTheme } from "./resolve-theme.js";
|
|
3
|
+
|
|
4
|
+
describe("resolveTheme", () => {
|
|
5
|
+
it("returns dark when preference is dark regardless of system", () => {
|
|
6
|
+
expect(resolveTheme("dark", false)).toBe("dark");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("returns light when preference is light regardless of system", () => {
|
|
10
|
+
expect(resolveTheme("light", true)).toBe("light");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("follows system preference when auto", () => {
|
|
14
|
+
expect(resolveTheme("auto", true)).toBe("dark");
|
|
15
|
+
expect(resolveTheme("auto", false)).toBe("light");
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ResolvedTheme, ThemePreference } from "./theme-types.js";
|
|
2
|
+
|
|
3
|
+
/** Maps user preference + system dark mode to an applied `data-theme` value. */
|
|
4
|
+
export function resolveTheme(
|
|
5
|
+
preference: ThemePreference,
|
|
6
|
+
prefersDark: boolean,
|
|
7
|
+
): ResolvedTheme {
|
|
8
|
+
if (preference === "auto") {
|
|
9
|
+
return prefersDark ? "dark" : "light";
|
|
10
|
+
}
|
|
11
|
+
return preference;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function readStoredThemePreference(): ThemePreference | null {
|
|
15
|
+
if (typeof window === "undefined") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const stored = window.localStorage.getItem("slugbase-theme-preference");
|
|
19
|
+
if (stored === "light" || stored === "dark" || stored === "auto") {
|
|
20
|
+
return stored;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function persistThemePreference(preference: ThemePreference): void {
|
|
26
|
+
if (typeof window === "undefined") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
window.localStorage.setItem("slugbase-theme-preference", preference);
|
|
30
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useState,
|
|
8
|
+
type ReactNode,
|
|
9
|
+
} from "react";
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_THEME_PREFERENCE,
|
|
12
|
+
type ResolvedTheme,
|
|
13
|
+
type ThemePreference,
|
|
14
|
+
} from "./theme-types.js";
|
|
15
|
+
import {
|
|
16
|
+
persistThemePreference,
|
|
17
|
+
readStoredThemePreference,
|
|
18
|
+
resolveTheme,
|
|
19
|
+
} from "./resolve-theme.js";
|
|
20
|
+
|
|
21
|
+
export type ThemeContextValue = {
|
|
22
|
+
preference: ThemePreference;
|
|
23
|
+
resolved: ResolvedTheme;
|
|
24
|
+
setPreference: (next: ThemePreference) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
28
|
+
|
|
29
|
+
function getSystemPrefersDark(): boolean {
|
|
30
|
+
if (typeof window === "undefined") {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function applyDocumentTheme(resolved: ResolvedTheme): void {
|
|
37
|
+
if (typeof document === "undefined") {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
document.documentElement.dataset.theme = resolved;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ThemeProviderProps = {
|
|
44
|
+
children: ReactNode;
|
|
45
|
+
/** SSR hint before client hydration. */
|
|
46
|
+
initialPreference?: ThemePreference;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export function ThemeProvider({
|
|
50
|
+
children,
|
|
51
|
+
initialPreference = DEFAULT_THEME_PREFERENCE,
|
|
52
|
+
}: ThemeProviderProps) {
|
|
53
|
+
const [preference, setPreferenceState] = useState<ThemePreference>(() => {
|
|
54
|
+
return readStoredThemePreference() ?? initialPreference;
|
|
55
|
+
});
|
|
56
|
+
const [prefersDark, setPrefersDark] = useState(getSystemPrefersDark);
|
|
57
|
+
|
|
58
|
+
const resolved = useMemo(
|
|
59
|
+
() => resolveTheme(preference, prefersDark),
|
|
60
|
+
[preference, prefersDark],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
applyDocumentTheme(resolved);
|
|
65
|
+
}, [resolved]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
69
|
+
const onChange = () => {
|
|
70
|
+
setPrefersDark(media.matches);
|
|
71
|
+
};
|
|
72
|
+
media.addEventListener("change", onChange);
|
|
73
|
+
return () => {
|
|
74
|
+
media.removeEventListener("change", onChange);
|
|
75
|
+
};
|
|
76
|
+
}, []);
|
|
77
|
+
|
|
78
|
+
const setPreference = useCallback((next: ThemePreference) => {
|
|
79
|
+
setPreferenceState(next);
|
|
80
|
+
persistThemePreference(next);
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const value = useMemo(
|
|
84
|
+
() => ({ preference, resolved, setPreference }),
|
|
85
|
+
[preference, resolved, setPreference],
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function useTheme(): ThemeContextValue {
|
|
94
|
+
const ctx = useContext(ThemeContext);
|
|
95
|
+
if (!ctx) {
|
|
96
|
+
throw new Error("useTheme must be used within ThemeProvider");
|
|
97
|
+
}
|
|
98
|
+
return ctx;
|
|
99
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Config } from "tailwindcss";
|
|
2
|
+
import { slugbaseTailwindPreset } from "./src/tailwind/token-theme.js";
|
|
3
|
+
|
|
4
|
+
/** Shared Tailwind preset - consume via `presets` in app packages. */
|
|
5
|
+
const config: Config = {
|
|
6
|
+
content: ["./src/**/*.{ts,tsx}"],
|
|
7
|
+
presets: [slugbaseTailwindPreset],
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default config;
|
|
11
|
+
export { slugbaseTailwindPreset, resolveTokenColor, slugbaseCssVarColors } from "./src/tailwind/token-theme.js";
|