@umituz/web-dashboard 3.2.2 → 4.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 (125) hide show
  1. package/README.md +202 -14
  2. package/package.json +21 -29
  3. package/src/domains/analytics/components/AnalyticsCard.tsx +23 -21
  4. package/src/domains/analytics/components/AnalyticsChart.tsx +345 -149
  5. package/src/domains/analytics/components/AnalyticsLayout.tsx +93 -112
  6. package/src/domains/analytics/components/MetricCard.tsx +42 -64
  7. package/src/domains/analytics/hooks/index.ts +7 -1
  8. package/src/domains/analytics/hooks/useAnalytics.ts +144 -139
  9. package/src/domains/analytics/index.ts +11 -0
  10. package/src/domains/analytics/services/AnalyticsEngineService.ts +103 -55
  11. package/src/domains/analytics/services/PerformanceService.ts +117 -69
  12. package/src/domains/analytics/types/analytics.ts +20 -6
  13. package/src/domains/analytics/types/index.ts +3 -0
  14. package/src/domains/analytics/utils/analytics.test.ts +183 -0
  15. package/src/domains/analytics/utils/analytics.ts +26 -9
  16. package/src/domains/analytics/utils/i18nKeys.ts +40 -0
  17. package/src/domains/analytics/utils/index.ts +2 -0
  18. package/src/domains/auth/components/AuthLayout.tsx +63 -54
  19. package/src/domains/auth/components/ForgotPasswordForm.tsx +96 -98
  20. package/src/domains/auth/components/LoginForm.tsx +167 -226
  21. package/src/domains/auth/components/RegisterForm.tsx +202 -272
  22. package/src/domains/auth/components/ResetPasswordForm.tsx +105 -151
  23. package/src/domains/auth/components/shared/AuthErrorBanner.tsx +35 -0
  24. package/src/domains/auth/components/shared/EmailInput.tsx +91 -0
  25. package/src/domains/auth/components/shared/PasswordInput.tsx +119 -0
  26. package/src/domains/auth/components/shared/PasswordStrengthIndicator.tsx +75 -0
  27. package/src/domains/auth/components/shared/SocialLoginButtons.tsx +58 -0
  28. package/src/domains/auth/components/shared/SocialProviderIcons.tsx +48 -0
  29. package/src/domains/auth/components/shared/index.ts +20 -0
  30. package/src/domains/auth/hooks/index.ts +1 -0
  31. package/src/domains/auth/hooks/useAuth.ts +151 -232
  32. package/src/domains/auth/index.ts +8 -4
  33. package/src/domains/auth/types/auth.ts +26 -5
  34. package/src/domains/auth/types/index.ts +1 -0
  35. package/src/domains/auth/utils/auth.test.ts +182 -0
  36. package/src/domains/auth/utils/auth.ts +71 -130
  37. package/src/domains/auth/utils/i18nKeys.ts +127 -0
  38. package/src/domains/auth/utils/index.ts +7 -1
  39. package/src/domains/auth/utils/secureToken.ts +33 -0
  40. package/src/domains/auth/utils/validationKeys.ts +23 -0
  41. package/src/domains/calendar/hooks/useCalendar.ts +145 -151
  42. package/src/domains/calendar/index.ts +0 -1
  43. package/src/domains/calendar/services/CalendarService.ts +122 -179
  44. package/src/domains/calendar/utils/contentItemMapper.test.ts +108 -0
  45. package/src/domains/calendar/utils/contentItemMapper.ts +99 -0
  46. package/src/domains/calendar/utils/index.ts +33 -30
  47. package/src/domains/layouts/components/BrandLogo.tsx +12 -9
  48. package/src/domains/layouts/components/DashboardHeader.tsx +78 -80
  49. package/src/domains/layouts/components/DashboardLayout.tsx +45 -21
  50. package/src/domains/layouts/components/DashboardSidebar.tsx +48 -51
  51. package/src/domains/layouts/types/layout.ts +7 -1
  52. package/src/domains/layouts/types/user.ts +3 -1
  53. package/src/domains/modules/build.test.tsx +97 -0
  54. package/src/domains/modules/build.tsx +71 -0
  55. package/src/domains/modules/index.ts +2 -0
  56. package/src/domains/modules/types.ts +29 -0
  57. package/src/domains/onboarding/components/AppFocusStep.tsx +99 -68
  58. package/src/domains/onboarding/components/OnboardingWizard.tsx +109 -183
  59. package/src/domains/onboarding/components/PlanStep.tsx +156 -123
  60. package/src/domains/onboarding/components/PlatformsStep.tsx +74 -54
  61. package/src/domains/onboarding/components/StepNavigation.tsx +108 -0
  62. package/src/domains/onboarding/components/StepProgress.tsx +62 -0
  63. package/src/domains/onboarding/components/UserTypeStep.tsx +59 -80
  64. package/src/domains/onboarding/components/index.ts +4 -0
  65. package/src/domains/onboarding/hooks/useOnboarding.ts +76 -66
  66. package/src/domains/onboarding/index.ts +1 -0
  67. package/src/domains/onboarding/types/onboarding.ts +57 -17
  68. package/src/domains/onboarding/utils/i18nKeys.ts +72 -0
  69. package/src/domains/onboarding/utils/index.ts +2 -0
  70. package/src/domains/permissions/PermissionsContext.tsx +103 -0
  71. package/src/domains/permissions/hasPermission.test.ts +50 -0
  72. package/src/domains/permissions/hasPermission.ts +38 -0
  73. package/src/domains/permissions/index.ts +19 -0
  74. package/src/domains/permissions/types.ts +35 -0
  75. package/src/domains/permissions/utils/i18nKeys.ts +11 -0
  76. package/src/domains/session/DemoLogin.tsx +50 -0
  77. package/src/domains/session/OrgSwitcher.tsx +110 -0
  78. package/src/domains/session/SessionContext.tsx +150 -0
  79. package/src/domains/session/inMemoryAuthAdapter.ts +53 -0
  80. package/src/domains/session/index.ts +18 -0
  81. package/src/domains/session/types.ts +47 -0
  82. package/src/domains/session/utils/i18nKeys.ts +12 -0
  83. package/src/domains/settings/components/AuditLogList.tsx +59 -0
  84. package/src/domains/settings/components/PermissionMatrixScreen.tsx +80 -0
  85. package/src/domains/settings/components/SettingsLayout.tsx +75 -60
  86. package/src/domains/settings/components/SettingsSection.tsx +35 -22
  87. package/src/domains/settings/components/UserManagementTable.tsx +87 -0
  88. package/src/domains/settings/components/index.ts +10 -0
  89. package/src/domains/settings/hooks/useSettings.ts +77 -52
  90. package/src/domains/settings/types/settings.ts +2 -0
  91. package/src/domains/settings/utils/i18nKeys.ts +18 -0
  92. package/src/domains/settings/utils/settings.ts +11 -4
  93. package/src/i18n/I18nContext.tsx +33 -0
  94. package/src/i18n/defaultT.test.ts +43 -0
  95. package/src/i18n/defaultT.ts +34 -0
  96. package/src/i18n/index.ts +7 -0
  97. package/src/i18n/types.ts +13 -0
  98. package/src/index.ts +1 -7
  99. package/src/primitives/Button.tsx +30 -0
  100. package/src/primitives/Checkbox.tsx +40 -0
  101. package/src/primitives/Input.tsx +19 -0
  102. package/src/primitives/Skeleton.tsx +26 -0
  103. package/src/primitives/cn.ts +15 -0
  104. package/src/primitives/index.ts +23 -0
  105. package/src/styles/dashboard.css +3045 -0
  106. package/src/styles/tokens.contract.test.ts +66 -0
  107. package/src/styles/tokens.css +124 -0
  108. package/src/domains/billing/components/BillingLayout.tsx +0 -63
  109. package/src/domains/billing/components/BillingPage.tsx +0 -49
  110. package/src/domains/billing/components/BillingPortal.tsx +0 -198
  111. package/src/domains/billing/components/InvoiceCard.tsx +0 -143
  112. package/src/domains/billing/components/PaymentMethodsList.tsx +0 -116
  113. package/src/domains/billing/components/PlanComparison.tsx +0 -199
  114. package/src/domains/billing/components/UsageCard.tsx +0 -103
  115. package/src/domains/billing/components/index.ts +0 -13
  116. package/src/domains/billing/constants/billing.ts +0 -69
  117. package/src/domains/billing/hooks/index.ts +0 -7
  118. package/src/domains/billing/hooks/useBilling.ts +0 -385
  119. package/src/domains/billing/index.ts +0 -73
  120. package/src/domains/billing/types/billing.ts +0 -342
  121. package/src/domains/billing/types/index.ts +0 -30
  122. package/src/domains/billing/utils/billing.ts +0 -360
  123. package/src/domains/billing/utils/index.ts +0 -31
  124. package/src/domains/billing/utils/polar.ts +0 -63
  125. package/src/domains/calendar/components/index.ts +0 -7
@@ -4,3 +4,13 @@
4
4
 
5
5
  export { SettingsLayout } from './SettingsLayout';
6
6
  export { SettingsSection } from './SettingsSection';
7
+ export {
8
+ UserManagementTable,
9
+ type UserManagementTableProps,
10
+ type ManagedUser,
11
+ } from './UserManagementTable';
12
+ export {
13
+ PermissionMatrixScreen,
14
+ type PermissionMatrixScreenProps,
15
+ } from './PermissionMatrixScreen';
16
+ export { AuditLogList, type AuditLogListProps, type AuditEntry } from './AuditLogList';
@@ -1,75 +1,100 @@
1
1
  /**
2
2
  * Settings Hooks
3
3
  *
4
- * Custom React hooks for settings functionality
4
+ * Custom React hooks for settings functionality.
5
+ * Uses a single `useReducer` so the three update paths share one
6
+ * well-tested tree-walking helper instead of duplicating it.
5
7
  */
6
8
 
7
- import { useState, useCallback } from "react";
9
+ import { useReducer, useCallback } from "react";
8
10
  import type { SettingsConfig, SettingsItem } from "../types/settings";
9
11
 
12
+ /**
13
+ * Action discriminated union for the settings reducer.
14
+ */
15
+ type SettingsAction =
16
+ | { type: 'updateItem'; sectionKey: string; itemKey: string; updates: Partial<SettingsItem> }
17
+ | { type: 'toggleItem'; sectionKey: string; itemKey: string }
18
+ | { type: 'setItemBadge'; sectionKey: string; itemKey: string; badge: number | undefined };
19
+
20
+ /**
21
+ * Apply a transform to the item identified by (sectionKey, itemKey).
22
+ * Single source of truth for the tree-walking logic.
23
+ */
24
+ const mapItemInTree = (
25
+ config: SettingsConfig,
26
+ sectionKey: string,
27
+ itemKey: string,
28
+ transform: (item: SettingsItem) => SettingsItem,
29
+ ): SettingsConfig => ({
30
+ ...config,
31
+ sections: config.sections.map((section) =>
32
+ section.key !== sectionKey
33
+ ? section
34
+ : {
35
+ ...section,
36
+ items: section.items.map((item) => (item.key === itemKey ? transform(item) : item)),
37
+ },
38
+ ),
39
+ });
40
+
41
+ const settingsReducer = (state: SettingsConfig, action: SettingsAction): SettingsConfig => {
42
+ switch (action.type) {
43
+ case 'updateItem':
44
+ return mapItemInTree(state, action.sectionKey, action.itemKey, (item) => ({
45
+ ...item,
46
+ ...action.updates,
47
+ }));
48
+ case 'toggleItem':
49
+ return mapItemInTree(state, action.sectionKey, action.itemKey, (item) => ({
50
+ ...item,
51
+ // Tri-state semantics: undefined is treated as enabled by default.
52
+ enabled: item.enabled === undefined ? false : !item.enabled,
53
+ }));
54
+ case 'setItemBadge':
55
+ return mapItemInTree(state, action.sectionKey, action.itemKey, (item) => ({
56
+ ...item,
57
+ badge: action.badge,
58
+ }));
59
+ }
60
+ };
61
+
62
+ /**
63
+ * Public surface of the hook. Returned callbacks are stable
64
+ * (no deps that change between renders).
65
+ */
66
+ export interface UseSettingsReturn {
67
+ config: SettingsConfig;
68
+ updateItem: (sectionKey: string, itemKey: string, updates: Partial<SettingsItem>) => void;
69
+ toggleItem: (sectionKey: string, itemKey: string) => void;
70
+ setItemBadge: (sectionKey: string, itemKey: string, badge: number | undefined) => void;
71
+ }
72
+
10
73
  /**
11
74
  * Use Settings Hook
12
75
  *
13
- * Manages settings state and actions
14
- *
15
- * @param initialConfig - Initial settings configuration
16
- * @returns Settings state and actions
76
+ * Manages settings configuration state and provides update actions.
17
77
  */
18
- export function useSettings(initialConfig: SettingsConfig) {
19
- const [config, setConfig] = useState<SettingsConfig>(initialConfig);
78
+ export function useSettings(initialConfig: SettingsConfig): UseSettingsReturn {
79
+ const [config, dispatch] = useReducer(settingsReducer, initialConfig);
20
80
 
21
81
  const updateItem = useCallback(
22
82
  (sectionKey: string, itemKey: string, updates: Partial<SettingsItem>) => {
23
- setConfig((prev) => ({
24
- ...prev,
25
- sections: prev.sections.map((section) =>
26
- section.key === sectionKey
27
- ? {
28
- ...section,
29
- items: section.items.map((item) =>
30
- item.key === itemKey ? { ...item, ...updates } : item
31
- ),
32
- }
33
- : section
34
- ),
35
- }));
83
+ dispatch({ type: 'updateItem', sectionKey, itemKey, updates });
36
84
  },
37
- []
85
+ [],
38
86
  );
39
87
 
40
88
  const toggleItem = useCallback((sectionKey: string, itemKey: string) => {
41
- setConfig((prev) => ({
42
- ...prev,
43
- sections: prev.sections.map((section) =>
44
- section.key === sectionKey
45
- ? {
46
- ...section,
47
- items: section.items.map((item) =>
48
- item.key === itemKey
49
- ? { ...item, enabled: item.enabled === undefined ? true : !item.enabled }
50
- : item
51
- ),
52
- }
53
- : section
54
- ),
55
- }));
89
+ dispatch({ type: 'toggleItem', sectionKey, itemKey });
56
90
  }, []);
57
91
 
58
- const setItemBadge = useCallback((sectionKey: string, itemKey: string, badge: number | undefined) => {
59
- setConfig((prev) => ({
60
- ...prev,
61
- sections: prev.sections.map((section) =>
62
- section.key === sectionKey
63
- ? {
64
- ...section,
65
- items: section.items.map((item) =>
66
- item.key === itemKey ? { ...item, badge } : item
67
- ),
68
- }
69
- : section
70
- ),
71
- }));
72
- }, []);
92
+ const setItemBadge = useCallback(
93
+ (sectionKey: string, itemKey: string, badge: number | undefined) => {
94
+ dispatch({ type: 'setItemBadge', sectionKey, itemKey, badge });
95
+ },
96
+ [],
97
+ );
73
98
 
74
99
  return {
75
100
  config,
@@ -20,6 +20,8 @@ export interface SettingsItem {
20
20
  icon?: LucideIcon;
21
21
  /** Route path for this setting */
22
22
  path?: string;
23
+ /** Click handler when no route is available */
24
+ onClick?: () => void;
23
25
  /** Whether this item is enabled */
24
26
  enabled?: boolean;
25
27
  /** Required permission to access */
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Settings domain i18n keys (admin skeleton screens) — the
3
+ * documented string contract. Column titles and role labels come
4
+ * from props; only empty/denied states fall back to these keys.
5
+ */
6
+
7
+ export const SETTINGS_ADMIN_KEYS = {
8
+ users: {
9
+ empty: "settings.users.empty",
10
+ },
11
+ matrix: {
12
+ granted: "settings.matrix.granted",
13
+ denied: "settings.matrix.denied",
14
+ },
15
+ audit: {
16
+ empty: "settings.audit.empty",
17
+ },
18
+ } as const;
@@ -51,13 +51,20 @@ export function filterSettingsByPermission<T extends SettingsItem>(
51
51
  });
52
52
  }
53
53
 
54
+ /**
55
+ * Fallback route when no default or first available route can be resolved.
56
+ * Caller should treat this as an explicit "no route configured" signal
57
+ * rather than a silent success.
58
+ */
59
+ export const SETTINGS_FALLBACK_ROUTE = "/settings";
60
+
54
61
  /**
55
62
  * Get default route from settings config
56
63
  *
57
- * @param config - Settings configuration
58
- * @returns Default route or first available route
64
+ * Returns null when no default route is resolvable. The caller is
65
+ * responsible for handling that case (e.g. redirect, error).
59
66
  */
60
- export function getSettingsDefaultRoute(config: SettingsConfig): string {
67
+ export function getSettingsDefaultRoute(config: SettingsConfig): string | null {
61
68
  if (config.defaultRoute) return config.defaultRoute;
62
69
 
63
70
  for (const section of config.sections) {
@@ -65,7 +72,7 @@ export function getSettingsDefaultRoute(config: SettingsConfig): string {
65
72
  if (firstEnabled?.path) return firstEnabled.path;
66
73
  }
67
74
 
68
- return "/settings";
75
+ return null;
69
76
  }
70
77
 
71
78
  /**
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Dashboard i18n injection.
3
+ *
4
+ * The package ships no i18n runtime. Hosts that have one wrap their
5
+ * app once: `<DashboardI18nProvider t={i18n.t}>` — the signature
6
+ * already matches i18next (and the fleet's custom `t()` helpers).
7
+ * Without a provider, `useT()` falls back to `defaultT`, which
8
+ * humanizes catalog keys into readable English.
9
+ *
10
+ * Components consume only `useT()`; the key catalogs (i18nKeys.ts)
11
+ * remain the documented string contract.
12
+ */
13
+
14
+ import { createContext, useContext, type ReactNode } from "react";
15
+ import { defaultT } from "./defaultT";
16
+ import type { TranslateFn } from "./types";
17
+
18
+ export interface DashboardI18nProviderProps {
19
+ /** Host translation function (e.g. i18next's `t`). */
20
+ t: TranslateFn;
21
+ children: ReactNode;
22
+ }
23
+
24
+ const I18nContext = createContext<TranslateFn | null>(null);
25
+
26
+ export const DashboardI18nProvider = ({
27
+ t,
28
+ children,
29
+ }: DashboardI18nProviderProps) => (
30
+ <I18nContext.Provider value={t}>{children}</I18nContext.Provider>
31
+ );
32
+
33
+ export const useT = (): TranslateFn => useContext(I18nContext) ?? defaultT;
@@ -0,0 +1,43 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { defaultT } from "./defaultT";
3
+
4
+ describe("defaultT", () => {
5
+ it("humanizes the last key segment", () => {
6
+ expect(defaultT("auth.login.welcomeBack")).toBe("Welcome back");
7
+ });
8
+
9
+ it("splits camelCase into words", () => {
10
+ expect(defaultT("auth.passwordStrength.passwordsDoNotMatch")).toBe(
11
+ "Passwords do not match",
12
+ );
13
+ });
14
+
15
+ it("splits snake_case and kebab-case segments", () => {
16
+ expect(defaultT("common.error_code")).toBe("Error code");
17
+ expect(defaultT("common.error-code")).toBe("Error code");
18
+ });
19
+
20
+ it("keeps digits attached to their word", () => {
21
+ expect(defaultT("analytics.common.last7Days")).toBe("Last7 days");
22
+ });
23
+
24
+ it("interpolates {{param}} placeholders the text carries", () => {
25
+ expect(defaultT("custom.{{brand}} console", { brand: "Acme" })).toBe(
26
+ "Acme console",
27
+ );
28
+ });
29
+
30
+ it("ignores params the humanized text cannot place", () => {
31
+ expect(defaultT("auth.login.signInTo", { brand: "Acme" })).toBe(
32
+ "Sign in to",
33
+ );
34
+ });
35
+
36
+ it("returns the raw key when it has no segments", () => {
37
+ expect(defaultT("")).toBe("");
38
+ });
39
+
40
+ it("handles single-segment keys", () => {
41
+ expect(defaultT("settings")).toBe("Settings");
42
+ });
43
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Default translator — the zero-config fallback when no host i18n
3
+ * is provided. Humanizes catalog keys into sentence case
4
+ * ("auth.login.welcomeBack" → "Welcome back") and interpolates
5
+ * `{{param}}` placeholders when the humanized text carries one.
6
+ * Params the text cannot place are ignored — real interpolation is
7
+ * the host runtime's job via the DashboardI18nProvider bridge.
8
+ */
9
+
10
+ import type { TranslateFn } from "./types";
11
+
12
+ /** Split a key segment into words: camelCase, snake_case, kebab-case. */
13
+ const WORD_SPLIT = /[._\-]+|(?<=[a-z0-9])(?=[A-Z])/g;
14
+
15
+ const humanizeSegment = (segment: string): string => {
16
+ const words = segment.split(WORD_SPLIT).filter(Boolean);
17
+ if (words.length === 0) return segment;
18
+ const sentence = words.join(" ").toLowerCase();
19
+ return sentence.charAt(0).toUpperCase() + sentence.slice(1);
20
+ };
21
+
22
+ /** Replace `{{param}}` placeholders with param values. */
23
+ const interpolate = (text: string, params?: Record<string, unknown>): string => {
24
+ if (!params) return text;
25
+ return text.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, name: string) =>
26
+ name in params ? String(params[name]) : match,
27
+ );
28
+ };
29
+
30
+ export const defaultT: TranslateFn = (key, params) => {
31
+ const segments = key.split(".").filter(Boolean);
32
+ const source = segments.length > 0 ? segments[segments.length - 1] : key;
33
+ return interpolate(humanizeSegment(source), params);
34
+ };
@@ -0,0 +1,7 @@
1
+ export {
2
+ DashboardI18nProvider,
3
+ useT,
4
+ type DashboardI18nProviderProps,
5
+ } from "./I18nContext";
6
+ export { defaultT } from "./defaultT";
7
+ export type { TranslateFn } from "./types";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Translation function contract.
3
+ *
4
+ * Keyed by the package's i18n catalog constants (e.g. AUTH_KEYS);
5
+ * `params` feeds `{{placeholder}}` interpolation. The signature is
6
+ * deliberately compatible with i18next's `t` so hosts can bridge
7
+ * their own setup: `<DashboardI18nProvider t={i18n.t}>`.
8
+ */
9
+
10
+ export type TranslateFn = (
11
+ key: string,
12
+ params?: Record<string, unknown>,
13
+ ) => string;
package/src/index.ts CHANGED
@@ -7,15 +7,9 @@
7
7
  * @version 3.0.0
8
8
  */
9
9
 
10
- // Export all domains
10
+ // Core domains with complete public API
11
11
  export * from './domains/layouts';
12
12
  export * from './domains/settings';
13
- export * from './domains/billing';
14
-
15
- // Domains - using selective exports to avoid conflicts
16
- export * from './domains/layouts';
17
- export * from './domains/settings';
18
- export * from './domains/billing';
19
13
  export * from './domains/calendar';
20
14
  export {
21
15
  OnboardingWizard,
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Button — camp-agnostic action button (`udb-btn`).
3
+ *
4
+ * Clones only the surface this package actually uses: variants
5
+ * default/destructive/outline/ghost, sizes default/sm/icon. No `asChild`
6
+ * (radix-free), no variant library — classes come from `styles.css`.
7
+ */
8
+
9
+ import { forwardRef, type ButtonHTMLAttributes } from 'react';
10
+ import { cn } from './cn';
11
+
12
+ export type ButtonVariant = 'default' | 'destructive' | 'outline' | 'ghost';
13
+ export type ButtonSize = 'default' | 'sm' | 'icon';
14
+
15
+ export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
16
+ variant?: ButtonVariant;
17
+ size?: ButtonSize;
18
+ }
19
+
20
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
21
+ ({ className, variant = 'default', size = 'default', ...props }, ref) => (
22
+ <button
23
+ ref={ref}
24
+ className={cn('udb-btn', `udb-btn--${variant}`, `udb-btn--${size}`, className)}
25
+ {...props}
26
+ />
27
+ ),
28
+ );
29
+
30
+ Button.displayName = 'Button';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Checkbox — native checkbox with a boolean-change callback (`udb-checkbox`).
3
+ *
4
+ * Keeps the `onCheckedChange` convenience this package relies on; every
5
+ * other native attribute passes straight through.
6
+ */
7
+
8
+ import { forwardRef, useCallback, type InputHTMLAttributes } from 'react';
9
+ import { cn } from './cn';
10
+
11
+ export interface CheckboxProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'> {
12
+ checked?: boolean;
13
+ onCheckedChange?: (checked: boolean) => void;
14
+ }
15
+
16
+ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
17
+ ({ className, checked, onCheckedChange, onChange, ...props }, ref) => {
18
+ // Memoized so parent re-renders don't recreate the handler identity.
19
+ const handleChange = useCallback(
20
+ (e: React.ChangeEvent<HTMLInputElement>) => {
21
+ onCheckedChange?.(e.target.checked);
22
+ onChange?.(e);
23
+ },
24
+ [onCheckedChange, onChange],
25
+ );
26
+
27
+ return (
28
+ <input
29
+ ref={ref}
30
+ type="checkbox"
31
+ checked={checked}
32
+ onChange={handleChange}
33
+ className={cn('udb-checkbox', className)}
34
+ {...props}
35
+ />
36
+ );
37
+ },
38
+ );
39
+
40
+ Checkbox.displayName = 'Checkbox';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Input — text input field (`udb-input`).
3
+ *
4
+ * Minimal clone of the surface this package uses: a native input with
5
+ * token-driven styling. Everything else passes through untouched.
6
+ */
7
+
8
+ import { forwardRef, type InputHTMLAttributes } from 'react';
9
+ import { cn } from './cn';
10
+
11
+ export type InputProps = InputHTMLAttributes<HTMLInputElement>;
12
+
13
+ export const Input = forwardRef<HTMLInputElement, InputProps>(
14
+ ({ className, type = 'text', ...props }, ref) => (
15
+ <input ref={ref} type={type} className={cn('udb-input', className)} {...props} />
16
+ ),
17
+ );
18
+
19
+ Input.displayName = 'Input';
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Skeleton — loading placeholder (`udb-skeleton`).
3
+ *
4
+ * The pulse animation is defined in `styles.css` (a CSS file can ship
5
+ * keyframes; inline styles cannot).
6
+ */
7
+
8
+ import { forwardRef, type HTMLAttributes } from 'react';
9
+ import { cn } from './cn';
10
+
11
+ export interface SkeletonProps extends HTMLAttributes<HTMLDivElement> {
12
+ variant?: 'text' | 'circular' | 'rectangular';
13
+ }
14
+
15
+ export const Skeleton = forwardRef<HTMLDivElement, SkeletonProps>(
16
+ ({ className, variant = 'rectangular', ...props }, ref) => (
17
+ <div
18
+ ref={ref}
19
+ className={cn('udb-skeleton', `udb-skeleton--${variant}`, className)}
20
+ role="status"
21
+ {...props}
22
+ />
23
+ ),
24
+ );
25
+
26
+ Skeleton.displayName = 'Skeleton';
@@ -0,0 +1,15 @@
1
+ /**
2
+ * cn — conditional className join (clsx only).
3
+ *
4
+ * Deliberately drops the old tailwind-merge layer: v4 ships its own
5
+ * `udb-*` class vocabulary where overrides append instead of conflicting,
6
+ * so plain conditional joining is the whole job.
7
+ */
8
+
9
+ import { clsx, type ClassValue } from 'clsx';
10
+
11
+ export type { ClassValue };
12
+
13
+ export function cn(...inputs: ClassValue[]): string {
14
+ return clsx(inputs);
15
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Local primitives — the package's only UI atoms.
3
+ *
4
+ * Replaces the `@umituz/web-design-system` dependency (v3) with a
5
+ * camp-agnostic set: `udb-*` classes + `--u-*` tokens, no utility
6
+ * framework, no cva, no radix. Import as `@umituz/web-dashboard/primitives`
7
+ * or relatively from inside the package.
8
+ */
9
+
10
+ export { cn } from './cn';
11
+ export type { ClassValue } from './cn';
12
+
13
+ export { Button } from './Button';
14
+ export type { ButtonProps, ButtonVariant, ButtonSize } from './Button';
15
+
16
+ export { Input } from './Input';
17
+ export type { InputProps } from './Input';
18
+
19
+ export { Checkbox } from './Checkbox';
20
+ export type { CheckboxProps } from './Checkbox';
21
+
22
+ export { Skeleton } from './Skeleton';
23
+ export type { SkeletonProps } from './Skeleton';