@motor-hero/ui-kit 0.18.0 → 0.20.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/README.md CHANGED
@@ -43,6 +43,47 @@ function App() {
43
43
  }
44
44
  ```
45
45
 
46
+ ## Temas e Multi-tenant
47
+
48
+ Por padrão o kit usa o tema **zinc** (claro/escuro), alternado pelo `ThemeProvider`.
49
+ Para servir várias marcas (tenants) no mesmo app, passe a prop opcional `tenantTheme`.
50
+ **Sem ela, nada muda** — apps existentes seguem com o tema padrão.
51
+
52
+ Pense em dois eixos independentes: a **marca** (qual tenant) e o **modo** (claro/escuro).
53
+ Só o grupo de cores da **marca** é por tenant; neutros (`background`, `card`, `border`…) e
54
+ status (`destructive`, `success`) são compartilhados e respondem só ao modo.
55
+
56
+ ```tsx
57
+ import { ThemeProvider } from "@motor-hero/ui-kit"
58
+ import type { TenantTheme } from "@motor-hero/ui-kit"
59
+
60
+ const tenantTheme: TenantTheme = {
61
+ light: {
62
+ primary: "hsl(145 63% 32%)",
63
+ primaryForeground: "hsl(0 0% 100%)",
64
+ secondary: "hsl(0 0% 96%)",
65
+ secondaryForeground: "hsl(0 0% 12%)",
66
+ accent: "hsl(45 95% 55%)",
67
+ accentForeground: "hsl(0 0% 8%)",
68
+ },
69
+ dark: { primary: "hsl(145 50% 55%)" }, // opcional e parcial: só o que muda no escuro
70
+ }
71
+
72
+ <ThemeProvider defaultTheme="dark" tenantTheme={tenantTheme}>
73
+ <App />
74
+ </ThemeProvider>
75
+ ```
76
+
77
+ **Regras:**
78
+
79
+ - Mínimo por tenant: `primary` + `primaryForeground` (2 cores). A marca completa tem três cores — `primary`, `secondary` e `accent` — cada uma com seu foreground.
80
+ - `dark` é opcional e parcial — informe apenas os tokens que mudam; o resto herda do `light`.
81
+ - Valores são qualquer cor CSS válida (`hsl(...)`, `#hex`, `oklch(...)`); prefira `hsl(...)`.
82
+ - Os `*Foreground` são responsabilidade sua — garanta contraste AA, o kit não deriva.
83
+ - As cores entram como estilo inline no `<html>`, vencendo o `.dark` e qualquer `@theme` do app (sem conflito de cascata). Memoize o objeto `tenantTheme`.
84
+
85
+ Guia completo em [ui.motorhero.com.br](https://ui.motorhero.com.br) → **Temas Multi-tenant**.
86
+
46
87
  ## Desenvolvimento
47
88
 
48
89
  ```bash
@@ -1,14 +1,16 @@
1
+ import { type CompanyTheme } from "../lib/company-theme";
1
2
  type Theme = "dark" | "light" | "system";
2
3
  type ThemeProviderProps = {
3
4
  children: React.ReactNode;
4
5
  defaultTheme?: Theme;
5
6
  storageKey?: string;
7
+ companyTheme?: CompanyTheme;
6
8
  };
7
9
  type ThemeProviderState = {
8
10
  theme: Theme;
9
11
  setTheme: (theme: Theme) => void;
10
12
  };
11
- export declare function ThemeProvider({ children, defaultTheme, storageKey, ...props }: ThemeProviderProps): import("react").JSX.Element;
13
+ export declare function ThemeProvider({ children, defaultTheme, storageKey, companyTheme, ...props }: ThemeProviderProps): import("react").JSX.Element;
12
14
  export declare function useTheme(): ThemeProviderState;
13
15
  export type { Theme, ThemeProviderProps, ThemeProviderState };
14
16
  //# sourceMappingURL=theme-provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"theme-provider.d.ts","sourceRoot":"","sources":["../../src/components/theme-provider.tsx"],"names":[],"mappings":"AAEA,KAAK,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;AAExC,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,YAAY,CAAC,EAAE,KAAK,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB,CAAA;AAED,KAAK,kBAAkB,GAAG;IACxB,KAAK,EAAE,KAAK,CAAA;IACZ,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CACjC,CAAA;AASD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,YAAuB,EACvB,UAAuB,EACvB,GAAG,KAAK,EACT,EAAE,kBAAkB,+BA+BpB;AAED,wBAAgB,QAAQ,uBAMvB;AAED,YAAY,EAAE,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,CAAA"}
1
+ {"version":3,"file":"theme-provider.d.ts","sourceRoot":"","sources":["../../src/components/theme-provider.tsx"],"names":[],"mappings":"AACA,OAAO,EAAqB,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAE3E,KAAK,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;AAExC,KAAK,kBAAkB,GAAG;IACxB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,YAAY,CAAC,EAAE,KAAK,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B,CAAA;AAED,KAAK,kBAAkB,GAAG;IACxB,KAAK,EAAE,KAAK,CAAA;IACZ,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;CACjC,CAAA;AASD,wBAAgB,aAAa,CAAC,EAC5B,QAAQ,EACR,YAAuB,EACvB,UAAuB,EACvB,YAAY,EACZ,GAAG,KAAK,EACT,EAAE,kBAAkB,+BAsCpB;AAED,wBAAgB,QAAQ,uBAMvB;AAED,YAAY,EAAE,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,CAAA"}
@@ -0,0 +1,10 @@
1
+ export type UseCompanySubdomainOptions = {
2
+ enableOverride?: boolean;
3
+ overrideStorageKey?: string;
4
+ };
5
+ export type UseCompanySubdomainResult = {
6
+ subdomain: string | undefined;
7
+ };
8
+ export declare function getSubdomain(hostname: string): string | undefined;
9
+ export declare function useCompanySubdomain(opts?: UseCompanySubdomainOptions): UseCompanySubdomainResult;
10
+ //# sourceMappingURL=use-company-subdomain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-company-subdomain.d.ts","sourceRoot":"","sources":["../../src/hooks/use-company-subdomain.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,0BAA0B,GAAG;IACvC,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,kBAAkB,CAAC,EAAE,MAAM,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;CAC9B,CAAA;AAED,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAMjE;AAiBD,wBAAgB,mBAAmB,CACjC,IAAI,GAAE,0BAA+B,GACpC,yBAAyB,CAc3B"}
package/dist/index.cjs CHANGED
@@ -56,10 +56,13 @@ __export(index_exports, {
56
56
  ThemeProvider: () => ThemeProvider,
57
57
  Toaster: () => Toaster,
58
58
  UserMenu: () => UserMenu,
59
+ applyCompanyTheme: () => applyCompanyTheme,
59
60
  cn: () => cn,
60
61
  defaultRenderLink: () => defaultRenderLink,
61
62
  extractApiError: () => extractApiError,
63
+ getSubdomain: () => getSubdomain,
62
64
  toast: () => import_sonner2.toast,
65
+ useCompanySubdomain: () => useCompanySubdomain,
63
66
  useCustomToast: () => useCustomToast,
64
67
  useDisclosure: () => useDisclosure,
65
68
  useIsDesktop: () => useIsDesktop,
@@ -69,6 +72,33 @@ module.exports = __toCommonJS(index_exports);
69
72
 
70
73
  // src/components/theme-provider.tsx
71
74
  var import_react = require("react");
75
+
76
+ // src/lib/company-theme.ts
77
+ var VAR_MAP = {
78
+ primary: "--color-primary",
79
+ primaryForeground: "--color-primary-foreground",
80
+ secondary: "--color-secondary",
81
+ secondaryForeground: "--color-secondary-foreground",
82
+ accent: "--color-accent",
83
+ accentForeground: "--color-accent-foreground",
84
+ ring: "--color-ring"
85
+ };
86
+ function definedTokens(tokens) {
87
+ if (!tokens) return {};
88
+ return Object.fromEntries(
89
+ Object.entries(tokens).filter(([, value]) => value != null)
90
+ );
91
+ }
92
+ function applyCompanyTheme(root, companyTheme, mode) {
93
+ const tokens = companyTheme ? mode === "dark" ? { ...companyTheme.light, ...definedTokens(companyTheme.dark) } : companyTheme.light : {};
94
+ for (const key of Object.keys(VAR_MAP)) {
95
+ const value = tokens[key];
96
+ if (value) root.style.setProperty(VAR_MAP[key], value);
97
+ else root.style.removeProperty(VAR_MAP[key]);
98
+ }
99
+ }
100
+
101
+ // src/components/theme-provider.tsx
72
102
  var import_jsx_runtime = require("react/jsx-runtime");
73
103
  var initialState = {
74
104
  theme: "system",
@@ -79,6 +109,7 @@ function ThemeProvider({
79
109
  children,
80
110
  defaultTheme = "system",
81
111
  storageKey = "ui-theme",
112
+ companyTheme,
82
113
  ...props
83
114
  }) {
84
115
  const [theme, setTheme] = (0, import_react.useState)(
@@ -86,14 +117,20 @@ function ThemeProvider({
86
117
  );
87
118
  (0, import_react.useEffect)(() => {
88
119
  const root = window.document.documentElement;
89
- root.classList.remove("light", "dark");
120
+ const applyResolved = (resolved) => {
121
+ root.classList.remove("light", "dark");
122
+ root.classList.add(resolved);
123
+ applyCompanyTheme(root, companyTheme, resolved);
124
+ };
90
125
  if (theme === "system") {
91
- const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
92
- root.classList.add(systemTheme);
93
- return;
126
+ const mql = window.matchMedia("(prefers-color-scheme: dark)");
127
+ applyResolved(mql.matches ? "dark" : "light");
128
+ const onChange = (e) => applyResolved(e.matches ? "dark" : "light");
129
+ mql.addEventListener("change", onChange);
130
+ return () => mql.removeEventListener("change", onChange);
94
131
  }
95
- root.classList.add(theme);
96
- }, [theme]);
132
+ applyResolved(theme);
133
+ }, [theme, companyTheme]);
97
134
  const value = {
98
135
  theme,
99
136
  setTheme: (theme2) => {
@@ -111,6 +148,42 @@ function useTheme() {
111
148
  return context;
112
149
  }
113
150
 
151
+ // src/hooks/use-company-subdomain.ts
152
+ var import_react2 = require("react");
153
+ function getSubdomain(hostname) {
154
+ const labels = hostname.split(".");
155
+ if (labels.length < 2) return void 0;
156
+ const first = labels[0];
157
+ if (!first || first === "www" || first === "localhost") return void 0;
158
+ return first;
159
+ }
160
+ function resolveOverride(storageKey) {
161
+ if (typeof window === "undefined") return void 0;
162
+ const params = new URLSearchParams(window.location.search);
163
+ if (params.has("company")) {
164
+ const value = params.get("company");
165
+ if (value) {
166
+ window.localStorage.setItem(storageKey, value);
167
+ return value;
168
+ }
169
+ window.localStorage.removeItem(storageKey);
170
+ return void 0;
171
+ }
172
+ return window.localStorage.getItem(storageKey) ?? void 0;
173
+ }
174
+ function useCompanySubdomain(opts = {}) {
175
+ const { enableOverride = false, overrideStorageKey = "company-subdomain" } = opts;
176
+ const [subdomain] = (0, import_react2.useState)(() => {
177
+ if (enableOverride) {
178
+ const override = resolveOverride(overrideStorageKey);
179
+ if (override) return override;
180
+ }
181
+ if (typeof window === "undefined") return void 0;
182
+ return getSubdomain(window.location.hostname);
183
+ });
184
+ return { subdomain };
185
+ }
186
+
114
187
  // src/components/mode-toggle.tsx
115
188
  var DropdownMenu = __toESM(require("@radix-ui/react-dropdown-menu"), 1);
116
189
  var import_lucide_react = require("lucide-react");
@@ -340,11 +413,11 @@ function ProgressBar({
340
413
  }
341
414
 
342
415
  // src/components/form-field.tsx
343
- var import_react2 = require("react");
416
+ var import_react3 = require("react");
344
417
  var import_jsx_runtime9 = require("react/jsx-runtime");
345
418
  function FormField({ label, htmlFor, error, required, children, className }) {
346
- const errorId = (0, import_react2.useId)();
347
- const field = error && (0, import_react2.isValidElement)(children) ? (0, import_react2.cloneElement)(children, {
419
+ const errorId = (0, import_react3.useId)();
420
+ const field = error && (0, import_react3.isValidElement)(children) ? (0, import_react3.cloneElement)(children, {
348
421
  "aria-invalid": true,
349
422
  "aria-describedby": [
350
423
  children.props["aria-describedby"],
@@ -815,7 +888,7 @@ function DataTableWrapper({
815
888
  }
816
889
 
817
890
  // src/components/mobile-card-list.tsx
818
- var import_react3 = require("react");
891
+ var import_react4 = require("react");
819
892
  var import_jsx_runtime19 = require("react/jsx-runtime");
820
893
  var SCROLL_THRESHOLD = 8;
821
894
  function MobileCardList({
@@ -826,8 +899,8 @@ function MobileCardList({
826
899
  loadingCount = 5,
827
900
  className
828
901
  }) {
829
- const start = (0, import_react3.useRef)(null);
830
- const scrolled = (0, import_react3.useRef)(false);
902
+ const start = (0, import_react4.useRef)(null);
903
+ const scrolled = (0, import_react4.useRef)(false);
831
904
  const onTouchStart = (e) => {
832
905
  const t = e.touches[0];
833
906
  start.current = { x: t.clientX, y: t.clientY };
@@ -929,10 +1002,10 @@ function Toaster(props) {
929
1002
 
930
1003
  // src/components/app-shell.tsx
931
1004
  var import_lucide_react6 = require("lucide-react");
932
- var import_react6 = require("react");
1005
+ var import_react7 = require("react");
933
1006
 
934
1007
  // src/components/sidebar-nav.tsx
935
- var import_react4 = require("react");
1008
+ var import_react5 = require("react");
936
1009
 
937
1010
  // src/components/types.tsx
938
1011
  var import_jsx_runtime22 = require("react/jsx-runtime");
@@ -971,14 +1044,14 @@ function SidebarNav({
971
1044
  !isCollapsed && /* @__PURE__ */ (0, import_jsx_runtime23.jsx)("span", { className: "truncate", children: item.label })
972
1045
  ] })
973
1046
  });
974
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react4.Fragment, { children: link }, item.href);
1047
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(import_react5.Fragment, { children: link }, item.href);
975
1048
  }) });
976
1049
  }
977
1050
 
978
1051
  // src/components/user-menu.tsx
979
1052
  var DropdownMenu2 = __toESM(require("@radix-ui/react-dropdown-menu"), 1);
980
1053
  var import_lucide_react5 = require("lucide-react");
981
- var import_react5 = require("react");
1054
+ var import_react6 = require("react");
982
1055
  var import_vaul2 = require("vaul");
983
1056
  var import_jsx_runtime24 = require("react/jsx-runtime");
984
1057
  function initials(user) {
@@ -999,7 +1072,7 @@ function UserMenu({
999
1072
  align = "end"
1000
1073
  }) {
1001
1074
  const isDesktop = useIsDesktop();
1002
- const [open, setOpen] = (0, import_react5.useState)(false);
1075
+ const [open, setOpen] = (0, import_react6.useState)(false);
1003
1076
  const label = initials(user);
1004
1077
  const trigger = /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
1005
1078
  "button",
@@ -1164,8 +1237,8 @@ function AppShell({
1164
1237
  defaultCollapsed = false,
1165
1238
  children
1166
1239
  }) {
1167
- const [collapsed, setCollapsed] = (0, import_react6.useState)(defaultCollapsed);
1168
- const [mobileOpen, setMobileOpen] = (0, import_react6.useState)(false);
1240
+ const [collapsed, setCollapsed] = (0, import_react7.useState)(defaultCollapsed);
1241
+ const [mobileOpen, setMobileOpen] = (0, import_react7.useState)(false);
1169
1242
  const nav = (isCollapsed, onNavigate) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
1170
1243
  SidebarNav,
1171
1244
  {
@@ -1254,7 +1327,7 @@ function AppShell({
1254
1327
  // src/components/row-actions-menu.tsx
1255
1328
  var DropdownMenu3 = __toESM(require("@radix-ui/react-dropdown-menu"), 1);
1256
1329
  var import_lucide_react7 = require("lucide-react");
1257
- var import_react7 = require("react");
1330
+ var import_react8 = require("react");
1258
1331
  var import_vaul3 = require("vaul");
1259
1332
  var import_jsx_runtime26 = require("react/jsx-runtime");
1260
1333
  var triggerClass = "inline-flex h-8 w-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50";
@@ -1268,7 +1341,7 @@ function RowActionsMenu({
1268
1341
  renderLink = defaultRenderLink
1269
1342
  }) {
1270
1343
  const isDesktop = useIsDesktop();
1271
- const [open, setOpen] = (0, import_react7.useState)(false);
1344
+ const [open, setOpen] = (0, import_react8.useState)(false);
1272
1345
  if (actions.length === 0) return null;
1273
1346
  const trigger = /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
1274
1347
  "button",
@@ -1385,20 +1458,20 @@ function extractApiError(err, fallbackMessage = "Ocorreu um erro inesperado.") {
1385
1458
  }
1386
1459
 
1387
1460
  // src/hooks/use-disclosure.ts
1388
- var import_react8 = require("react");
1461
+ var import_react9 = require("react");
1389
1462
  function useDisclosure(initial = false) {
1390
- const [open, setOpen] = (0, import_react8.useState)(initial);
1391
- const onOpen = (0, import_react8.useCallback)(() => setOpen(true), []);
1392
- const onClose = (0, import_react8.useCallback)(() => setOpen(false), []);
1393
- const onToggle = (0, import_react8.useCallback)(() => setOpen((v) => !v), []);
1463
+ const [open, setOpen] = (0, import_react9.useState)(initial);
1464
+ const onOpen = (0, import_react9.useCallback)(() => setOpen(true), []);
1465
+ const onClose = (0, import_react9.useCallback)(() => setOpen(false), []);
1466
+ const onToggle = (0, import_react9.useCallback)(() => setOpen((v) => !v), []);
1394
1467
  return { open, onOpen, onClose, onToggle, setOpen };
1395
1468
  }
1396
1469
 
1397
1470
  // src/hooks/use-toast.ts
1398
1471
  var import_sonner2 = require("sonner");
1399
- var import_react9 = require("react");
1472
+ var import_react10 = require("react");
1400
1473
  function useCustomToast() {
1401
- const showToast = (0, import_react9.useCallback)(
1474
+ const showToast = (0, import_react10.useCallback)(
1402
1475
  (title, description, status = "success") => {
1403
1476
  switch (status) {
1404
1477
  case "success":
@@ -1447,10 +1520,13 @@ function useCustomToast() {
1447
1520
  ThemeProvider,
1448
1521
  Toaster,
1449
1522
  UserMenu,
1523
+ applyCompanyTheme,
1450
1524
  cn,
1451
1525
  defaultRenderLink,
1452
1526
  extractApiError,
1527
+ getSubdomain,
1453
1528
  toast,
1529
+ useCompanySubdomain,
1454
1530
  useCustomToast,
1455
1531
  useDisclosure,
1456
1532
  useIsDesktop,