@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,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,3 @@
1
+ export const THEME_STORAGE_KEY = "slugbase-theme-preference";
2
+ export const DEFAULT_THEME_PREFERENCE = "dark";
3
+ //# sourceMappingURL=theme-types.js.map
@@ -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,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ };
@@ -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
+ });