@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,6 @@
|
|
|
1
|
+
import type { ResolvedTheme, ThemePreference } from "./theme-types.js";
|
|
2
|
+
/** Maps user preference + system dark mode to an applied `data-theme` value. */
|
|
3
|
+
export declare function resolveTheme(preference: ThemePreference, prefersDark: boolean): ResolvedTheme;
|
|
4
|
+
export declare function readStoredThemePreference(): ThemePreference | null;
|
|
5
|
+
export declare function persistThemePreference(preference: ThemePreference): void;
|
|
6
|
+
//# sourceMappingURL=resolve-theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-theme.d.ts","sourceRoot":"","sources":["../../src/theme/resolve-theme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEvE,gFAAgF;AAChF,wBAAgB,YAAY,CAC1B,UAAU,EAAE,eAAe,EAC3B,WAAW,EAAE,OAAO,GACnB,aAAa,CAKf;AAED,wBAAgB,yBAAyB,IAAI,eAAe,GAAG,IAAI,CASlE;AAED,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,eAAe,GAAG,IAAI,CAKxE"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Maps user preference + system dark mode to an applied `data-theme` value. */
|
|
2
|
+
export function resolveTheme(preference, prefersDark) {
|
|
3
|
+
if (preference === "auto") {
|
|
4
|
+
return prefersDark ? "dark" : "light";
|
|
5
|
+
}
|
|
6
|
+
return preference;
|
|
7
|
+
}
|
|
8
|
+
export function readStoredThemePreference() {
|
|
9
|
+
if (typeof window === "undefined") {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const stored = window.localStorage.getItem("slugbase-theme-preference");
|
|
13
|
+
if (stored === "light" || stored === "dark" || stored === "auto") {
|
|
14
|
+
return stored;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
export function persistThemePreference(preference) {
|
|
19
|
+
if (typeof window === "undefined") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
window.localStorage.setItem("slugbase-theme-preference", preference);
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=resolve-theme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolve-theme.js","sourceRoot":"","sources":["../../src/theme/resolve-theme.ts"],"names":[],"mappings":"AAEA,gFAAgF;AAChF,MAAM,UAAU,YAAY,CAC1B,UAA2B,EAC3B,WAAoB;IAEpB,IAAI,UAAU,KAAK,MAAM,EAAE,CAAC;QAC1B,OAAO,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IACxC,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,yBAAyB;IACvC,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACxE,IAAI,MAAM,KAAK,OAAO,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,MAAM,EAAE,CAAC;QACjE,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,UAA2B;IAChE,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO;IACT,CAAC;IACD,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,2BAA2B,EAAE,UAAU,CAAC,CAAC;AACvE,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type ResolvedTheme, type ThemePreference } from "./theme-types.js";
|
|
3
|
+
export type ThemeContextValue = {
|
|
4
|
+
preference: ThemePreference;
|
|
5
|
+
resolved: ResolvedTheme;
|
|
6
|
+
setPreference: (next: ThemePreference) => void;
|
|
7
|
+
};
|
|
8
|
+
export type ThemeProviderProps = {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
/** SSR hint before client hydration. */
|
|
11
|
+
initialPreference?: ThemePreference;
|
|
12
|
+
};
|
|
13
|
+
export declare function ThemeProvider({ children, initialPreference, }: ThemeProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export declare function useTheme(): ThemeContextValue;
|
|
15
|
+
//# sourceMappingURL=theme-context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-context.d.ts","sourceRoot":"","sources":["../../src/theme/theme-context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,eAAe,EACrB,MAAM,kBAAkB,CAAC;AAO1B,MAAM,MAAM,iBAAiB,GAAG;IAC9B,UAAU,EAAE,eAAe,CAAC;IAC5B,QAAQ,EAAE,aAAa,CAAC;IACxB,aAAa,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;CAChD,CAAC;AAkBF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,SAAS,CAAC;IACpB,wCAAwC;IACxC,iBAAiB,CAAC,EAAE,eAAe,CAAC;CACrC,CAAC;AAEF,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,iBAA4C,GAC7C,EAAE,kBAAkB,2CAuCpB;AAED,wBAAgB,QAAQ,IAAI,iBAAiB,CAM5C"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
|
|
3
|
+
import { DEFAULT_THEME_PREFERENCE, } from "./theme-types.js";
|
|
4
|
+
import { persistThemePreference, readStoredThemePreference, resolveTheme, } from "./resolve-theme.js";
|
|
5
|
+
const ThemeContext = createContext(null);
|
|
6
|
+
function getSystemPrefersDark() {
|
|
7
|
+
if (typeof window === "undefined") {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
11
|
+
}
|
|
12
|
+
function applyDocumentTheme(resolved) {
|
|
13
|
+
if (typeof document === "undefined") {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
document.documentElement.dataset.theme = resolved;
|
|
17
|
+
}
|
|
18
|
+
export function ThemeProvider({ children, initialPreference = DEFAULT_THEME_PREFERENCE, }) {
|
|
19
|
+
const [preference, setPreferenceState] = useState(() => {
|
|
20
|
+
return readStoredThemePreference() ?? initialPreference;
|
|
21
|
+
});
|
|
22
|
+
const [prefersDark, setPrefersDark] = useState(getSystemPrefersDark);
|
|
23
|
+
const resolved = useMemo(() => resolveTheme(preference, prefersDark), [preference, prefersDark]);
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
applyDocumentTheme(resolved);
|
|
26
|
+
}, [resolved]);
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
29
|
+
const onChange = () => {
|
|
30
|
+
setPrefersDark(media.matches);
|
|
31
|
+
};
|
|
32
|
+
media.addEventListener("change", onChange);
|
|
33
|
+
return () => {
|
|
34
|
+
media.removeEventListener("change", onChange);
|
|
35
|
+
};
|
|
36
|
+
}, []);
|
|
37
|
+
const setPreference = useCallback((next) => {
|
|
38
|
+
setPreferenceState(next);
|
|
39
|
+
persistThemePreference(next);
|
|
40
|
+
}, []);
|
|
41
|
+
const value = useMemo(() => ({ preference, resolved, setPreference }), [preference, resolved, setPreference]);
|
|
42
|
+
return (_jsx(ThemeContext.Provider, { value: value, children: children }));
|
|
43
|
+
}
|
|
44
|
+
export function useTheme() {
|
|
45
|
+
const ctx = useContext(ThemeContext);
|
|
46
|
+
if (!ctx) {
|
|
47
|
+
throw new Error("useTheme must be used within ThemeProvider");
|
|
48
|
+
}
|
|
49
|
+
return ctx;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=theme-context.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-context.js","sourceRoot":"","sources":["../../src/theme/theme-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,aAAa,EACb,WAAW,EACX,UAAU,EACV,SAAS,EACT,OAAO,EACP,QAAQ,GAET,MAAM,OAAO,CAAC;AACf,OAAO,EACL,wBAAwB,GAGzB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,YAAY,GACb,MAAM,oBAAoB,CAAC;AAQ5B,MAAM,YAAY,GAAG,aAAa,CAA2B,IAAI,CAAC,CAAC;AAEnE,SAAS,oBAAoB;IAC3B,IAAI,OAAO,MAAM,KAAK,WAAW,EAAE,CAAC;QAClC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAM,CAAC,UAAU,CAAC,8BAA8B,CAAC,CAAC,OAAO,CAAC;AACnE,CAAC;AAED,SAAS,kBAAkB,CAAC,QAAuB;IACjD,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;QACpC,OAAO;IACT,CAAC;IACD,QAAQ,CAAC,eAAe,CAAC,OAAO,CAAC,KAAK,GAAG,QAAQ,CAAC;AACpD,CAAC;AAQD,MAAM,UAAU,aAAa,CAAC,EAC5B,QAAQ,EACR,iBAAiB,GAAG,wBAAwB,GACzB;IACnB,MAAM,CAAC,UAAU,EAAE,kBAAkB,CAAC,GAAG,QAAQ,CAAkB,GAAG,EAAE;QACtE,OAAO,yBAAyB,EAAE,IAAI,iBAAiB,CAAC;IAC1D,CAAC,CAAC,CAAC;IACH,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,oBAAoB,CAAC,CAAC;IAErE,MAAM,QAAQ,GAAG,OAAO,CACtB,GAAG,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,WAAW,CAAC,EAC3C,CAAC,UAAU,EAAE,WAAW,CAAC,CAC1B,CAAC;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,kBAAkB,CAAC,QAAQ,CAAC,CAAC;IAC/B,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC;IAEf,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,8BAA8B,CAAC,CAAC;QAChE,MAAM,QAAQ,GAAG,GAAG,EAAE;YACpB,cAAc,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAChC,CAAC,CAAC;QACF,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC3C,OAAO,GAAG,EAAE;YACV,KAAK,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAChD,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,aAAa,GAAG,WAAW,CAAC,CAAC,IAAqB,EAAE,EAAE;QAC1D,kBAAkB,CAAC,IAAI,CAAC,CAAC;QACzB,sBAAsB,CAAC,IAAI,CAAC,CAAC;IAC/B,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,QAAQ,EAAE,aAAa,EAAE,CAAC,EAC/C,CAAC,UAAU,EAAE,QAAQ,EAAE,aAAa,CAAC,CACtC,CAAC;IAEF,OAAO,CACL,KAAC,YAAY,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAAG,QAAQ,GAAyB,CACxE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,QAAQ;IACtB,MAAM,GAAG,GAAG,UAAU,CAAC,YAAY,CAAC,CAAC;IACrC,IAAI,CAAC,GAAG,EAAE,CAAC;QACT,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAChE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type ThemePreference = "light" | "dark" | "auto";
|
|
2
|
+
export type ResolvedTheme = "light" | "dark";
|
|
3
|
+
export declare const THEME_STORAGE_KEY: "slugbase-theme-preference";
|
|
4
|
+
export declare const DEFAULT_THEME_PREFERENCE: ThemePreference;
|
|
5
|
+
//# sourceMappingURL=theme-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-types.d.ts","sourceRoot":"","sources":["../../src/theme/theme-types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAAC;AAExD,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,MAAM,CAAC;AAE7C,eAAO,MAAM,iBAAiB,EAAG,2BAAoC,CAAC;AAEtE,eAAO,MAAM,wBAAwB,EAAE,eAAwB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme-types.js","sourceRoot":"","sources":["../../src/theme/theme-types.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,iBAAiB,GAAG,2BAAoC,CAAC;AAEtE,MAAM,CAAC,MAAM,wBAAwB,GAAoB,MAAM,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@slugbase/ui",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mdg-labs/slugbase.git",
|
|
9
|
+
"directory": "packages/ui"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public",
|
|
13
|
+
"registry": "https://registry.npmjs.org"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./tailwind": {
|
|
21
|
+
"types": "./src/tailwind/token-theme.ts",
|
|
22
|
+
"default": "./src/tailwind/token-theme.ts"
|
|
23
|
+
},
|
|
24
|
+
"./styles/colors-and-type.css": "./src/styles/colors-and-type.css"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"lint": "eslint src/",
|
|
28
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
29
|
+
"build": "tsc -p tsconfig.build.json",
|
|
30
|
+
"test:unit": "vitest run --config vitest.config.ts",
|
|
31
|
+
"test:integration": "vitest run --passWithNoTests"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@fontsource/ibm-plex-mono": "^5.2.7",
|
|
35
|
+
"@fontsource/ibm-plex-sans": "^5.2.8",
|
|
36
|
+
"@radix-ui/react-checkbox": "^1.3.3",
|
|
37
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
38
|
+
"@radix-ui/react-popover": "^1.1.17",
|
|
39
|
+
"@radix-ui/react-tooltip": "^1.2.10",
|
|
40
|
+
"lucide-react": "^1.17.0",
|
|
41
|
+
"react": "^19.2.6",
|
|
42
|
+
"react-dom": "^19.2.6"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
46
|
+
"@testing-library/react": "^16.3.2",
|
|
47
|
+
"@types/react": "^19.2.15",
|
|
48
|
+
"@types/react-dom": "^19.2.3",
|
|
49
|
+
"autoprefixer": "^10.5.0",
|
|
50
|
+
"happy-dom": "^20.9.0",
|
|
51
|
+
"postcss": "^8.5.15",
|
|
52
|
+
"tailwindcss": "^3.4.17",
|
|
53
|
+
"vitest": "^4.1.6"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { AppShell } from "./AppShell.js";
|
|
4
|
+
|
|
5
|
+
describe("AppShell", () => {
|
|
6
|
+
it("matches base layout snapshot", () => {
|
|
7
|
+
const { container } = render(
|
|
8
|
+
<AppShell
|
|
9
|
+
sidebar={<div>Sidebar content</div>}
|
|
10
|
+
topBar={<div>TopBar content</div>}
|
|
11
|
+
>
|
|
12
|
+
<p>Content</p>
|
|
13
|
+
</AppShell>,
|
|
14
|
+
);
|
|
15
|
+
expect(container).toMatchSnapshot();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("renders sidebar, topbar, and main regions", () => {
|
|
19
|
+
const { container } = render(
|
|
20
|
+
<AppShell sidebar={<span>Nav</span>} topBar={<span>Bar</span>}>
|
|
21
|
+
<p>Main</p>
|
|
22
|
+
</AppShell>,
|
|
23
|
+
);
|
|
24
|
+
expect(container.querySelector('[data-testid="app-shell-sidebar"]')).toBeTruthy();
|
|
25
|
+
expect(container.querySelector('[data-testid="app-shell-topbar"]')).toBeTruthy();
|
|
26
|
+
expect(container.querySelector('[data-testid="app-shell-main"]')).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type AppShellProps = {
|
|
4
|
+
/** 248px left sidebar content */
|
|
5
|
+
sidebar: ReactNode;
|
|
6
|
+
/** 52px fixed-height top bar content */
|
|
7
|
+
topBar: ReactNode;
|
|
8
|
+
/** Scrollable main content area */
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Base signed-in layout shell.
|
|
14
|
+
* Grid: 248px sidebar | flexible main column with 52px topbar + scrollable content.
|
|
15
|
+
* Layout is viewport-locked (h-screen, overflow-hidden) - scroll lives inside the
|
|
16
|
+
* main content area only (spec §9.3.1).
|
|
17
|
+
*/
|
|
18
|
+
export function AppShell({ sidebar, topBar, children }: AppShellProps) {
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className="grid h-screen overflow-hidden bg-canvas font-sans text-body text-fg"
|
|
22
|
+
style={{ gridTemplateColumns: "248px 1fr" }}
|
|
23
|
+
data-testid="app-shell"
|
|
24
|
+
>
|
|
25
|
+
<aside
|
|
26
|
+
className="flex min-h-0 flex-col overflow-y-auto border-r border-[color:var(--border-subtle)] bg-base"
|
|
27
|
+
data-testid="app-shell-sidebar"
|
|
28
|
+
>
|
|
29
|
+
{sidebar}
|
|
30
|
+
</aside>
|
|
31
|
+
|
|
32
|
+
<div className="flex min-h-0 min-w-0 flex-col overflow-hidden">
|
|
33
|
+
<header
|
|
34
|
+
className="flex shrink-0 items-center border-b border-[color:var(--border-subtle)]"
|
|
35
|
+
style={{ height: 52, background: "color-mix(in srgb, var(--base) 60%, transparent)" }}
|
|
36
|
+
data-testid="app-shell-topbar"
|
|
37
|
+
>
|
|
38
|
+
{topBar}
|
|
39
|
+
</header>
|
|
40
|
+
<main
|
|
41
|
+
className="flex-1 overflow-y-auto bg-canvas p-sp-7"
|
|
42
|
+
data-testid="app-shell-main"
|
|
43
|
+
>
|
|
44
|
+
{children}
|
|
45
|
+
</main>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type BadgeVariant = "accent" | "success" | "warning" | "neutral";
|
|
4
|
+
|
|
5
|
+
export type BadgeProps = {
|
|
6
|
+
variant?: BadgeVariant;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const variantClasses: Record<BadgeVariant, string> = {
|
|
12
|
+
accent: "bg-accent-subtle text-accent-text",
|
|
13
|
+
success: "bg-success-subtle text-success-text",
|
|
14
|
+
warning: "bg-warning-subtle text-warning-text",
|
|
15
|
+
neutral: "bg-raised-2 text-fg-muted",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Status / plan badge - matches prototype `.badge` pattern.
|
|
20
|
+
* Variant: accent | success | warning | neutral.
|
|
21
|
+
*/
|
|
22
|
+
export function Badge({
|
|
23
|
+
variant = "neutral",
|
|
24
|
+
children,
|
|
25
|
+
className = "",
|
|
26
|
+
}: BadgeProps) {
|
|
27
|
+
return (
|
|
28
|
+
<span
|
|
29
|
+
className={`inline-flex items-center gap-1 rounded-full font-semibold ${variantClasses[variant]} ${className}`}
|
|
30
|
+
style={{
|
|
31
|
+
height: "18px",
|
|
32
|
+
padding: "0 7px",
|
|
33
|
+
fontSize: "var(--text-micro)",
|
|
34
|
+
lineHeight: "var(--lh-micro)",
|
|
35
|
+
letterSpacing: "0.02em",
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</span>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export type ButtonVariant = "primary" | "secondary" | "ghost" | "danger";
|
|
4
|
+
|
|
5
|
+
export type ButtonSize = "default" | "sm" | "lg";
|
|
6
|
+
|
|
7
|
+
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
8
|
+
variant?: ButtonVariant;
|
|
9
|
+
size?: ButtonSize;
|
|
10
|
+
block?: boolean;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const variantClasses: Record<ButtonVariant, string> = {
|
|
15
|
+
primary:
|
|
16
|
+
"border border-transparent bg-accent text-accent-fg font-semibold hover:bg-accent-hover active:bg-accent-active disabled:opacity-50",
|
|
17
|
+
secondary:
|
|
18
|
+
"border border-[color:var(--border)] bg-raised text-fg hover:bg-raised-2 hover:border-[color:var(--border-strong)] disabled:opacity-50",
|
|
19
|
+
ghost:
|
|
20
|
+
"border border-transparent bg-transparent text-fg-muted hover:bg-raised hover:text-fg disabled:opacity-50",
|
|
21
|
+
danger:
|
|
22
|
+
"border border-transparent bg-danger text-danger-fg font-semibold hover:bg-danger-hover disabled:opacity-50",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const sizeClasses: Record<ButtonSize, string> = {
|
|
26
|
+
default: "h-[34px] px-sp-5",
|
|
27
|
+
sm: "h-[28px] px-sp-4",
|
|
28
|
+
lg: "h-[44px] px-sp-6",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const sizeFontStyles: Record<ButtonSize, string> = {
|
|
32
|
+
default: "var(--text-body)",
|
|
33
|
+
sm: "var(--text-small)",
|
|
34
|
+
lg: "var(--text-body-lg)",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function Button({
|
|
38
|
+
variant = "primary",
|
|
39
|
+
size = "default",
|
|
40
|
+
block = false,
|
|
41
|
+
className = "",
|
|
42
|
+
type = "button",
|
|
43
|
+
children,
|
|
44
|
+
...props
|
|
45
|
+
}: ButtonProps) {
|
|
46
|
+
return (
|
|
47
|
+
<button
|
|
48
|
+
type={type}
|
|
49
|
+
className={`inline-flex items-center justify-center gap-sp-3 rounded-md font-medium transition-colors duration-micro ease-in-out active:translate-y-[0.5px] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas disabled:cursor-not-allowed ${sizeClasses[size]} ${variantClasses[variant]}${block ? " w-full" : ""} ${className}`}
|
|
50
|
+
style={{ fontSize: sizeFontStyles[size], lineHeight: "1" }}
|
|
51
|
+
{...props}
|
|
52
|
+
>
|
|
53
|
+
{children}
|
|
54
|
+
</button>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
2
|
+
|
|
3
|
+
export type CheckboxProps = {
|
|
4
|
+
checked?: boolean | "indeterminate";
|
|
5
|
+
onCheckedChange?: (checked: boolean | "indeterminate") => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
id?: string;
|
|
8
|
+
"aria-label"?: string;
|
|
9
|
+
"aria-labelledby"?: string;
|
|
10
|
+
className?: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 3-state checkbox (off / on / indeterminate).
|
|
15
|
+
* Styled to match prototype `.cb` pattern from app.css.
|
|
16
|
+
* Uses Radix Checkbox primitive for accessible keyboard + pointer handling.
|
|
17
|
+
*/
|
|
18
|
+
export function Checkbox({
|
|
19
|
+
checked,
|
|
20
|
+
onCheckedChange,
|
|
21
|
+
disabled,
|
|
22
|
+
id,
|
|
23
|
+
className = "",
|
|
24
|
+
...ariaProps
|
|
25
|
+
}: CheckboxProps) {
|
|
26
|
+
return (
|
|
27
|
+
<CheckboxPrimitive.Root
|
|
28
|
+
id={id}
|
|
29
|
+
checked={checked}
|
|
30
|
+
onCheckedChange={onCheckedChange}
|
|
31
|
+
disabled={disabled}
|
|
32
|
+
className={`flex-none cursor-pointer rounded-sm bg-canvas transition-[background,border-color] duration-micro ease-in-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-canvas disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-accent data-[state=checked]:bg-accent data-[state=indeterminate]:border-accent data-[state=indeterminate]:bg-accent ${className}`}
|
|
33
|
+
style={{
|
|
34
|
+
width: "17px",
|
|
35
|
+
height: "17px",
|
|
36
|
+
border: "1.5px solid var(--border-strong)",
|
|
37
|
+
display: "grid",
|
|
38
|
+
placeItems: "center",
|
|
39
|
+
}}
|
|
40
|
+
{...(ariaProps["aria-label"] ? { "aria-label": ariaProps["aria-label"] } : {})}
|
|
41
|
+
{...(ariaProps["aria-labelledby"]
|
|
42
|
+
? { "aria-labelledby": ariaProps["aria-labelledby"] }
|
|
43
|
+
: {})}
|
|
44
|
+
>
|
|
45
|
+
<CheckboxPrimitive.Indicator className="flex items-center justify-center text-accent-fg">
|
|
46
|
+
{checked === "indeterminate" ? (
|
|
47
|
+
<span
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
className="block rounded-[2px] bg-accent-fg"
|
|
50
|
+
style={{ width: "8px", height: "2px" }}
|
|
51
|
+
/>
|
|
52
|
+
) : (
|
|
53
|
+
<svg
|
|
54
|
+
aria-hidden="true"
|
|
55
|
+
width="10"
|
|
56
|
+
height="10"
|
|
57
|
+
viewBox="0 0 24 24"
|
|
58
|
+
fill="none"
|
|
59
|
+
stroke="currentColor"
|
|
60
|
+
strokeWidth="3"
|
|
61
|
+
strokeLinecap="round"
|
|
62
|
+
strokeLinejoin="round"
|
|
63
|
+
>
|
|
64
|
+
<polyline points="20 6 9 17 4 12" />
|
|
65
|
+
</svg>
|
|
66
|
+
)}
|
|
67
|
+
</CheckboxPrimitive.Indicator>
|
|
68
|
+
</CheckboxPrimitive.Root>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { ColorPicker, PRESET_COLORS } from "./ColorPicker.js";
|
|
5
|
+
|
|
6
|
+
const labels = {
|
|
7
|
+
label: "Color",
|
|
8
|
+
none: "None",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe("ColorPicker", () => {
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
cleanup();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("renders 16 swatches in a grid", () => {
|
|
17
|
+
render(<ColorPicker value={null} onChange={vi.fn()} labels={labels} />);
|
|
18
|
+
|
|
19
|
+
expect(PRESET_COLORS).toHaveLength(16);
|
|
20
|
+
expect(screen.getByTestId("color-picker-grid").children).toHaveLength(16);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("selects a swatch and toggles off when clicked again", () => {
|
|
24
|
+
const onChange = vi.fn();
|
|
25
|
+
|
|
26
|
+
render(<ColorPicker value={null} onChange={onChange} labels={labels} />);
|
|
27
|
+
|
|
28
|
+
fireEvent.click(screen.getByTestId("color-picker-swatch-7782f7"));
|
|
29
|
+
expect(onChange).toHaveBeenCalledWith("#7782f7");
|
|
30
|
+
|
|
31
|
+
cleanup();
|
|
32
|
+
onChange.mockClear();
|
|
33
|
+
|
|
34
|
+
render(<ColorPicker value="#7782f7" onChange={onChange} labels={labels} />);
|
|
35
|
+
fireEvent.click(screen.getByTestId("color-picker-swatch-7782f7"));
|
|
36
|
+
expect(onChange).toHaveBeenCalledWith(null);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("clears the selected color", () => {
|
|
40
|
+
const onChange = vi.fn();
|
|
41
|
+
|
|
42
|
+
render(<ColorPicker value="#45c98a" onChange={onChange} labels={labels} />);
|
|
43
|
+
|
|
44
|
+
fireEvent.click(screen.getByTestId("color-picker-clear"));
|
|
45
|
+
expect(onChange).toHaveBeenCalledWith(null);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("hides clear control when no color is selected", () => {
|
|
49
|
+
render(<ColorPicker value={null} onChange={vi.fn()} labels={labels} />);
|
|
50
|
+
|
|
51
|
+
expect(screen.queryByTestId("color-picker-clear")).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { CheckIcon } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
/** Curated palette — semantic hues + accent variants aligned with design tokens. */
|
|
4
|
+
export const PRESET_COLORS = [
|
|
5
|
+
"#7782f7",
|
|
6
|
+
"#45c98a",
|
|
7
|
+
"#e6b24e",
|
|
8
|
+
"#f0686b",
|
|
9
|
+
"#5ba4e6",
|
|
10
|
+
"#56b6e6",
|
|
11
|
+
"#e8944a",
|
|
12
|
+
"#a77bea",
|
|
13
|
+
"#b178f7",
|
|
14
|
+
"#f78fb0",
|
|
15
|
+
"#5fd6a0",
|
|
16
|
+
"#8a90a0",
|
|
17
|
+
"#6571ec",
|
|
18
|
+
"#9aa2fa",
|
|
19
|
+
"#efc878",
|
|
20
|
+
"#f48a8c",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
export type ColorPickerLabels = {
|
|
24
|
+
label: string;
|
|
25
|
+
none: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ColorPickerProps = {
|
|
29
|
+
value: string | null;
|
|
30
|
+
onChange: (color: string | null) => void;
|
|
31
|
+
labels: ColorPickerLabels;
|
|
32
|
+
testId?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export function ColorPicker({
|
|
36
|
+
value,
|
|
37
|
+
onChange,
|
|
38
|
+
labels,
|
|
39
|
+
testId = "color-picker",
|
|
40
|
+
}: ColorPickerProps) {
|
|
41
|
+
return (
|
|
42
|
+
<div className="flex flex-col gap-sp-2" data-testid={testId}>
|
|
43
|
+
<span
|
|
44
|
+
className="text-fg-muted"
|
|
45
|
+
style={{ fontSize: "var(--text-small)", lineHeight: "var(--lh-small)" }}
|
|
46
|
+
>
|
|
47
|
+
{labels.label}
|
|
48
|
+
</span>
|
|
49
|
+
<div className="grid grid-cols-8 gap-sp-3" data-testid={`${testId}-grid`}>
|
|
50
|
+
{PRESET_COLORS.map((swatch) => (
|
|
51
|
+
<button
|
|
52
|
+
key={swatch}
|
|
53
|
+
type="button"
|
|
54
|
+
className="relative h-7 w-7 rounded-full transition-transform hover:scale-110 focus:outline-none focus:ring-2 focus:ring-[color:var(--accent)] focus:ring-offset-2 focus:ring-offset-[color:var(--overlay)]"
|
|
55
|
+
style={{ backgroundColor: swatch }}
|
|
56
|
+
aria-label={swatch}
|
|
57
|
+
aria-pressed={value === swatch}
|
|
58
|
+
data-testid={`${testId}-swatch-${swatch.slice(1)}`}
|
|
59
|
+
onClick={() => {
|
|
60
|
+
onChange(value === swatch ? null : swatch);
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
{value === swatch && (
|
|
64
|
+
<CheckIcon
|
|
65
|
+
size={14}
|
|
66
|
+
className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 text-white"
|
|
67
|
+
/>
|
|
68
|
+
)}
|
|
69
|
+
</button>
|
|
70
|
+
))}
|
|
71
|
+
</div>
|
|
72
|
+
{value && (
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
className="self-start text-fg-muted transition-colors hover:text-fg"
|
|
76
|
+
style={{ fontSize: "var(--text-small)" }}
|
|
77
|
+
data-testid={`${testId}-clear`}
|
|
78
|
+
onClick={() => {
|
|
79
|
+
onChange(null);
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
{labels.none}
|
|
83
|
+
</button>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
|
|
5
|
+
import { ConfirmDialog } from "./ConfirmDialog.js";
|
|
6
|
+
|
|
7
|
+
function ConfirmHarness() {
|
|
8
|
+
const [open, setOpen] = useState(false);
|
|
9
|
+
const onConfirm = vi.fn();
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<>
|
|
13
|
+
<button type="button" onClick={() => { setOpen(true); }}>
|
|
14
|
+
Open confirm
|
|
15
|
+
</button>
|
|
16
|
+
<ConfirmDialog
|
|
17
|
+
open={open}
|
|
18
|
+
onOpenChange={setOpen}
|
|
19
|
+
title="Remove member"
|
|
20
|
+
description="This cannot be undone."
|
|
21
|
+
confirmLabel="Remove"
|
|
22
|
+
cancelLabel="Cancel"
|
|
23
|
+
destructive
|
|
24
|
+
onConfirm={() => {
|
|
25
|
+
onConfirm();
|
|
26
|
+
setOpen(false);
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
</>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("ConfirmDialog", () => {
|
|
34
|
+
it("supports keyboard-friendly confirm and cancel actions", () => {
|
|
35
|
+
render(<ConfirmHarness />);
|
|
36
|
+
|
|
37
|
+
fireEvent.click(screen.getByRole("button", { name: "Open confirm" }));
|
|
38
|
+
expect(screen.getByRole("dialog")).toBeTruthy();
|
|
39
|
+
expect(screen.getByText("Remove member")).toBeTruthy();
|
|
40
|
+
|
|
41
|
+
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
|
|
42
|
+
expect(screen.queryByRole("dialog")).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
});
|