@liam-public/browser-react-cms 0.1.0 → 0.2.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
@@ -1,37 +1,59 @@
1
1
  # @liam-public/browser-react-cms
2
2
 
3
- Reusable CMS/admin **chrome** on top of `@liam-public/browser-react-ui` the parts every
4
- admin app reinvents: a responsive app-shell, auth route-guards, and page primitives.
5
-
6
- ## Exports
7
-
8
- - **`CmsLayout`** responsive shell (desktop sidebar + mobile hamburger sheet) driven by a
9
- `nav: NavItem[]` prop (`{ to, label, icon, end?, badge? }`), with `brand` and `headerRight`.
10
- - **`RequireAuth`** / **`RequireRole`** route guards over `useAuth()` from
11
- `@liam-public/browser-react-auth` (the guard `browser-react-auth` doesn't ship). `RequireAuth`
12
- takes `onUnauthenticated` (e.g. trigger the OIDC redirect) + a `fallback`; `RequireRole` takes
13
- `anyOf` roles + a `getRoles(user)` extractor.
14
- - **`PageHeader`**, **`PageStack`** page title row + vertical section spacing.
15
- - **`DesktopOnly`/`MobileCards`/`MobileCard`/`MobileField`** responsive table/card primitives.
16
- - **`LoadingSpinner`**, **`ConfirmDialog`** common feedback components.
3
+ Batteries-included CMS/admin framework on top of `@liam-public/browser-react-ui` (Tailwind v4 +
4
+ Radix). Owns the cross-cutting concerns every admin app reinvents.
5
+
6
+ ## What's included
7
+
8
+ | Area | Exports |
9
+ | --- | --- |
10
+ | **App shell** | `CmsLayout` (sidebar + mobile sheet, `nav`/`brand`/`headerRight`), `PageHeader`, `PageStack`, responsive `DesktopOnly`/`MobileCards`/`MobileCard`/`MobileField` |
11
+ | **Auth & authz** | `RequireAuth`, `RequireRole` (over `browser-react-auth`); `AuthProvider`/`useAuth`/`createAuthenticatedFetch` re-exported |
12
+ | **Dark / light mode** | `ThemeProvider`, `useTheme`, `ThemeToggle` + a `.dark` variant in `theme.css` |
13
+ | **Notifications** | `ToastProvider`, `useToast()` (`toast`/`success`/`error`/`dismiss`) |
14
+ | **Validation / forms** | `ResourceForm` + `useResourceForm` (Zod + react-hook-form), `TextField`, `TextareaField` |
15
+ | **Data table + paging** | `DataTable` (column-driven, fed by `useList`), `Pagination` |
16
+ | **Dashboards** | `StatCard`, `StatGrid`, `ChartCard` + re-exported recharts primitives |
17
+ | **Analytics** | `AnalyticsProvider`, `useAnalytics`, `usePageViews`, `createBeaconAnalytics`/`createConsoleAnalytics` |
18
+ | **i18n** | `createI18n`, `I18nProvider`, `useTranslation`, `LanguageSwitcher` |
19
+ | **Date/number** | `formatDate`/`formatCurrency`/`formatNumber`/`formatPercentage`/`formatFileSize` (re-exported from `@liam-public/text`) |
20
+ | **Feedback** | `LoadingSpinner`, `ConfirmDialog` |
17
21
 
18
22
  ## Setup
19
23
 
20
- Peer deps: `react`, `react-dom`, `react-router-dom`. Run Tailwind v4 in your app and import the
21
- theme once:
24
+ Run Tailwind v4 in your app and import the theme once; peer deps are `react`, `react-dom`,
25
+ `react-router-dom`, `react-hook-form`, `zod`, `@tanstack/react-query`.
22
26
 
23
27
  ```css
24
- /* app.css */
25
28
  @import '@liam-public/browser-react-cms/theme.css';
26
29
  ```
27
30
 
28
31
  ```tsx
29
- import { CmsLayout, RequireAuth } from '@liam-public/browser-react-cms'
30
- import { LayoutDashboard } from 'lucide-react'
31
-
32
- <RequireAuth onUnauthenticated={() => authClient.signInRedirect()}>
33
- <CmsLayout brand="My App" nav={[{ to: '/', label: 'Dashboard', icon: LayoutDashboard, end: true }]}>
34
- <Routes>…</Routes>
35
- </CmsLayout>
36
- </RequireAuth>
32
+ <ThemeProvider>
33
+ <ToastProvider>
34
+ <AnalyticsProvider adapter={createBeaconAnalytics({ endpoint: '/ingest/analytics', app: 'my-cms' })}>
35
+ <QueryClientProvider client={qc}>
36
+ <DataProviderProvider provider={dataProvider}>
37
+ <BrowserRouter>
38
+ <RequireAuth onUnauthenticated={() => authClient.signInRedirect()}>
39
+ <CmsLayout brand="My App" nav={nav} headerRight={<ThemeToggle />}>
40
+ <Routes>…</Routes>
41
+ </CmsLayout>
42
+ </RequireAuth>
43
+ </BrowserRouter>
44
+ </DataProviderProvider>
45
+ </QueryClientProvider>
46
+ </AnalyticsProvider>
47
+ </ToastProvider>
48
+ </ThemeProvider>
49
+ ```
50
+
51
+ A Zod-validated CRUD form:
52
+
53
+ ```tsx
54
+ const schema = z.object({ name: z.string().min(1, 'Required'), url: z.string().url() })
55
+ <ResourceForm schema={schema} defaultValues={{ name: '', url: '' }} onSubmit={save}>
56
+ <TextField name="name" label="Name" />
57
+ <TextField name="url" label="Feed URL" />
58
+ </ResourceForm>
37
59
  ```
package/dist/index.d.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ComponentType } from 'react';
3
+ import { FieldValues, DefaultValues, SubmitHandler, Path, UseFormReturn } from 'react-hook-form';
4
+ import { ZodType } from 'zod';
5
+ import { ListParams } from '@liam-public/browser-react-data';
6
+ import { Resource, i18n } from 'i18next';
7
+ export { AuthContextValue, AuthProvider, createAuthenticatedFetch, useAuth } from '@liam-public/browser-react-auth';
8
+ export { LocalizedText, formatCurrency, formatDate, formatFileSize, formatNumber, formatPercentage, isLocalizedText, selectText } from '@liam-public/text';
9
+ export { Area, AreaChart, Bar, BarChart, CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
10
+ export { useTranslation } from 'react-i18next';
3
11
 
4
12
  /** A single sidebar navigation entry. `icon` is any lucide-react (or compatible) icon. */
5
13
  interface NavItem {
@@ -29,33 +37,6 @@ interface CmsLayoutProps {
29
37
  */
30
38
  declare function CmsLayout({ children, nav, brand, headerRight }: CmsLayoutProps): react.JSX.Element;
31
39
 
32
- interface RequireAuthProps {
33
- readonly children: ReactNode;
34
- /** Rendered while loading or when unauthenticated (defaults to a "Redirecting…" line). */
35
- readonly fallback?: ReactNode;
36
- /**
37
- * Called once when the user is confirmed unauthenticated — e.g. trigger the OIDC
38
- * redirect from `@liam-workspace/auth-client`, or navigate to `/login`.
39
- */
40
- readonly onUnauthenticated?: () => void;
41
- }
42
- /**
43
- * Gate that renders `children` only for an authenticated user. Wraps `useAuth()` from
44
- * `@liam-public/browser-react-auth`, filling the route-guard gap that package leaves open.
45
- */
46
- declare function RequireAuth({ children, fallback, onUnauthenticated }: RequireAuthProps): react.JSX.Element;
47
- interface RequireRoleProps {
48
- readonly children: ReactNode;
49
- /** The user is allowed if they hold ANY of these roles. */
50
- readonly anyOf: readonly string[];
51
- /** Extract the user's roles from the auth `user` object (shape is app-specific). */
52
- readonly getRoles: (user: unknown) => readonly string[];
53
- /** Rendered when the user lacks the required role(s). */
54
- readonly fallback?: ReactNode;
55
- }
56
- /** Gate that additionally requires the authenticated user to hold one of `anyOf` roles. */
57
- declare function RequireRole({ children, anyOf, getRoles, fallback }: RequireRoleProps): react.JSX.Element;
58
-
59
40
  interface PageHeaderProps {
60
41
  readonly title: string;
61
42
  readonly action?: ReactNode;
@@ -108,4 +89,195 @@ interface ConfirmDialogProps {
108
89
  /** A modal confirm dialog with loading + error states. */
109
90
  declare function ConfirmDialog({ open, onOpenChange, title, description, onConfirm, loading, error, confirmLabel, destructive, }: ConfirmDialogProps): react.JSX.Element;
110
91
 
111
- export { CmsLayout, type CmsLayoutProps, ConfirmDialog, type ConfirmDialogProps, DesktopOnly, LoadingSpinner, type LoadingSpinnerProps, MobileCard, MobileCards, MobileField, type NavItem, PageHeader, type PageHeaderProps, PageStack, type PageStackProps, RequireAuth, type RequireAuthProps, RequireRole, type RequireRoleProps };
92
+ interface RequireAuthProps {
93
+ readonly children: ReactNode;
94
+ /** Rendered while loading or when unauthenticated (defaults to a "Redirecting…" line). */
95
+ readonly fallback?: ReactNode;
96
+ /**
97
+ * Called once when the user is confirmed unauthenticated — e.g. trigger the OIDC
98
+ * redirect from `@liam-workspace/auth-client`, or navigate to `/login`.
99
+ */
100
+ readonly onUnauthenticated?: () => void;
101
+ }
102
+ /**
103
+ * Gate that renders `children` only for an authenticated user. Wraps `useAuth()` from
104
+ * `@liam-public/browser-react-auth`, filling the route-guard gap that package leaves open.
105
+ */
106
+ declare function RequireAuth({ children, fallback, onUnauthenticated }: RequireAuthProps): react.JSX.Element;
107
+ interface RequireRoleProps {
108
+ readonly children: ReactNode;
109
+ /** The user is allowed if they hold ANY of these roles. */
110
+ readonly anyOf: readonly string[];
111
+ /** Extract the user's roles from the auth `user` object (shape is app-specific). */
112
+ readonly getRoles: (user: unknown) => readonly string[];
113
+ /** Rendered when the user lacks the required role(s). */
114
+ readonly fallback?: ReactNode;
115
+ }
116
+ /** Gate that additionally requires the authenticated user to hold one of `anyOf` roles. */
117
+ declare function RequireRole({ children, anyOf, getRoles, fallback }: RequireRoleProps): react.JSX.Element;
118
+
119
+ type Theme = 'light' | 'dark' | 'system';
120
+ interface ThemeContextValue {
121
+ readonly theme: Theme;
122
+ readonly resolved: 'light' | 'dark';
123
+ readonly setTheme: (theme: Theme) => void;
124
+ readonly toggle: () => void;
125
+ }
126
+ interface ThemeProviderProps {
127
+ readonly children: ReactNode;
128
+ readonly defaultTheme?: Theme;
129
+ readonly storageKey?: string;
130
+ }
131
+ /** Manages light/dark/system theme, persists the choice, and toggles `.dark` on `<html>`. */
132
+ declare function ThemeProvider({ children, defaultTheme, storageKey, }: ThemeProviderProps): react.JSX.Element;
133
+ declare function useTheme(): ThemeContextValue;
134
+ /** A button that toggles light/dark. */
135
+ declare function ThemeToggle(): react.JSX.Element;
136
+
137
+ type ToastVariant = 'default' | 'success' | 'error';
138
+ interface ToastOptions {
139
+ readonly title?: string;
140
+ readonly description?: string;
141
+ readonly variant?: ToastVariant;
142
+ /** Auto-dismiss after this many ms (default 5000; 0 disables). */
143
+ readonly duration?: number;
144
+ }
145
+ interface ToastContextValue {
146
+ toast: (options: ToastOptions) => number;
147
+ success: (title: string, description?: string) => number;
148
+ error: (title: string, description?: string) => number;
149
+ dismiss: (id: number) => void;
150
+ }
151
+ /** Wrap the app to enable `useToast()`. Renders the toast viewport itself. */
152
+ declare function ToastProvider({ children }: {
153
+ children: ReactNode;
154
+ }): react.JSX.Element;
155
+ declare function useToast(): ToastContextValue;
156
+
157
+ /** A react-hook-form instance validated by a Zod schema. */
158
+ declare function useResourceForm<T extends FieldValues>(schema: ZodType<T>, defaultValues?: DefaultValues<T>): UseFormReturn<T>;
159
+ interface ResourceFormProps<T extends FieldValues> {
160
+ readonly schema: ZodType<T>;
161
+ readonly defaultValues?: DefaultValues<T>;
162
+ readonly onSubmit: SubmitHandler<T>;
163
+ readonly children: ReactNode;
164
+ readonly submitLabel?: string;
165
+ /** Optional extra footer content (e.g. a Cancel button) rendered next to Submit. */
166
+ readonly footer?: ReactNode;
167
+ }
168
+ /**
169
+ * A Zod-validated form. Wrap `<TextField>`/`<TextareaField>` (or any field using
170
+ * `useFormContext`) as children; submit is wired with validation + a disabled-while-submitting button.
171
+ */
172
+ declare function ResourceForm<T extends FieldValues>({ schema, defaultValues, onSubmit, children, submitLabel, footer, }: ResourceFormProps<T>): react.JSX.Element;
173
+ interface FieldProps<T extends FieldValues> {
174
+ readonly name: Path<T>;
175
+ readonly label: string;
176
+ readonly placeholder?: string;
177
+ readonly type?: string;
178
+ }
179
+ /** A labelled text input bound to the surrounding `ResourceForm` with inline validation errors. */
180
+ declare function TextField<T extends FieldValues>({ name, label, placeholder, type }: FieldProps<T>): react.JSX.Element;
181
+ /** A labelled textarea bound to the surrounding `ResourceForm`. */
182
+ declare function TextareaField<T extends FieldValues>({ name, label, placeholder }: FieldProps<T>): react.JSX.Element;
183
+
184
+ interface Column<T> {
185
+ /** Key into the row, used for the default cell value + as a React key. */
186
+ readonly key: string;
187
+ readonly header: string;
188
+ /** Custom cell renderer; defaults to `String(row[key])`. */
189
+ readonly render?: (row: T) => ReactNode;
190
+ readonly className?: string;
191
+ }
192
+ interface PaginationProps {
193
+ readonly page: number;
194
+ readonly pageCount: number;
195
+ readonly onPageChange: (page: number) => void;
196
+ }
197
+ /** Prev/next pager with a page indicator. */
198
+ declare function Pagination({ page, pageCount, onPageChange }: PaginationProps): react.JSX.Element | null;
199
+ interface DataTableProps<T> {
200
+ readonly resource: string;
201
+ readonly columns: readonly Column<T>[];
202
+ readonly pageSize?: number;
203
+ /** Extra list params (sort/filter) merged into each query. */
204
+ readonly params?: Omit<ListParams, 'page' | 'pageSize'>;
205
+ readonly emptyMessage?: string;
206
+ }
207
+ /**
208
+ * A paginated table for a resource, fed by `useList` from `@liam-public/browser-react-data`.
209
+ * Requires a `<DataProviderProvider>` + a `QueryClientProvider` above it.
210
+ */
211
+ declare function DataTable<T extends Record<string, unknown>>({ resource, columns, pageSize, params, emptyMessage, }: DataTableProps<T>): react.JSX.Element;
212
+
213
+ interface StatCardProps {
214
+ readonly label: string;
215
+ readonly value: ReactNode;
216
+ readonly icon?: ComponentType<{
217
+ className?: string;
218
+ }>;
219
+ readonly hint?: string;
220
+ readonly className?: string;
221
+ }
222
+ /** A KPI card: label, big value, optional icon + hint. */
223
+ declare function StatCard({ label, value, icon: Icon, hint, className }: StatCardProps): react.JSX.Element;
224
+ /** Responsive grid of KPI cards (1 / 2 / 4 columns). */
225
+ declare function StatGrid({ children, columns }: {
226
+ children: ReactNode;
227
+ columns?: 2 | 3 | 4;
228
+ }): react.JSX.Element;
229
+ interface ChartCardProps {
230
+ readonly title: string;
231
+ readonly children: ReactNode;
232
+ readonly height?: number;
233
+ readonly action?: ReactNode;
234
+ }
235
+ /**
236
+ * A titled card sized for a chart. Put a recharts `<ResponsiveContainer>` (or any chart)
237
+ * inside; `recharts` is a dependency so apps don't need to install it separately.
238
+ */
239
+ declare function ChartCard({ title, children, height, action }: ChartCardProps): react.JSX.Element;
240
+
241
+ /** Pluggable analytics sink. Implement once (or use a provided adapter) and call from anywhere. */
242
+ interface AnalyticsAdapter {
243
+ pageView(path: string): void;
244
+ track(event: string, props?: Record<string, unknown>): void;
245
+ }
246
+ declare function AnalyticsProvider({ adapter, children }: {
247
+ adapter: AnalyticsAdapter;
248
+ children: ReactNode;
249
+ }): react.JSX.Element;
250
+ declare function useAnalytics(): AnalyticsAdapter;
251
+ /** Fire a `pageView` on every router path change. Mount once inside the Router + AnalyticsProvider. */
252
+ declare function usePageViews(): void;
253
+ /** A beacon adapter that POSTs events to an ingest endpoint (uses `sendBeacon` when available). */
254
+ declare function createBeaconAnalytics(options: {
255
+ endpoint: string;
256
+ app?: string;
257
+ }): AnalyticsAdapter;
258
+ /** A console adapter for local development. */
259
+ declare function createConsoleAnalytics(): AnalyticsAdapter;
260
+
261
+ interface CreateI18nOptions {
262
+ /** i18next resource bundles: `{ en: { translation: {...} }, vi: { translation: {...} } }`. */
263
+ readonly resources: Resource;
264
+ readonly lng?: string;
265
+ readonly fallbackLng?: string;
266
+ }
267
+ /** Create a configured, isolated i18next instance for the CMS. */
268
+ declare function createI18n(options: CreateI18nOptions): i18n;
269
+ declare function I18nProvider({ i18n, children }: {
270
+ i18n: i18n;
271
+ children: ReactNode;
272
+ }): react.JSX.Element;
273
+
274
+ interface LanguageOption {
275
+ readonly code: string;
276
+ readonly label: string;
277
+ }
278
+ /** A dropdown that switches the active i18next language. */
279
+ declare function LanguageSwitcher({ languages }: {
280
+ languages: readonly LanguageOption[];
281
+ }): react.JSX.Element;
282
+
283
+ export { type AnalyticsAdapter, AnalyticsProvider, ChartCard, type ChartCardProps, CmsLayout, type CmsLayoutProps, type Column, ConfirmDialog, type ConfirmDialogProps, type CreateI18nOptions, DataTable, type DataTableProps, DesktopOnly, I18nProvider, type LanguageOption, LanguageSwitcher, LoadingSpinner, type LoadingSpinnerProps, MobileCard, MobileCards, MobileField, type NavItem, PageHeader, type PageHeaderProps, PageStack, type PageStackProps, Pagination, type PaginationProps, RequireAuth, type RequireAuthProps, RequireRole, type RequireRoleProps, ResourceForm, type ResourceFormProps, StatCard, type StatCardProps, StatGrid, TextField, TextareaField, type Theme, type ThemeContextValue, ThemeProvider, type ThemeProviderProps, ThemeToggle, type ToastContextValue, type ToastOptions, ToastProvider, type ToastVariant, createBeaconAnalytics, createConsoleAnalytics, createI18n, useAnalytics, usePageViews, useResourceForm, useTheme, useToast };
package/dist/index.js CHANGED
@@ -57,59 +57,33 @@ function CmsLayout({ children, nav, brand = "CMS", headerRight }) {
57
57
  ] });
58
58
  }
59
59
 
60
- // src/guards.tsx
61
- import { useEffect } from "react";
62
- import { useAuth } from "@liam-public/browser-react-auth";
63
- import { Fragment as Fragment2, jsx as jsx2 } from "react/jsx-runtime";
64
- function DefaultFallback({ message }) {
65
- return /* @__PURE__ */ jsx2("div", { className: "flex h-screen items-center justify-center", children: /* @__PURE__ */ jsx2("p", { className: "text-sm text-muted-foreground", children: message }) });
66
- }
67
- function RequireAuth({ children, fallback, onUnauthenticated }) {
68
- const { isAuthenticated, isLoading } = useAuth();
69
- useEffect(() => {
70
- if (!isLoading && !isAuthenticated) onUnauthenticated?.();
71
- }, [isLoading, isAuthenticated, onUnauthenticated]);
72
- if (isLoading) return /* @__PURE__ */ jsx2(Fragment2, { children: fallback ?? /* @__PURE__ */ jsx2(DefaultFallback, { message: "Loading\u2026" }) });
73
- if (!isAuthenticated) return /* @__PURE__ */ jsx2(Fragment2, { children: fallback ?? /* @__PURE__ */ jsx2(DefaultFallback, { message: "Redirecting to sign in\u2026" }) });
74
- return /* @__PURE__ */ jsx2(Fragment2, { children });
75
- }
76
- function RequireRole({ children, anyOf, getRoles, fallback }) {
77
- const { user, isLoading, isAuthenticated } = useAuth();
78
- if (isLoading) return /* @__PURE__ */ jsx2(Fragment2, { children: fallback ?? /* @__PURE__ */ jsx2(DefaultFallback, { message: "Loading\u2026" }) });
79
- if (!isAuthenticated) return /* @__PURE__ */ jsx2(Fragment2, { children: fallback ?? /* @__PURE__ */ jsx2(DefaultFallback, { message: "Not signed in." }) });
80
- const roles = getRoles(user);
81
- const allowed = anyOf.some((r) => roles.includes(r));
82
- if (!allowed) return /* @__PURE__ */ jsx2(Fragment2, { children: fallback ?? /* @__PURE__ */ jsx2(DefaultFallback, { message: "You don't have access to this page." }) });
83
- return /* @__PURE__ */ jsx2(Fragment2, { children });
84
- }
85
-
86
60
  // src/page.tsx
87
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
61
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
88
62
  function PageHeader({ title, action }) {
89
63
  return /* @__PURE__ */ jsxs2("div", { className: "flex items-center justify-between", children: [
90
- /* @__PURE__ */ jsx3("h1", { className: "text-2xl font-bold", children: title }),
64
+ /* @__PURE__ */ jsx2("h1", { className: "text-2xl font-bold", children: title }),
91
65
  action
92
66
  ] });
93
67
  }
94
68
  function PageStack({ children, spacing = 6 }) {
95
- return /* @__PURE__ */ jsx3("div", { className: spacing === 4 ? "space-y-4" : "space-y-6", children });
69
+ return /* @__PURE__ */ jsx2("div", { className: spacing === 4 ? "space-y-4" : "space-y-6", children });
96
70
  }
97
71
 
98
72
  // src/responsive-table.tsx
99
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
73
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
100
74
  function DesktopOnly({ children }) {
101
- return /* @__PURE__ */ jsx4("div", { className: "hidden md:block", children });
75
+ return /* @__PURE__ */ jsx3("div", { className: "hidden md:block", children });
102
76
  }
103
77
  function MobileCards({ children }) {
104
- return /* @__PURE__ */ jsx4("div", { className: "flex flex-col gap-3 md:hidden", children });
78
+ return /* @__PURE__ */ jsx3("div", { className: "flex flex-col gap-3 md:hidden", children });
105
79
  }
106
80
  function MobileCard({ children }) {
107
- return /* @__PURE__ */ jsx4("div", { className: "rounded-lg border bg-card p-3 text-sm", children });
81
+ return /* @__PURE__ */ jsx3("div", { className: "rounded-lg border bg-card p-3 text-sm", children });
108
82
  }
109
83
  function MobileField({ label, children }) {
110
84
  return /* @__PURE__ */ jsxs3("div", { className: "flex items-start justify-between gap-3 py-0.5", children: [
111
- /* @__PURE__ */ jsx4("span", { className: "text-muted-foreground", children: label }),
112
- /* @__PURE__ */ jsx4("span", { className: "text-right font-medium break-words", children })
85
+ /* @__PURE__ */ jsx3("span", { className: "text-muted-foreground", children: label }),
86
+ /* @__PURE__ */ jsx3("span", { className: "text-right font-medium break-words", children })
113
87
  ] });
114
88
  }
115
89
 
@@ -124,11 +98,11 @@ import {
124
98
  DialogHeader,
125
99
  DialogTitle
126
100
  } from "@liam-public/browser-react-ui";
127
- import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
101
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
128
102
  function LoadingSpinner({ size = "md" }) {
129
103
  const padding = size === "lg" ? "py-16" : "py-8";
130
104
  const icon = size === "lg" ? "h-8 w-8" : "h-6 w-6";
131
- return /* @__PURE__ */ jsx5("div", { className: `flex items-center justify-center ${padding}`, children: /* @__PURE__ */ jsx5(Loader2, { className: `${icon} animate-spin text-muted-foreground` }) });
105
+ return /* @__PURE__ */ jsx4("div", { className: `flex items-center justify-center ${padding}`, children: /* @__PURE__ */ jsx4(Loader2, { className: `${icon} animate-spin text-muted-foreground` }) });
132
106
  }
133
107
  function ConfirmDialog({
134
108
  open,
@@ -141,14 +115,14 @@ function ConfirmDialog({
141
115
  confirmLabel = "Confirm",
142
116
  destructive
143
117
  }) {
144
- return /* @__PURE__ */ jsx5(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs4(DialogContent, { children: [
118
+ return /* @__PURE__ */ jsx4(Dialog, { open, onOpenChange, children: /* @__PURE__ */ jsxs4(DialogContent, { children: [
145
119
  /* @__PURE__ */ jsxs4(DialogHeader, { children: [
146
- /* @__PURE__ */ jsx5(DialogTitle, { children: title }),
147
- /* @__PURE__ */ jsx5(DialogDescription, { children: description })
120
+ /* @__PURE__ */ jsx4(DialogTitle, { children: title }),
121
+ /* @__PURE__ */ jsx4(DialogDescription, { children: description })
148
122
  ] }),
149
- error && /* @__PURE__ */ jsx5("p", { className: "text-sm text-destructive", children: error }),
123
+ error && /* @__PURE__ */ jsx4("p", { className: "text-sm text-destructive", children: error }),
150
124
  /* @__PURE__ */ jsxs4(DialogFooter, { children: [
151
- /* @__PURE__ */ jsx5(Button2, { variant: "outline", onClick: () => onOpenChange(false), disabled: loading, children: "Cancel" }),
125
+ /* @__PURE__ */ jsx4(Button2, { variant: "outline", onClick: () => onOpenChange(false), disabled: loading, children: "Cancel" }),
152
126
  /* @__PURE__ */ jsxs4(
153
127
  Button2,
154
128
  {
@@ -156,7 +130,7 @@ function ConfirmDialog({
156
130
  onClick: onConfirm,
157
131
  disabled: loading,
158
132
  children: [
159
- loading && /* @__PURE__ */ jsx5(Loader2, { className: "h-4 w-4 animate-spin" }),
133
+ loading && /* @__PURE__ */ jsx4(Loader2, { className: "h-4 w-4 animate-spin" }),
160
134
  confirmLabel
161
135
  ]
162
136
  }
@@ -164,17 +138,494 @@ function ConfirmDialog({
164
138
  ] })
165
139
  ] }) });
166
140
  }
141
+
142
+ // src/guards.tsx
143
+ import { useEffect } from "react";
144
+ import { useAuth } from "@liam-public/browser-react-auth";
145
+ import { Fragment as Fragment2, jsx as jsx5 } from "react/jsx-runtime";
146
+ function DefaultFallback({ message }) {
147
+ return /* @__PURE__ */ jsx5("div", { className: "flex h-screen items-center justify-center", children: /* @__PURE__ */ jsx5("p", { className: "text-sm text-muted-foreground", children: message }) });
148
+ }
149
+ function RequireAuth({ children, fallback, onUnauthenticated }) {
150
+ const { isAuthenticated, isLoading } = useAuth();
151
+ useEffect(() => {
152
+ if (!isLoading && !isAuthenticated) onUnauthenticated?.();
153
+ }, [isLoading, isAuthenticated, onUnauthenticated]);
154
+ if (isLoading) return /* @__PURE__ */ jsx5(Fragment2, { children: fallback ?? /* @__PURE__ */ jsx5(DefaultFallback, { message: "Loading\u2026" }) });
155
+ if (!isAuthenticated) return /* @__PURE__ */ jsx5(Fragment2, { children: fallback ?? /* @__PURE__ */ jsx5(DefaultFallback, { message: "Redirecting to sign in\u2026" }) });
156
+ return /* @__PURE__ */ jsx5(Fragment2, { children });
157
+ }
158
+ function RequireRole({ children, anyOf, getRoles, fallback }) {
159
+ const { user, isLoading, isAuthenticated } = useAuth();
160
+ if (isLoading) return /* @__PURE__ */ jsx5(Fragment2, { children: fallback ?? /* @__PURE__ */ jsx5(DefaultFallback, { message: "Loading\u2026" }) });
161
+ if (!isAuthenticated) return /* @__PURE__ */ jsx5(Fragment2, { children: fallback ?? /* @__PURE__ */ jsx5(DefaultFallback, { message: "Not signed in." }) });
162
+ const roles = getRoles(user);
163
+ const allowed = anyOf.some((r) => roles.includes(r));
164
+ if (!allowed) return /* @__PURE__ */ jsx5(Fragment2, { children: fallback ?? /* @__PURE__ */ jsx5(DefaultFallback, { message: "You don't have access to this page." }) });
165
+ return /* @__PURE__ */ jsx5(Fragment2, { children });
166
+ }
167
+
168
+ // src/theme.tsx
169
+ import { createContext, useCallback, useContext, useEffect as useEffect2, useState as useState2 } from "react";
170
+ import { Moon, Sun } from "lucide-react";
171
+ import { Button as Button3 } from "@liam-public/browser-react-ui";
172
+ import { jsx as jsx6 } from "react/jsx-runtime";
173
+ var ThemeContext = createContext(null);
174
+ function readStored(key) {
175
+ try {
176
+ return typeof localStorage !== "undefined" ? localStorage.getItem(key) : null;
177
+ } catch {
178
+ return null;
179
+ }
180
+ }
181
+ function writeStored(key, value) {
182
+ try {
183
+ if (typeof localStorage !== "undefined") localStorage.setItem(key, value);
184
+ } catch {
185
+ }
186
+ }
187
+ function prefersDark() {
188
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" ? window.matchMedia("(prefers-color-scheme: dark)").matches : false;
189
+ }
190
+ function resolve(theme) {
191
+ return theme === "system" ? prefersDark() ? "dark" : "light" : theme;
192
+ }
193
+ function applyClass(resolved) {
194
+ if (typeof document !== "undefined") {
195
+ document.documentElement.classList.toggle("dark", resolved === "dark");
196
+ }
197
+ }
198
+ function ThemeProvider({
199
+ children,
200
+ defaultTheme = "system",
201
+ storageKey = "cms-theme"
202
+ }) {
203
+ const [theme, setThemeState] = useState2(
204
+ () => readStored(storageKey) ?? defaultTheme
205
+ );
206
+ useEffect2(() => {
207
+ applyClass(resolve(theme));
208
+ }, [theme]);
209
+ useEffect2(() => {
210
+ if (theme !== "system" || typeof window === "undefined" || !window.matchMedia) return;
211
+ const mq = window.matchMedia("(prefers-color-scheme: dark)");
212
+ const onChange = () => applyClass(resolve("system"));
213
+ mq.addEventListener("change", onChange);
214
+ return () => mq.removeEventListener("change", onChange);
215
+ }, [theme]);
216
+ const setTheme = useCallback(
217
+ (next) => {
218
+ writeStored(storageKey, next);
219
+ setThemeState(next);
220
+ },
221
+ [storageKey]
222
+ );
223
+ const toggle = useCallback(() => {
224
+ setTheme(resolve(theme) === "dark" ? "light" : "dark");
225
+ }, [theme, setTheme]);
226
+ return /* @__PURE__ */ jsx6(ThemeContext.Provider, { value: { theme, resolved: resolve(theme), setTheme, toggle }, children });
227
+ }
228
+ function useTheme() {
229
+ const ctx = useContext(ThemeContext);
230
+ if (!ctx) throw new Error("useTheme must be used within a <ThemeProvider>");
231
+ return ctx;
232
+ }
233
+ function ThemeToggle() {
234
+ const { resolved, toggle } = useTheme();
235
+ return /* @__PURE__ */ jsx6(Button3, { variant: "ghost", size: "icon", onClick: toggle, "aria-label": "Toggle dark mode", children: resolved === "dark" ? /* @__PURE__ */ jsx6(Sun, { className: "h-5 w-5" }) : /* @__PURE__ */ jsx6(Moon, { className: "h-5 w-5" }) });
236
+ }
237
+
238
+ // src/toast.tsx
239
+ import { createContext as createContext2, useCallback as useCallback2, useContext as useContext2, useRef, useState as useState3 } from "react";
240
+ import { CheckCircle2, Info, X, XCircle } from "lucide-react";
241
+ import { cn as cn2 } from "@liam-public/browser-react-ui";
242
+ import { jsx as jsx7, jsxs as jsxs5 } from "react/jsx-runtime";
243
+ var ToastContext = createContext2(null);
244
+ var ICONS = {
245
+ default: Info,
246
+ success: CheckCircle2,
247
+ error: XCircle
248
+ };
249
+ function ToastProvider({ children }) {
250
+ const [items, setItems] = useState3([]);
251
+ const seq = useRef(0);
252
+ const dismiss = useCallback2((id) => {
253
+ setItems((prev) => prev.filter((t) => t.id !== id));
254
+ }, []);
255
+ const toast = useCallback2(
256
+ (options) => {
257
+ const id = ++seq.current;
258
+ const item = { id, variant: options.variant ?? "default", ...options };
259
+ setItems((prev) => [...prev, item]);
260
+ const duration = options.duration ?? 5e3;
261
+ if (duration > 0) setTimeout(() => dismiss(id), duration);
262
+ return id;
263
+ },
264
+ [dismiss]
265
+ );
266
+ const success = useCallback2((title, description) => toast({ title, description, variant: "success" }), [toast]);
267
+ const error = useCallback2((title, description) => toast({ title, description, variant: "error" }), [toast]);
268
+ return /* @__PURE__ */ jsxs5(ToastContext.Provider, { value: { toast, success, error, dismiss }, children: [
269
+ children,
270
+ /* @__PURE__ */ jsx7("div", { className: "pointer-events-none fixed bottom-4 right-4 z-50 flex w-full max-w-sm flex-col gap-2", children: items.map((t) => {
271
+ const Icon = ICONS[t.variant];
272
+ return /* @__PURE__ */ jsxs5(
273
+ "div",
274
+ {
275
+ role: "status",
276
+ className: cn2(
277
+ "pointer-events-auto flex items-start gap-2 rounded-lg border bg-card p-3 text-sm shadow-lg",
278
+ t.variant === "error" && "border-destructive/40",
279
+ t.variant === "success" && "border-green-500/40"
280
+ ),
281
+ children: [
282
+ /* @__PURE__ */ jsx7(
283
+ Icon,
284
+ {
285
+ className: cn2(
286
+ "mt-0.5 h-4 w-4 shrink-0",
287
+ t.variant === "error" && "text-destructive",
288
+ t.variant === "success" && "text-green-600",
289
+ t.variant === "default" && "text-muted-foreground"
290
+ )
291
+ }
292
+ ),
293
+ /* @__PURE__ */ jsxs5("div", { className: "flex-1", children: [
294
+ t.title && /* @__PURE__ */ jsx7("p", { className: "font-medium", children: t.title }),
295
+ t.description && /* @__PURE__ */ jsx7("p", { className: "text-muted-foreground", children: t.description })
296
+ ] }),
297
+ /* @__PURE__ */ jsx7(
298
+ "button",
299
+ {
300
+ onClick: () => dismiss(t.id),
301
+ "aria-label": "Dismiss",
302
+ className: "text-muted-foreground hover:text-foreground",
303
+ children: /* @__PURE__ */ jsx7(X, { className: "h-4 w-4" })
304
+ }
305
+ )
306
+ ]
307
+ },
308
+ t.id
309
+ );
310
+ }) })
311
+ ] });
312
+ }
313
+ function useToast() {
314
+ const ctx = useContext2(ToastContext);
315
+ if (!ctx) throw new Error("useToast must be used within a <ToastProvider>");
316
+ return ctx;
317
+ }
318
+
319
+ // src/form.tsx
320
+ import {
321
+ FormProvider,
322
+ useForm,
323
+ useFormContext
324
+ } from "react-hook-form";
325
+ import { zodResolver } from "@hookform/resolvers/zod";
326
+ import { Button as Button4, Input, Label } from "@liam-public/browser-react-ui";
327
+ import { jsx as jsx8, jsxs as jsxs6 } from "react/jsx-runtime";
328
+ function useResourceForm(schema, defaultValues) {
329
+ return useForm({
330
+ // resolver generics across zod 3/4 are loose; the runtime validation is correct.
331
+ resolver: zodResolver(schema),
332
+ defaultValues
333
+ });
334
+ }
335
+ function ResourceForm({
336
+ schema,
337
+ defaultValues,
338
+ onSubmit,
339
+ children,
340
+ submitLabel = "Save",
341
+ footer
342
+ }) {
343
+ const form = useResourceForm(schema, defaultValues);
344
+ return /* @__PURE__ */ jsx8(FormProvider, { ...form, children: /* @__PURE__ */ jsxs6("form", { onSubmit: form.handleSubmit(onSubmit), className: "space-y-4", children: [
345
+ children,
346
+ /* @__PURE__ */ jsxs6("div", { className: "flex items-center justify-end gap-2", children: [
347
+ footer,
348
+ /* @__PURE__ */ jsx8(Button4, { type: "submit", disabled: form.formState.isSubmitting, children: submitLabel })
349
+ ] })
350
+ ] }) });
351
+ }
352
+ function useFieldError(name) {
353
+ const {
354
+ formState: { errors }
355
+ } = useFormContext();
356
+ const err = errors[name];
357
+ return err?.message;
358
+ }
359
+ function TextField({ name, label, placeholder, type = "text" }) {
360
+ const { register } = useFormContext();
361
+ const error = useFieldError(name);
362
+ return /* @__PURE__ */ jsxs6("div", { className: "space-y-1.5", children: [
363
+ /* @__PURE__ */ jsx8(Label, { htmlFor: name, children: label }),
364
+ /* @__PURE__ */ jsx8(Input, { id: name, type, placeholder, "aria-invalid": !!error, ...register(name) }),
365
+ error && /* @__PURE__ */ jsx8("p", { className: "text-sm text-destructive", children: error })
366
+ ] });
367
+ }
368
+ function TextareaField({ name, label, placeholder }) {
369
+ const { register } = useFormContext();
370
+ const error = useFieldError(name);
371
+ return /* @__PURE__ */ jsxs6("div", { className: "space-y-1.5", children: [
372
+ /* @__PURE__ */ jsx8(Label, { htmlFor: name, children: label }),
373
+ /* @__PURE__ */ jsx8(
374
+ "textarea",
375
+ {
376
+ id: name,
377
+ placeholder,
378
+ "aria-invalid": !!error,
379
+ className: "flex min-h-20 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
380
+ ...register(name)
381
+ }
382
+ ),
383
+ error && /* @__PURE__ */ jsx8("p", { className: "text-sm text-destructive", children: error })
384
+ ] });
385
+ }
386
+
387
+ // src/data-table.tsx
388
+ import { useState as useState4 } from "react";
389
+ import { ChevronLeft, ChevronRight } from "lucide-react";
390
+ import {
391
+ Button as Button5,
392
+ Table,
393
+ TableBody,
394
+ TableCell,
395
+ TableHead,
396
+ TableHeader,
397
+ TableRow
398
+ } from "@liam-public/browser-react-ui";
399
+ import { useList } from "@liam-public/browser-react-data";
400
+ import { jsx as jsx9, jsxs as jsxs7 } from "react/jsx-runtime";
401
+ function Pagination({ page, pageCount, onPageChange }) {
402
+ if (pageCount <= 1) return null;
403
+ return /* @__PURE__ */ jsxs7("div", { className: "flex items-center justify-end gap-2 pt-3 text-sm", children: [
404
+ /* @__PURE__ */ jsxs7("span", { className: "text-muted-foreground", children: [
405
+ "Page ",
406
+ page,
407
+ " of ",
408
+ pageCount
409
+ ] }),
410
+ /* @__PURE__ */ jsx9(Button5, { variant: "outline", size: "icon-sm", disabled: page <= 1, onClick: () => onPageChange(page - 1), "aria-label": "Previous page", children: /* @__PURE__ */ jsx9(ChevronLeft, { className: "h-4 w-4" }) }),
411
+ /* @__PURE__ */ jsx9(Button5, { variant: "outline", size: "icon-sm", disabled: page >= pageCount, onClick: () => onPageChange(page + 1), "aria-label": "Next page", children: /* @__PURE__ */ jsx9(ChevronRight, { className: "h-4 w-4" }) })
412
+ ] });
413
+ }
414
+ function DataTable({
415
+ resource,
416
+ columns,
417
+ pageSize = 20,
418
+ params,
419
+ emptyMessage = "No records."
420
+ }) {
421
+ const [page, setPage] = useState4(1);
422
+ const { data, isLoading, isError, error } = useList(resource, { page, pageSize, ...params });
423
+ if (isLoading) return /* @__PURE__ */ jsx9(LoadingSpinner, {});
424
+ if (isError) return /* @__PURE__ */ jsx9("p", { className: "py-8 text-center text-sm text-destructive", children: error?.message ?? "Failed to load." });
425
+ const rows = data?.data ?? [];
426
+ const total = data?.total ?? 0;
427
+ const pageCount = Math.max(1, Math.ceil(total / pageSize));
428
+ if (rows.length === 0) {
429
+ return /* @__PURE__ */ jsx9("p", { className: "py-8 text-center text-sm text-muted-foreground", children: emptyMessage });
430
+ }
431
+ return /* @__PURE__ */ jsxs7("div", { children: [
432
+ /* @__PURE__ */ jsxs7(Table, { children: [
433
+ /* @__PURE__ */ jsx9(TableHeader, { children: /* @__PURE__ */ jsx9(TableRow, { children: columns.map((c) => /* @__PURE__ */ jsx9(TableHead, { className: c.className, children: c.header }, c.key)) }) }),
434
+ /* @__PURE__ */ jsx9(TableBody, { children: rows.map((row, i) => /* @__PURE__ */ jsx9(TableRow, { children: columns.map((c) => /* @__PURE__ */ jsx9(TableCell, { className: c.className, children: c.render ? c.render(row) : String(row[c.key] ?? "") }, c.key)) }, row.id ?? i)) })
435
+ ] }),
436
+ /* @__PURE__ */ jsx9(Pagination, { page, pageCount, onPageChange: setPage })
437
+ ] });
438
+ }
439
+
440
+ // src/dashboard.tsx
441
+ import { Card, CardContent, CardHeader, CardTitle, cn as cn3 } from "@liam-public/browser-react-ui";
442
+ import {
443
+ ResponsiveContainer,
444
+ LineChart,
445
+ Line,
446
+ BarChart,
447
+ Bar,
448
+ AreaChart,
449
+ Area,
450
+ XAxis,
451
+ YAxis,
452
+ CartesianGrid,
453
+ Tooltip,
454
+ Legend
455
+ } from "recharts";
456
+ import { jsx as jsx10, jsxs as jsxs8 } from "react/jsx-runtime";
457
+ function StatCard({ label, value, icon: Icon, hint, className }) {
458
+ return /* @__PURE__ */ jsxs8(Card, { className, children: [
459
+ /* @__PURE__ */ jsxs8(CardHeader, { className: "flex flex-row items-center justify-between space-y-0 pb-2", children: [
460
+ /* @__PURE__ */ jsx10(CardTitle, { className: "text-sm font-medium text-muted-foreground", children: label }),
461
+ Icon && /* @__PURE__ */ jsx10(Icon, { className: "h-4 w-4 text-muted-foreground" })
462
+ ] }),
463
+ /* @__PURE__ */ jsxs8(CardContent, { children: [
464
+ /* @__PURE__ */ jsx10("div", { className: "text-2xl font-bold", children: value }),
465
+ hint && /* @__PURE__ */ jsx10("p", { className: "text-xs text-muted-foreground", children: hint })
466
+ ] })
467
+ ] });
468
+ }
469
+ function StatGrid({ children, columns = 4 }) {
470
+ const cols = columns === 2 ? "sm:grid-cols-2" : columns === 3 ? "sm:grid-cols-2 lg:grid-cols-3" : "sm:grid-cols-2 lg:grid-cols-4";
471
+ return /* @__PURE__ */ jsx10("div", { className: cn3("grid grid-cols-1 gap-4", cols), children });
472
+ }
473
+ function ChartCard({ title, children, height = 280, action }) {
474
+ return /* @__PURE__ */ jsxs8(Card, { children: [
475
+ /* @__PURE__ */ jsxs8(CardHeader, { className: "flex flex-row items-center justify-between space-y-0", children: [
476
+ /* @__PURE__ */ jsx10(CardTitle, { className: "text-base", children: title }),
477
+ action
478
+ ] }),
479
+ /* @__PURE__ */ jsx10(CardContent, { children: /* @__PURE__ */ jsx10("div", { style: { height }, children }) })
480
+ ] });
481
+ }
482
+
483
+ // src/analytics.tsx
484
+ import { createContext as createContext3, useContext as useContext3, useEffect as useEffect3, useRef as useRef2 } from "react";
485
+ import { useLocation } from "react-router-dom";
486
+ import { jsx as jsx11 } from "react/jsx-runtime";
487
+ var noop = { pageView: () => {
488
+ }, track: () => {
489
+ } };
490
+ var AnalyticsContext = createContext3(noop);
491
+ function AnalyticsProvider({ adapter, children }) {
492
+ return /* @__PURE__ */ jsx11(AnalyticsContext.Provider, { value: adapter, children });
493
+ }
494
+ function useAnalytics() {
495
+ return useContext3(AnalyticsContext);
496
+ }
497
+ function usePageViews() {
498
+ const analytics = useAnalytics();
499
+ const location = useLocation();
500
+ const last = useRef2("");
501
+ useEffect3(() => {
502
+ const path = location.pathname + location.search;
503
+ if (path !== last.current) {
504
+ last.current = path;
505
+ analytics.pageView(path);
506
+ }
507
+ }, [location, analytics]);
508
+ }
509
+ function createBeaconAnalytics(options) {
510
+ const send = (type, payload) => {
511
+ const body = JSON.stringify({ type, app: options.app, ts: (/* @__PURE__ */ new Date()).toISOString(), ...payload });
512
+ if (typeof navigator !== "undefined" && navigator.sendBeacon) {
513
+ navigator.sendBeacon(options.endpoint, new Blob([body], { type: "application/json" }));
514
+ } else if (typeof fetch !== "undefined") {
515
+ void fetch(options.endpoint, { method: "POST", headers: { "content-type": "application/json" }, body, keepalive: true });
516
+ }
517
+ };
518
+ return {
519
+ pageView: (path) => send("pageView", { path }),
520
+ track: (event, props) => send("track", { event, props })
521
+ };
522
+ }
523
+ function createConsoleAnalytics() {
524
+ return {
525
+ pageView: (path) => console.info("[analytics] pageView", path),
526
+ track: (event, props) => console.info("[analytics] track", event, props)
527
+ };
528
+ }
529
+
530
+ // src/i18n.tsx
531
+ import i18next from "i18next";
532
+ import { I18nextProvider, initReactI18next, useTranslation } from "react-i18next";
533
+ import {
534
+ Select,
535
+ SelectContent,
536
+ SelectItem,
537
+ SelectTrigger,
538
+ SelectValue
539
+ } from "@liam-public/browser-react-ui";
540
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
541
+ function createI18n(options) {
542
+ const instance = i18next.createInstance();
543
+ void instance.use(initReactI18next).init({
544
+ resources: options.resources,
545
+ lng: options.lng ?? "en",
546
+ fallbackLng: options.fallbackLng ?? "en",
547
+ interpolation: { escapeValue: false }
548
+ });
549
+ return instance;
550
+ }
551
+ function I18nProvider({ i18n, children }) {
552
+ return /* @__PURE__ */ jsx12(I18nextProvider, { i18n, children });
553
+ }
554
+ function LanguageSwitcher({ languages }) {
555
+ const { i18n } = useTranslation();
556
+ return /* @__PURE__ */ jsxs9(Select, { value: i18n.language, onValueChange: (value) => void i18n.changeLanguage(value), children: [
557
+ /* @__PURE__ */ jsx12(SelectTrigger, { className: "w-36", children: /* @__PURE__ */ jsx12(SelectValue, {}) }),
558
+ /* @__PURE__ */ jsx12(SelectContent, { children: languages.map((l) => /* @__PURE__ */ jsx12(SelectItem, { value: l.code, children: l.label }, l.code)) })
559
+ ] });
560
+ }
561
+
562
+ // src/wire.ts
563
+ import { AuthProvider, useAuth as useAuth2, createAuthenticatedFetch } from "@liam-public/browser-react-auth";
564
+ import {
565
+ formatDate,
566
+ formatCurrency,
567
+ formatNumber,
568
+ formatPercentage,
569
+ formatFileSize,
570
+ selectText,
571
+ isLocalizedText
572
+ } from "@liam-public/text";
167
573
  export {
574
+ AnalyticsProvider,
575
+ Area,
576
+ AreaChart,
577
+ AuthProvider,
578
+ Bar,
579
+ BarChart,
580
+ CartesianGrid,
581
+ ChartCard,
168
582
  CmsLayout,
169
583
  ConfirmDialog,
584
+ DataTable,
170
585
  DesktopOnly,
586
+ I18nProvider,
587
+ LanguageSwitcher,
588
+ Legend,
589
+ Line,
590
+ LineChart,
171
591
  LoadingSpinner,
172
592
  MobileCard,
173
593
  MobileCards,
174
594
  MobileField,
175
595
  PageHeader,
176
596
  PageStack,
597
+ Pagination,
177
598
  RequireAuth,
178
- RequireRole
599
+ RequireRole,
600
+ ResourceForm,
601
+ ResponsiveContainer,
602
+ StatCard,
603
+ StatGrid,
604
+ TextField,
605
+ TextareaField,
606
+ ThemeProvider,
607
+ ThemeToggle,
608
+ ToastProvider,
609
+ Tooltip,
610
+ XAxis,
611
+ YAxis,
612
+ createAuthenticatedFetch,
613
+ createBeaconAnalytics,
614
+ createConsoleAnalytics,
615
+ createI18n,
616
+ formatCurrency,
617
+ formatDate,
618
+ formatFileSize,
619
+ formatNumber,
620
+ formatPercentage,
621
+ isLocalizedText,
622
+ selectText,
623
+ useAnalytics,
624
+ useAuth2 as useAuth,
625
+ usePageViews,
626
+ useResourceForm,
627
+ useTheme,
628
+ useToast,
629
+ useTranslation
179
630
  };
180
631
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cms-layout.tsx","../src/guards.tsx","../src/page.tsx","../src/responsive-table.tsx","../src/feedback.tsx"],"sourcesContent":["import { useState } from 'react'\nimport type { ComponentType, ReactNode } from 'react'\nimport { NavLink } from 'react-router-dom'\nimport { Menu } from 'lucide-react'\nimport {\n Button,\n Separator,\n Sheet,\n SheetContent,\n SheetTrigger,\n cn,\n} from '@liam-public/browser-react-ui'\n\n/** A single sidebar navigation entry. `icon` is any lucide-react (or compatible) icon. */\nexport interface NavItem {\n readonly to: string\n readonly label: string\n readonly icon: ComponentType<{ className?: string }>\n /** Exact-match the route (e.g. for the index `/`). */\n readonly end?: boolean\n /** Optional trailing content, e.g. an alert count `<Badge>`. */\n readonly badge?: ReactNode\n}\n\nfunction NavList({ nav, onNavigate }: { nav: readonly NavItem[]; onNavigate?: () => void }) {\n return (\n <>\n {nav.map((item) => (\n <NavLink\n key={item.to}\n to={item.to}\n end={item.end}\n onClick={onNavigate}\n className={({ isActive }) =>\n cn(\n 'flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent',\n isActive && 'bg-accent text-accent-foreground',\n )\n }\n >\n <item.icon className=\"h-4 w-4\" />\n {item.label}\n {item.badge != null && <span className=\"ml-auto\">{item.badge}</span>}\n </NavLink>\n ))}\n </>\n )\n}\n\nexport interface CmsLayoutProps {\n readonly children: ReactNode\n /** Sidebar navigation entries. */\n readonly nav: readonly NavItem[]\n /** Brand shown at the top of the sidebar (defaults to \"CMS\"). */\n readonly brand?: ReactNode\n /** Optional content for the top-right of the desktop header (e.g. a user menu). */\n readonly headerRight?: ReactNode\n}\n\n/**\n * Responsive CMS app shell: a desktop sidebar (md+) and a mobile hamburger sheet,\n * with an active-link-aware nav driven entirely by the `nav` prop. Bring your own\n * router (`react-router-dom`) and Tailwind theme.\n */\nexport function CmsLayout({ children, nav, brand = 'CMS', headerRight }: CmsLayoutProps) {\n const [open, setOpen] = useState(false)\n return (\n <div className=\"flex h-screen flex-col md:flex-row\">\n {/* Mobile top bar (hidden at md+) */}\n <header className=\"flex items-center gap-2 border-b p-3 md:hidden\">\n <Sheet open={open} onOpenChange={setOpen}>\n <SheetTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" aria-label=\"Open menu\" className=\"h-11 w-11\">\n <Menu className=\"h-5 w-5\" />\n </Button>\n </SheetTrigger>\n <SheetContent side=\"left\" className=\"p-0\">\n <div className=\"p-4 font-semibold text-lg\">{brand}</div>\n <Separator />\n <nav className=\"flex flex-col gap-1 p-2\">\n <NavList nav={nav} onNavigate={() => setOpen(false)} />\n </nav>\n </SheetContent>\n </Sheet>\n <span className=\"font-semibold\">{brand}</span>\n </header>\n\n {/* Desktop sidebar (hidden below md) */}\n <aside className=\"hidden md:flex w-56 border-r bg-muted/30 flex-col\">\n <div className=\"p-4 font-semibold text-lg\">{brand}</div>\n <Separator />\n <nav className=\"flex flex-1 flex-col gap-1 p-2\">\n <NavList nav={nav} />\n </nav>\n </aside>\n\n <main className=\"flex-1 overflow-auto\">\n {headerRight && (\n <div className=\"hidden md:flex items-center justify-end border-b p-3\">{headerRight}</div>\n )}\n <div className=\"p-4 md:p-6\">{children}</div>\n </main>\n </div>\n )\n}\n","import { useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport { useAuth } from '@liam-public/browser-react-auth'\n\nfunction DefaultFallback({ message }: { message: string }) {\n return (\n <div className=\"flex h-screen items-center justify-center\">\n <p className=\"text-sm text-muted-foreground\">{message}</p>\n </div>\n )\n}\n\nexport interface RequireAuthProps {\n readonly children: ReactNode\n /** Rendered while loading or when unauthenticated (defaults to a \"Redirecting…\" line). */\n readonly fallback?: ReactNode\n /**\n * Called once when the user is confirmed unauthenticated — e.g. trigger the OIDC\n * redirect from `@liam-workspace/auth-client`, or navigate to `/login`.\n */\n readonly onUnauthenticated?: () => void\n}\n\n/**\n * Gate that renders `children` only for an authenticated user. Wraps `useAuth()` from\n * `@liam-public/browser-react-auth`, filling the route-guard gap that package leaves open.\n */\nexport function RequireAuth({ children, fallback, onUnauthenticated }: RequireAuthProps) {\n const { isAuthenticated, isLoading } = useAuth()\n\n useEffect(() => {\n if (!isLoading && !isAuthenticated) onUnauthenticated?.()\n }, [isLoading, isAuthenticated, onUnauthenticated])\n\n if (isLoading) return <>{fallback ?? <DefaultFallback message=\"Loading…\" />}</>\n if (!isAuthenticated) return <>{fallback ?? <DefaultFallback message=\"Redirecting to sign in…\" />}</>\n return <>{children}</>\n}\n\nexport interface RequireRoleProps {\n readonly children: ReactNode\n /** The user is allowed if they hold ANY of these roles. */\n readonly anyOf: readonly string[]\n /** Extract the user's roles from the auth `user` object (shape is app-specific). */\n readonly getRoles: (user: unknown) => readonly string[]\n /** Rendered when the user lacks the required role(s). */\n readonly fallback?: ReactNode\n}\n\n/** Gate that additionally requires the authenticated user to hold one of `anyOf` roles. */\nexport function RequireRole({ children, anyOf, getRoles, fallback }: RequireRoleProps) {\n const { user, isLoading, isAuthenticated } = useAuth()\n if (isLoading) return <>{fallback ?? <DefaultFallback message=\"Loading…\" />}</>\n if (!isAuthenticated) return <>{fallback ?? <DefaultFallback message=\"Not signed in.\" />}</>\n const roles = getRoles(user)\n const allowed = anyOf.some((r) => roles.includes(r))\n if (!allowed) return <>{fallback ?? <DefaultFallback message=\"You don't have access to this page.\" />}</>\n return <>{children}</>\n}\n","import type { ReactNode } from 'react'\n\nexport interface PageHeaderProps {\n readonly title: string\n readonly action?: ReactNode\n}\n\n/** A page title row with an optional right-aligned action (e.g. a \"New\" button). */\nexport function PageHeader({ title, action }: PageHeaderProps) {\n return (\n <div className=\"flex items-center justify-between\">\n <h1 className=\"text-2xl font-bold\">{title}</h1>\n {action}\n </div>\n )\n}\n\nexport interface PageStackProps {\n readonly children: ReactNode\n readonly spacing?: 4 | 6\n}\n\n/** Vertical spacing container for page sections. */\nexport function PageStack({ children, spacing = 6 }: PageStackProps) {\n return <div className={spacing === 4 ? 'space-y-4' : 'space-y-6'}>{children}</div>\n}\n","import type { ReactNode } from 'react'\n\n/** Renders children only at md and up (desktop table view). */\nexport function DesktopOnly({ children }: { children: ReactNode }) {\n return <div className=\"hidden md:block\">{children}</div>\n}\n\n/** Renders children only below md (stacked-card view). */\nexport function MobileCards({ children }: { children: ReactNode }) {\n return <div className=\"flex flex-col gap-3 md:hidden\">{children}</div>\n}\n\n/** A single stacked card used inside MobileCards. */\nexport function MobileCard({ children }: { children: ReactNode }) {\n return <div className=\"rounded-lg border bg-card p-3 text-sm\">{children}</div>\n}\n\n/** A label/value line inside a MobileCard. */\nexport function MobileField({ label, children }: { label: string; children: ReactNode }) {\n return (\n <div className=\"flex items-start justify-between gap-3 py-0.5\">\n <span className=\"text-muted-foreground\">{label}</span>\n <span className=\"text-right font-medium break-words\">{children}</span>\n </div>\n )\n}\n","import { Loader2 } from 'lucide-react'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@liam-public/browser-react-ui'\n\nexport interface LoadingSpinnerProps {\n readonly size?: 'md' | 'lg'\n}\n\n/** Centered spinner for loading states. */\nexport function LoadingSpinner({ size = 'md' }: LoadingSpinnerProps) {\n const padding = size === 'lg' ? 'py-16' : 'py-8'\n const icon = size === 'lg' ? 'h-8 w-8' : 'h-6 w-6'\n return (\n <div className={`flex items-center justify-center ${padding}`}>\n <Loader2 className={`${icon} animate-spin text-muted-foreground`} />\n </div>\n )\n}\n\nexport interface ConfirmDialogProps {\n readonly open: boolean\n readonly onOpenChange: (open: boolean) => void\n readonly title: string\n readonly description: string\n readonly onConfirm: () => void\n readonly loading?: boolean\n readonly error?: string | null\n /** Confirm-button label (defaults to \"Confirm\"). */\n readonly confirmLabel?: string\n /** Set true for destructive actions (red confirm button). */\n readonly destructive?: boolean\n}\n\n/** A modal confirm dialog with loading + error states. */\nexport function ConfirmDialog({\n open,\n onOpenChange,\n title,\n description,\n onConfirm,\n loading,\n error,\n confirmLabel = 'Confirm',\n destructive,\n}: ConfirmDialogProps) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n {error && <p className=\"text-sm text-destructive\">{error}</p>}\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)} disabled={loading}>\n Cancel\n </Button>\n <Button\n variant={destructive ? 'destructive' : 'default'}\n onClick={onConfirm}\n disabled={loading}\n >\n {loading && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {confirmLabel}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n"],"mappings":"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","names":["Fragment","jsx","jsx","jsxs","jsx","jsxs","Button","jsx","jsxs"]}
1
+ {"version":3,"sources":["../src/cms-layout.tsx","../src/page.tsx","../src/responsive-table.tsx","../src/feedback.tsx","../src/guards.tsx","../src/theme.tsx","../src/toast.tsx","../src/form.tsx","../src/data-table.tsx","../src/dashboard.tsx","../src/analytics.tsx","../src/i18n.tsx","../src/wire.ts"],"sourcesContent":["import { useState } from 'react'\nimport type { ComponentType, ReactNode } from 'react'\nimport { NavLink } from 'react-router-dom'\nimport { Menu } from 'lucide-react'\nimport {\n Button,\n Separator,\n Sheet,\n SheetContent,\n SheetTrigger,\n cn,\n} from '@liam-public/browser-react-ui'\n\n/** A single sidebar navigation entry. `icon` is any lucide-react (or compatible) icon. */\nexport interface NavItem {\n readonly to: string\n readonly label: string\n readonly icon: ComponentType<{ className?: string }>\n /** Exact-match the route (e.g. for the index `/`). */\n readonly end?: boolean\n /** Optional trailing content, e.g. an alert count `<Badge>`. */\n readonly badge?: ReactNode\n}\n\nfunction NavList({ nav, onNavigate }: { nav: readonly NavItem[]; onNavigate?: () => void }) {\n return (\n <>\n {nav.map((item) => (\n <NavLink\n key={item.to}\n to={item.to}\n end={item.end}\n onClick={onNavigate}\n className={({ isActive }) =>\n cn(\n 'flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors hover:bg-accent',\n isActive && 'bg-accent text-accent-foreground',\n )\n }\n >\n <item.icon className=\"h-4 w-4\" />\n {item.label}\n {item.badge != null && <span className=\"ml-auto\">{item.badge}</span>}\n </NavLink>\n ))}\n </>\n )\n}\n\nexport interface CmsLayoutProps {\n readonly children: ReactNode\n /** Sidebar navigation entries. */\n readonly nav: readonly NavItem[]\n /** Brand shown at the top of the sidebar (defaults to \"CMS\"). */\n readonly brand?: ReactNode\n /** Optional content for the top-right of the desktop header (e.g. a user menu). */\n readonly headerRight?: ReactNode\n}\n\n/**\n * Responsive CMS app shell: a desktop sidebar (md+) and a mobile hamburger sheet,\n * with an active-link-aware nav driven entirely by the `nav` prop. Bring your own\n * router (`react-router-dom`) and Tailwind theme.\n */\nexport function CmsLayout({ children, nav, brand = 'CMS', headerRight }: CmsLayoutProps) {\n const [open, setOpen] = useState(false)\n return (\n <div className=\"flex h-screen flex-col md:flex-row\">\n {/* Mobile top bar (hidden at md+) */}\n <header className=\"flex items-center gap-2 border-b p-3 md:hidden\">\n <Sheet open={open} onOpenChange={setOpen}>\n <SheetTrigger asChild>\n <Button variant=\"ghost\" size=\"icon\" aria-label=\"Open menu\" className=\"h-11 w-11\">\n <Menu className=\"h-5 w-5\" />\n </Button>\n </SheetTrigger>\n <SheetContent side=\"left\" className=\"p-0\">\n <div className=\"p-4 font-semibold text-lg\">{brand}</div>\n <Separator />\n <nav className=\"flex flex-col gap-1 p-2\">\n <NavList nav={nav} onNavigate={() => setOpen(false)} />\n </nav>\n </SheetContent>\n </Sheet>\n <span className=\"font-semibold\">{brand}</span>\n </header>\n\n {/* Desktop sidebar (hidden below md) */}\n <aside className=\"hidden md:flex w-56 border-r bg-muted/30 flex-col\">\n <div className=\"p-4 font-semibold text-lg\">{brand}</div>\n <Separator />\n <nav className=\"flex flex-1 flex-col gap-1 p-2\">\n <NavList nav={nav} />\n </nav>\n </aside>\n\n <main className=\"flex-1 overflow-auto\">\n {headerRight && (\n <div className=\"hidden md:flex items-center justify-end border-b p-3\">{headerRight}</div>\n )}\n <div className=\"p-4 md:p-6\">{children}</div>\n </main>\n </div>\n )\n}\n","import type { ReactNode } from 'react'\n\nexport interface PageHeaderProps {\n readonly title: string\n readonly action?: ReactNode\n}\n\n/** A page title row with an optional right-aligned action (e.g. a \"New\" button). */\nexport function PageHeader({ title, action }: PageHeaderProps) {\n return (\n <div className=\"flex items-center justify-between\">\n <h1 className=\"text-2xl font-bold\">{title}</h1>\n {action}\n </div>\n )\n}\n\nexport interface PageStackProps {\n readonly children: ReactNode\n readonly spacing?: 4 | 6\n}\n\n/** Vertical spacing container for page sections. */\nexport function PageStack({ children, spacing = 6 }: PageStackProps) {\n return <div className={spacing === 4 ? 'space-y-4' : 'space-y-6'}>{children}</div>\n}\n","import type { ReactNode } from 'react'\n\n/** Renders children only at md and up (desktop table view). */\nexport function DesktopOnly({ children }: { children: ReactNode }) {\n return <div className=\"hidden md:block\">{children}</div>\n}\n\n/** Renders children only below md (stacked-card view). */\nexport function MobileCards({ children }: { children: ReactNode }) {\n return <div className=\"flex flex-col gap-3 md:hidden\">{children}</div>\n}\n\n/** A single stacked card used inside MobileCards. */\nexport function MobileCard({ children }: { children: ReactNode }) {\n return <div className=\"rounded-lg border bg-card p-3 text-sm\">{children}</div>\n}\n\n/** A label/value line inside a MobileCard. */\nexport function MobileField({ label, children }: { label: string; children: ReactNode }) {\n return (\n <div className=\"flex items-start justify-between gap-3 py-0.5\">\n <span className=\"text-muted-foreground\">{label}</span>\n <span className=\"text-right font-medium break-words\">{children}</span>\n </div>\n )\n}\n","import { Loader2 } from 'lucide-react'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '@liam-public/browser-react-ui'\n\nexport interface LoadingSpinnerProps {\n readonly size?: 'md' | 'lg'\n}\n\n/** Centered spinner for loading states. */\nexport function LoadingSpinner({ size = 'md' }: LoadingSpinnerProps) {\n const padding = size === 'lg' ? 'py-16' : 'py-8'\n const icon = size === 'lg' ? 'h-8 w-8' : 'h-6 w-6'\n return (\n <div className={`flex items-center justify-center ${padding}`}>\n <Loader2 className={`${icon} animate-spin text-muted-foreground`} />\n </div>\n )\n}\n\nexport interface ConfirmDialogProps {\n readonly open: boolean\n readonly onOpenChange: (open: boolean) => void\n readonly title: string\n readonly description: string\n readonly onConfirm: () => void\n readonly loading?: boolean\n readonly error?: string | null\n /** Confirm-button label (defaults to \"Confirm\"). */\n readonly confirmLabel?: string\n /** Set true for destructive actions (red confirm button). */\n readonly destructive?: boolean\n}\n\n/** A modal confirm dialog with loading + error states. */\nexport function ConfirmDialog({\n open,\n onOpenChange,\n title,\n description,\n onConfirm,\n loading,\n error,\n confirmLabel = 'Confirm',\n destructive,\n}: ConfirmDialogProps) {\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{title}</DialogTitle>\n <DialogDescription>{description}</DialogDescription>\n </DialogHeader>\n {error && <p className=\"text-sm text-destructive\">{error}</p>}\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => onOpenChange(false)} disabled={loading}>\n Cancel\n </Button>\n <Button\n variant={destructive ? 'destructive' : 'default'}\n onClick={onConfirm}\n disabled={loading}\n >\n {loading && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n {confirmLabel}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n","import { useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport { useAuth } from '@liam-public/browser-react-auth'\n\nfunction DefaultFallback({ message }: { message: string }) {\n return (\n <div className=\"flex h-screen items-center justify-center\">\n <p className=\"text-sm text-muted-foreground\">{message}</p>\n </div>\n )\n}\n\nexport interface RequireAuthProps {\n readonly children: ReactNode\n /** Rendered while loading or when unauthenticated (defaults to a \"Redirecting…\" line). */\n readonly fallback?: ReactNode\n /**\n * Called once when the user is confirmed unauthenticated — e.g. trigger the OIDC\n * redirect from `@liam-workspace/auth-client`, or navigate to `/login`.\n */\n readonly onUnauthenticated?: () => void\n}\n\n/**\n * Gate that renders `children` only for an authenticated user. Wraps `useAuth()` from\n * `@liam-public/browser-react-auth`, filling the route-guard gap that package leaves open.\n */\nexport function RequireAuth({ children, fallback, onUnauthenticated }: RequireAuthProps) {\n const { isAuthenticated, isLoading } = useAuth()\n\n useEffect(() => {\n if (!isLoading && !isAuthenticated) onUnauthenticated?.()\n }, [isLoading, isAuthenticated, onUnauthenticated])\n\n if (isLoading) return <>{fallback ?? <DefaultFallback message=\"Loading…\" />}</>\n if (!isAuthenticated) return <>{fallback ?? <DefaultFallback message=\"Redirecting to sign in…\" />}</>\n return <>{children}</>\n}\n\nexport interface RequireRoleProps {\n readonly children: ReactNode\n /** The user is allowed if they hold ANY of these roles. */\n readonly anyOf: readonly string[]\n /** Extract the user's roles from the auth `user` object (shape is app-specific). */\n readonly getRoles: (user: unknown) => readonly string[]\n /** Rendered when the user lacks the required role(s). */\n readonly fallback?: ReactNode\n}\n\n/** Gate that additionally requires the authenticated user to hold one of `anyOf` roles. */\nexport function RequireRole({ children, anyOf, getRoles, fallback }: RequireRoleProps) {\n const { user, isLoading, isAuthenticated } = useAuth()\n if (isLoading) return <>{fallback ?? <DefaultFallback message=\"Loading…\" />}</>\n if (!isAuthenticated) return <>{fallback ?? <DefaultFallback message=\"Not signed in.\" />}</>\n const roles = getRoles(user)\n const allowed = anyOf.some((r) => roles.includes(r))\n if (!allowed) return <>{fallback ?? <DefaultFallback message=\"You don't have access to this page.\" />}</>\n return <>{children}</>\n}\n","import { createContext, useCallback, useContext, useEffect, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { Moon, Sun } from 'lucide-react'\nimport { Button } from '@liam-public/browser-react-ui'\n\nexport type Theme = 'light' | 'dark' | 'system'\n\nexport interface ThemeContextValue {\n readonly theme: Theme\n readonly resolved: 'light' | 'dark'\n readonly setTheme: (theme: Theme) => void\n readonly toggle: () => void\n}\n\nconst ThemeContext = createContext<ThemeContextValue | null>(null)\n\nfunction readStored(key: string): string | null {\n try {\n return typeof localStorage !== 'undefined' ? localStorage.getItem(key) : null\n } catch {\n return null // localStorage can throw (Safari private mode, stubbed/locked-down environments)\n }\n}\n\nfunction writeStored(key: string, value: string): void {\n try {\n if (typeof localStorage !== 'undefined') localStorage.setItem(key, value)\n } catch {\n /* ignore — running without persistence is acceptable */\n }\n}\n\nfunction prefersDark(): boolean {\n return typeof window !== 'undefined' && typeof window.matchMedia === 'function'\n ? window.matchMedia('(prefers-color-scheme: dark)').matches\n : false\n}\n\nfunction resolve(theme: Theme): 'light' | 'dark' {\n return theme === 'system' ? (prefersDark() ? 'dark' : 'light') : theme\n}\n\nfunction applyClass(resolved: 'light' | 'dark') {\n if (typeof document !== 'undefined') {\n document.documentElement.classList.toggle('dark', resolved === 'dark')\n }\n}\n\nexport interface ThemeProviderProps {\n readonly children: ReactNode\n readonly defaultTheme?: Theme\n readonly storageKey?: string\n}\n\n/** Manages light/dark/system theme, persists the choice, and toggles `.dark` on `<html>`. */\nexport function ThemeProvider({\n children,\n defaultTheme = 'system',\n storageKey = 'cms-theme',\n}: ThemeProviderProps) {\n const [theme, setThemeState] = useState<Theme>(\n () => (readStored(storageKey) as Theme | null) ?? defaultTheme,\n )\n\n useEffect(() => {\n applyClass(resolve(theme))\n }, [theme])\n\n // Track the OS preference while in `system` mode.\n useEffect(() => {\n if (theme !== 'system' || typeof window === 'undefined' || !window.matchMedia) return\n const mq = window.matchMedia('(prefers-color-scheme: dark)')\n const onChange = () => applyClass(resolve('system'))\n mq.addEventListener('change', onChange)\n return () => mq.removeEventListener('change', onChange)\n }, [theme])\n\n const setTheme = useCallback(\n (next: Theme) => {\n writeStored(storageKey, next)\n setThemeState(next)\n },\n [storageKey],\n )\n\n const toggle = useCallback(() => {\n setTheme(resolve(theme) === 'dark' ? 'light' : 'dark')\n }, [theme, setTheme])\n\n return (\n <ThemeContext.Provider value={{ theme, resolved: resolve(theme), setTheme, toggle }}>\n {children}\n </ThemeContext.Provider>\n )\n}\n\nexport function useTheme(): ThemeContextValue {\n const ctx = useContext(ThemeContext)\n if (!ctx) throw new Error('useTheme must be used within a <ThemeProvider>')\n return ctx\n}\n\n/** A button that toggles light/dark. */\nexport function ThemeToggle() {\n const { resolved, toggle } = useTheme()\n return (\n <Button variant=\"ghost\" size=\"icon\" onClick={toggle} aria-label=\"Toggle dark mode\">\n {resolved === 'dark' ? <Sun className=\"h-5 w-5\" /> : <Moon className=\"h-5 w-5\" />}\n </Button>\n )\n}\n","import { createContext, useCallback, useContext, useRef, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { CheckCircle2, Info, X, XCircle } from 'lucide-react'\nimport { cn } from '@liam-public/browser-react-ui'\n\nexport type ToastVariant = 'default' | 'success' | 'error'\n\nexport interface ToastOptions {\n readonly title?: string\n readonly description?: string\n readonly variant?: ToastVariant\n /** Auto-dismiss after this many ms (default 5000; 0 disables). */\n readonly duration?: number\n}\n\ninterface ToastItem extends ToastOptions {\n readonly id: number\n readonly variant: ToastVariant\n}\n\nexport interface ToastContextValue {\n toast: (options: ToastOptions) => number\n success: (title: string, description?: string) => number\n error: (title: string, description?: string) => number\n dismiss: (id: number) => void\n}\n\nconst ToastContext = createContext<ToastContextValue | null>(null)\n\nconst ICONS: Record<ToastVariant, typeof Info> = {\n default: Info,\n success: CheckCircle2,\n error: XCircle,\n}\n\n/** Wrap the app to enable `useToast()`. Renders the toast viewport itself. */\nexport function ToastProvider({ children }: { children: ReactNode }) {\n const [items, setItems] = useState<ToastItem[]>([])\n const seq = useRef(0)\n\n const dismiss = useCallback((id: number) => {\n setItems((prev) => prev.filter((t) => t.id !== id))\n }, [])\n\n const toast = useCallback(\n (options: ToastOptions) => {\n const id = ++seq.current\n const item: ToastItem = { id, variant: options.variant ?? 'default', ...options }\n setItems((prev) => [...prev, item])\n const duration = options.duration ?? 5000\n if (duration > 0) setTimeout(() => dismiss(id), duration)\n return id\n },\n [dismiss],\n )\n\n const success = useCallback((title: string, description?: string) => toast({ title, description, variant: 'success' }), [toast])\n const error = useCallback((title: string, description?: string) => toast({ title, description, variant: 'error' }), [toast])\n\n return (\n <ToastContext.Provider value={{ toast, success, error, dismiss }}>\n {children}\n <div className=\"pointer-events-none fixed bottom-4 right-4 z-50 flex w-full max-w-sm flex-col gap-2\">\n {items.map((t) => {\n const Icon = ICONS[t.variant]\n return (\n <div\n key={t.id}\n role=\"status\"\n className={cn(\n 'pointer-events-auto flex items-start gap-2 rounded-lg border bg-card p-3 text-sm shadow-lg',\n t.variant === 'error' && 'border-destructive/40',\n t.variant === 'success' && 'border-green-500/40',\n )}\n >\n <Icon\n className={cn(\n 'mt-0.5 h-4 w-4 shrink-0',\n t.variant === 'error' && 'text-destructive',\n t.variant === 'success' && 'text-green-600',\n t.variant === 'default' && 'text-muted-foreground',\n )}\n />\n <div className=\"flex-1\">\n {t.title && <p className=\"font-medium\">{t.title}</p>}\n {t.description && <p className=\"text-muted-foreground\">{t.description}</p>}\n </div>\n <button\n onClick={() => dismiss(t.id)}\n aria-label=\"Dismiss\"\n className=\"text-muted-foreground hover:text-foreground\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n </div>\n )\n })}\n </div>\n </ToastContext.Provider>\n )\n}\n\nexport function useToast(): ToastContextValue {\n const ctx = useContext(ToastContext)\n if (!ctx) throw new Error('useToast must be used within a <ToastProvider>')\n return ctx\n}\n","import type { ReactNode } from 'react'\nimport {\n FormProvider,\n useForm,\n useFormContext,\n type DefaultValues,\n type FieldValues,\n type Path,\n type SubmitHandler,\n type UseFormReturn,\n} from 'react-hook-form'\nimport { zodResolver } from '@hookform/resolvers/zod'\nimport type { ZodType } from 'zod'\nimport { Button, Input, Label } from '@liam-public/browser-react-ui'\n\n/** A react-hook-form instance validated by a Zod schema. */\nexport function useResourceForm<T extends FieldValues>(\n schema: ZodType<T>,\n defaultValues?: DefaultValues<T>,\n): UseFormReturn<T> {\n return useForm<T>({\n // resolver generics across zod 3/4 are loose; the runtime validation is correct.\n resolver: zodResolver(schema as never) as never,\n defaultValues,\n })\n}\n\nexport interface ResourceFormProps<T extends FieldValues> {\n readonly schema: ZodType<T>\n readonly defaultValues?: DefaultValues<T>\n readonly onSubmit: SubmitHandler<T>\n readonly children: ReactNode\n readonly submitLabel?: string\n /** Optional extra footer content (e.g. a Cancel button) rendered next to Submit. */\n readonly footer?: ReactNode\n}\n\n/**\n * A Zod-validated form. Wrap `<TextField>`/`<TextareaField>` (or any field using\n * `useFormContext`) as children; submit is wired with validation + a disabled-while-submitting button.\n */\nexport function ResourceForm<T extends FieldValues>({\n schema,\n defaultValues,\n onSubmit,\n children,\n submitLabel = 'Save',\n footer,\n}: ResourceFormProps<T>) {\n const form = useResourceForm<T>(schema, defaultValues)\n return (\n <FormProvider {...form}>\n <form onSubmit={form.handleSubmit(onSubmit)} className=\"space-y-4\">\n {children}\n <div className=\"flex items-center justify-end gap-2\">\n {footer}\n <Button type=\"submit\" disabled={form.formState.isSubmitting}>\n {submitLabel}\n </Button>\n </div>\n </form>\n </FormProvider>\n )\n}\n\ninterface FieldProps<T extends FieldValues> {\n readonly name: Path<T>\n readonly label: string\n readonly placeholder?: string\n readonly type?: string\n}\n\nfunction useFieldError<T extends FieldValues>(name: Path<T>): string | undefined {\n const {\n formState: { errors },\n } = useFormContext<T>()\n const err = errors[name]\n return err?.message as string | undefined\n}\n\n/** A labelled text input bound to the surrounding `ResourceForm` with inline validation errors. */\nexport function TextField<T extends FieldValues>({ name, label, placeholder, type = 'text' }: FieldProps<T>) {\n const { register } = useFormContext<T>()\n const error = useFieldError<T>(name)\n return (\n <div className=\"space-y-1.5\">\n <Label htmlFor={name}>{label}</Label>\n <Input id={name} type={type} placeholder={placeholder} aria-invalid={!!error} {...register(name)} />\n {error && <p className=\"text-sm text-destructive\">{error}</p>}\n </div>\n )\n}\n\n/** A labelled textarea bound to the surrounding `ResourceForm`. */\nexport function TextareaField<T extends FieldValues>({ name, label, placeholder }: FieldProps<T>) {\n const { register } = useFormContext<T>()\n const error = useFieldError<T>(name)\n return (\n <div className=\"space-y-1.5\">\n <Label htmlFor={name}>{label}</Label>\n <textarea\n id={name}\n placeholder={placeholder}\n aria-invalid={!!error}\n className=\"flex min-h-20 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n {...register(name)}\n />\n {error && <p className=\"text-sm text-destructive\">{error}</p>}\n </div>\n )\n}\n","import { useState } from 'react'\nimport type { ReactNode } from 'react'\nimport { ChevronLeft, ChevronRight } from 'lucide-react'\nimport {\n Button,\n Table,\n TableBody,\n TableCell,\n TableHead,\n TableHeader,\n TableRow,\n} from '@liam-public/browser-react-ui'\nimport { useList } from '@liam-public/browser-react-data'\nimport type { ListParams } from '@liam-public/browser-react-data'\nimport { LoadingSpinner } from './feedback.js'\n\nexport interface Column<T> {\n /** Key into the row, used for the default cell value + as a React key. */\n readonly key: string\n readonly header: string\n /** Custom cell renderer; defaults to `String(row[key])`. */\n readonly render?: (row: T) => ReactNode\n readonly className?: string\n}\n\nexport interface PaginationProps {\n readonly page: number\n readonly pageCount: number\n readonly onPageChange: (page: number) => void\n}\n\n/** Prev/next pager with a page indicator. */\nexport function Pagination({ page, pageCount, onPageChange }: PaginationProps) {\n if (pageCount <= 1) return null\n return (\n <div className=\"flex items-center justify-end gap-2 pt-3 text-sm\">\n <span className=\"text-muted-foreground\">\n Page {page} of {pageCount}\n </span>\n <Button variant=\"outline\" size=\"icon-sm\" disabled={page <= 1} onClick={() => onPageChange(page - 1)} aria-label=\"Previous page\">\n <ChevronLeft className=\"h-4 w-4\" />\n </Button>\n <Button variant=\"outline\" size=\"icon-sm\" disabled={page >= pageCount} onClick={() => onPageChange(page + 1)} aria-label=\"Next page\">\n <ChevronRight className=\"h-4 w-4\" />\n </Button>\n </div>\n )\n}\n\nexport interface DataTableProps<T> {\n readonly resource: string\n readonly columns: readonly Column<T>[]\n readonly pageSize?: number\n /** Extra list params (sort/filter) merged into each query. */\n readonly params?: Omit<ListParams, 'page' | 'pageSize'>\n readonly emptyMessage?: string\n}\n\n/**\n * A paginated table for a resource, fed by `useList` from `@liam-public/browser-react-data`.\n * Requires a `<DataProviderProvider>` + a `QueryClientProvider` above it.\n */\nexport function DataTable<T extends Record<string, unknown>>({\n resource,\n columns,\n pageSize = 20,\n params,\n emptyMessage = 'No records.',\n}: DataTableProps<T>) {\n const [page, setPage] = useState(1)\n const { data, isLoading, isError, error } = useList<T>(resource, { page, pageSize, ...params })\n\n if (isLoading) return <LoadingSpinner />\n if (isError) return <p className=\"py-8 text-center text-sm text-destructive\">{(error as Error)?.message ?? 'Failed to load.'}</p>\n\n const rows = data?.data ?? []\n const total = data?.total ?? 0\n const pageCount = Math.max(1, Math.ceil(total / pageSize))\n\n if (rows.length === 0) {\n return <p className=\"py-8 text-center text-sm text-muted-foreground\">{emptyMessage}</p>\n }\n\n return (\n <div>\n <Table>\n <TableHeader>\n <TableRow>\n {columns.map((c) => (\n <TableHead key={c.key} className={c.className}>\n {c.header}\n </TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {rows.map((row, i) => (\n <TableRow key={(row.id as string) ?? i}>\n {columns.map((c) => (\n <TableCell key={c.key} className={c.className}>\n {c.render ? c.render(row) : String(row[c.key] ?? '')}\n </TableCell>\n ))}\n </TableRow>\n ))}\n </TableBody>\n </Table>\n <Pagination page={page} pageCount={pageCount} onPageChange={setPage} />\n </div>\n )\n}\n","import type { ComponentType, ReactNode } from 'react'\nimport { Card, CardContent, CardHeader, CardTitle, cn } from '@liam-public/browser-react-ui'\n\nexport interface StatCardProps {\n readonly label: string\n readonly value: ReactNode\n readonly icon?: ComponentType<{ className?: string }>\n readonly hint?: string\n readonly className?: string\n}\n\n/** A KPI card: label, big value, optional icon + hint. */\nexport function StatCard({ label, value, icon: Icon, hint, className }: StatCardProps) {\n return (\n <Card className={className}>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0 pb-2\">\n <CardTitle className=\"text-sm font-medium text-muted-foreground\">{label}</CardTitle>\n {Icon && <Icon className=\"h-4 w-4 text-muted-foreground\" />}\n </CardHeader>\n <CardContent>\n <div className=\"text-2xl font-bold\">{value}</div>\n {hint && <p className=\"text-xs text-muted-foreground\">{hint}</p>}\n </CardContent>\n </Card>\n )\n}\n\n/** Responsive grid of KPI cards (1 / 2 / 4 columns). */\nexport function StatGrid({ children, columns = 4 }: { children: ReactNode; columns?: 2 | 3 | 4 }) {\n const cols = columns === 2 ? 'sm:grid-cols-2' : columns === 3 ? 'sm:grid-cols-2 lg:grid-cols-3' : 'sm:grid-cols-2 lg:grid-cols-4'\n return <div className={cn('grid grid-cols-1 gap-4', cols)}>{children}</div>\n}\n\nexport interface ChartCardProps {\n readonly title: string\n readonly children: ReactNode\n readonly height?: number\n readonly action?: ReactNode\n}\n\n/**\n * A titled card sized for a chart. Put a recharts `<ResponsiveContainer>` (or any chart)\n * inside; `recharts` is a dependency so apps don't need to install it separately.\n */\nexport function ChartCard({ title, children, height = 280, action }: ChartCardProps) {\n return (\n <Card>\n <CardHeader className=\"flex flex-row items-center justify-between space-y-0\">\n <CardTitle className=\"text-base\">{title}</CardTitle>\n {action}\n </CardHeader>\n <CardContent>\n <div style={{ height }}>{children}</div>\n </CardContent>\n </Card>\n )\n}\n\nexport {\n ResponsiveContainer,\n LineChart,\n Line,\n BarChart,\n Bar,\n AreaChart,\n Area,\n XAxis,\n YAxis,\n CartesianGrid,\n Tooltip,\n Legend,\n} from 'recharts'\n","import { createContext, useContext, useEffect, useRef } from 'react'\nimport type { ReactNode } from 'react'\nimport { useLocation } from 'react-router-dom'\n\n/** Pluggable analytics sink. Implement once (or use a provided adapter) and call from anywhere. */\nexport interface AnalyticsAdapter {\n pageView(path: string): void\n track(event: string, props?: Record<string, unknown>): void\n}\n\nconst noop: AnalyticsAdapter = { pageView: () => {}, track: () => {} }\nconst AnalyticsContext = createContext<AnalyticsAdapter>(noop)\n\nexport function AnalyticsProvider({ adapter, children }: { adapter: AnalyticsAdapter; children: ReactNode }) {\n return <AnalyticsContext.Provider value={adapter}>{children}</AnalyticsContext.Provider>\n}\n\nexport function useAnalytics(): AnalyticsAdapter {\n return useContext(AnalyticsContext)\n}\n\n/** Fire a `pageView` on every router path change. Mount once inside the Router + AnalyticsProvider. */\nexport function usePageViews() {\n const analytics = useAnalytics()\n const location = useLocation()\n const last = useRef<string>('')\n useEffect(() => {\n const path = location.pathname + location.search\n if (path !== last.current) {\n last.current = path\n analytics.pageView(path)\n }\n }, [location, analytics])\n}\n\n/** A beacon adapter that POSTs events to an ingest endpoint (uses `sendBeacon` when available). */\nexport function createBeaconAnalytics(options: { endpoint: string; app?: string }): AnalyticsAdapter {\n const send = (type: 'pageView' | 'track', payload: Record<string, unknown>) => {\n const body = JSON.stringify({ type, app: options.app, ts: new Date().toISOString(), ...payload })\n if (typeof navigator !== 'undefined' && navigator.sendBeacon) {\n navigator.sendBeacon(options.endpoint, new Blob([body], { type: 'application/json' }))\n } else if (typeof fetch !== 'undefined') {\n void fetch(options.endpoint, { method: 'POST', headers: { 'content-type': 'application/json' }, body, keepalive: true })\n }\n }\n return {\n pageView: (path) => send('pageView', { path }),\n track: (event, props) => send('track', { event, props }),\n }\n}\n\n/** A console adapter for local development. */\nexport function createConsoleAnalytics(): AnalyticsAdapter {\n return {\n pageView: (path) => console.info('[analytics] pageView', path),\n track: (event, props) => console.info('[analytics] track', event, props),\n }\n}\n","import type { ReactNode } from 'react'\nimport i18next, { type i18n as I18nInstance, type Resource } from 'i18next'\nimport { I18nextProvider, initReactI18next, useTranslation } from 'react-i18next'\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '@liam-public/browser-react-ui'\n\nexport interface CreateI18nOptions {\n /** i18next resource bundles: `{ en: { translation: {...} }, vi: { translation: {...} } }`. */\n readonly resources: Resource\n readonly lng?: string\n readonly fallbackLng?: string\n}\n\n/** Create a configured, isolated i18next instance for the CMS. */\nexport function createI18n(options: CreateI18nOptions): I18nInstance {\n const instance = i18next.createInstance()\n void instance.use(initReactI18next).init({\n resources: options.resources,\n lng: options.lng ?? 'en',\n fallbackLng: options.fallbackLng ?? 'en',\n interpolation: { escapeValue: false },\n })\n return instance\n}\n\nexport function I18nProvider({ i18n, children }: { i18n: I18nInstance; children: ReactNode }) {\n return <I18nextProvider i18n={i18n}>{children}</I18nextProvider>\n}\n\nexport { useTranslation }\n\nexport interface LanguageOption {\n readonly code: string\n readonly label: string\n}\n\n/** A dropdown that switches the active i18next language. */\nexport function LanguageSwitcher({ languages }: { languages: readonly LanguageOption[] }) {\n const { i18n } = useTranslation()\n return (\n <Select value={i18n.language} onValueChange={(value) => void i18n.changeLanguage(value)}>\n <SelectTrigger className=\"w-36\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n {languages.map((l) => (\n <SelectItem key={l.code} value={l.code}>\n {l.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n )\n}\n","// \"Wire the existing\": re-export the already-solved cross-cutting pieces so a CMS app\n// gets auth + datetime/number formatting from this one package.\n\nexport { AuthProvider, useAuth, createAuthenticatedFetch } from '@liam-public/browser-react-auth'\nexport type { AuthContextValue } from '@liam-public/browser-react-auth'\n\nexport {\n formatDate,\n formatCurrency,\n formatNumber,\n formatPercentage,\n formatFileSize,\n selectText,\n isLocalizedText,\n} from '@liam-public/text'\nexport type { LocalizedText } from '@liam-public/text'\n"],"mappings":"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","names":["jsx","jsxs","jsx","jsxs","Button","jsx","jsxs","Fragment","jsx","useEffect","useState","Button","jsx","createContext","useCallback","useContext","useState","cn","jsx","jsxs","Button","jsx","jsxs","useState","Button","jsx","jsxs","Button","useState","cn","jsx","jsxs","createContext","useContext","useEffect","useRef","jsx","jsx","jsxs","jsxs","jsx","useAuth"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@liam-public/browser-react-cms",
3
- "version": "0.1.0",
4
- "description": "Reusable CMS/admin chrome: responsive app-shell, auth route-guards, and page primitives, on Tailwind v4 + Radix.",
3
+ "version": "0.2.0",
4
+ "description": "Batteries-included CMS framework: app-shell, auth guards, dark mode, toasts, Zod forms, data-table + pagination, dashboards, analytics, and i18n, on Tailwind v4 + Radix.",
5
5
  "type": "module",
6
6
  "liamCompatibility": {
7
7
  "runtime": [
@@ -30,23 +30,35 @@
30
30
  "registry": "https://registry.npmjs.org/"
31
31
  },
32
32
  "peerDependencies": {
33
+ "@tanstack/react-query": "^5.0.0",
33
34
  "react": "^19.0.0",
34
35
  "react-dom": "^19.0.0",
35
- "react-router-dom": "^7.0.0"
36
+ "react-hook-form": "^7.0.0",
37
+ "react-router-dom": "^7.0.0",
38
+ "zod": "^3.23.0 || ^4.0.0"
36
39
  },
37
40
  "dependencies": {
41
+ "@hookform/resolvers": "^5.0.0",
42
+ "i18next": "^25.0.0",
38
43
  "lucide-react": "^0.563.0",
44
+ "react-i18next": "^15.0.0",
45
+ "recharts": "^2.13.0",
39
46
  "@liam-public/browser-react-auth": "0.1.0",
47
+ "@liam-public/browser-react-data": "0.1.0",
48
+ "@liam-public/text": "0.1.0",
40
49
  "@liam-public/browser-react-ui": "0.1.0"
41
50
  },
42
51
  "devDependencies": {
52
+ "@tanstack/react-query": "^5.0.0",
43
53
  "@testing-library/react": "^16.3.0",
44
54
  "@types/react": "^19.2.2",
45
55
  "@types/react-dom": "^19.2.2",
46
56
  "jsdom": "^27.0.1",
47
57
  "react": "^19.2.0",
48
58
  "react-dom": "^19.2.0",
49
- "react-router-dom": "^7.0.0"
59
+ "react-hook-form": "^7.53.0",
60
+ "react-router-dom": "^7.0.0",
61
+ "zod": "^4.0.0"
50
62
  },
51
63
  "scripts": {
52
64
  "build": "tsup src/index.ts --format esm --dts --sourcemap",
package/theme.css CHANGED
@@ -1,32 +1,99 @@
1
1
  @import 'tailwindcss';
2
2
 
3
- @theme inline {
4
- --color-background: oklch(1 0 0);
5
- --color-foreground: oklch(0.145 0 0);
6
- --color-card: oklch(1 0 0);
7
- --color-card-foreground: oklch(0.145 0 0);
8
- --color-popover: oklch(1 0 0);
9
- --color-popover-foreground: oklch(0.145 0 0);
10
- --color-primary: oklch(0.205 0 0);
11
- --color-primary-foreground: oklch(0.985 0 0);
12
- --color-secondary: oklch(0.97 0 0);
13
- --color-secondary-foreground: oklch(0.205 0 0);
14
- --color-muted: oklch(0.97 0 0);
15
- --color-muted-foreground: oklch(0.556 0 0);
16
- --color-accent: oklch(0.97 0 0);
17
- --color-accent-foreground: oklch(0.205 0 0);
18
- --color-destructive: oklch(0.577 0.245 27.325);
19
- --color-destructive-foreground: oklch(0.577 0.245 27.325);
20
- --color-border: oklch(0.922 0 0);
21
- --color-input: oklch(0.922 0 0);
22
- --color-ring: oklch(0.708 0 0);
3
+ /* Enable `dark:` utilities driven by a `.dark` class on an ancestor (toggled by ThemeProvider). */
4
+ @custom-variant dark (&:is(.dark *));
5
+
6
+ :root {
23
7
  --radius: 0.625rem;
24
- --color-sidebar: oklch(0.985 0 0);
25
- --color-sidebar-foreground: oklch(0.145 0 0);
26
- --color-sidebar-primary: oklch(0.205 0 0);
27
- --color-sidebar-primary-foreground: oklch(0.985 0 0);
28
- --color-sidebar-accent: oklch(0.97 0 0);
29
- --color-sidebar-accent-foreground: oklch(0.205 0 0);
30
- --color-sidebar-border: oklch(0.922 0 0);
31
- --color-sidebar-ring: oklch(0.708 0 0);
8
+ --background: oklch(1 0 0);
9
+ --foreground: oklch(0.145 0 0);
10
+ --card: oklch(1 0 0);
11
+ --card-foreground: oklch(0.145 0 0);
12
+ --popover: oklch(1 0 0);
13
+ --popover-foreground: oklch(0.145 0 0);
14
+ --primary: oklch(0.205 0 0);
15
+ --primary-foreground: oklch(0.985 0 0);
16
+ --secondary: oklch(0.97 0 0);
17
+ --secondary-foreground: oklch(0.205 0 0);
18
+ --muted: oklch(0.97 0 0);
19
+ --muted-foreground: oklch(0.556 0 0);
20
+ --accent: oklch(0.97 0 0);
21
+ --accent-foreground: oklch(0.205 0 0);
22
+ --destructive: oklch(0.577 0.245 27.325);
23
+ --destructive-foreground: oklch(0.985 0 0);
24
+ --border: oklch(0.922 0 0);
25
+ --input: oklch(0.922 0 0);
26
+ --ring: oklch(0.708 0 0);
27
+ --sidebar: oklch(0.985 0 0);
28
+ --sidebar-foreground: oklch(0.145 0 0);
29
+ --sidebar-primary: oklch(0.205 0 0);
30
+ --sidebar-primary-foreground: oklch(0.985 0 0);
31
+ --sidebar-accent: oklch(0.97 0 0);
32
+ --sidebar-accent-foreground: oklch(0.205 0 0);
33
+ --sidebar-border: oklch(0.922 0 0);
34
+ --sidebar-ring: oklch(0.708 0 0);
35
+ }
36
+
37
+ .dark {
38
+ --background: oklch(0.145 0 0);
39
+ --foreground: oklch(0.985 0 0);
40
+ --card: oklch(0.205 0 0);
41
+ --card-foreground: oklch(0.985 0 0);
42
+ --popover: oklch(0.205 0 0);
43
+ --popover-foreground: oklch(0.985 0 0);
44
+ --primary: oklch(0.922 0 0);
45
+ --primary-foreground: oklch(0.205 0 0);
46
+ --secondary: oklch(0.269 0 0);
47
+ --secondary-foreground: oklch(0.985 0 0);
48
+ --muted: oklch(0.269 0 0);
49
+ --muted-foreground: oklch(0.708 0 0);
50
+ --accent: oklch(0.269 0 0);
51
+ --accent-foreground: oklch(0.985 0 0);
52
+ --destructive: oklch(0.704 0.191 22.216);
53
+ --destructive-foreground: oklch(0.985 0 0);
54
+ --border: oklch(1 0 0 / 10%);
55
+ --input: oklch(1 0 0 / 15%);
56
+ --ring: oklch(0.556 0 0);
57
+ --sidebar: oklch(0.205 0 0);
58
+ --sidebar-foreground: oklch(0.985 0 0);
59
+ --sidebar-primary: oklch(0.922 0 0);
60
+ --sidebar-primary-foreground: oklch(0.205 0 0);
61
+ --sidebar-accent: oklch(0.269 0 0);
62
+ --sidebar-accent-foreground: oklch(0.985 0 0);
63
+ --sidebar-border: oklch(1 0 0 / 10%);
64
+ --sidebar-ring: oklch(0.556 0 0);
65
+ }
66
+
67
+ @theme inline {
68
+ --radius-sm: calc(var(--radius) - 4px);
69
+ --radius-md: calc(var(--radius) - 2px);
70
+ --radius-lg: var(--radius);
71
+ --radius-xl: calc(var(--radius) + 4px);
72
+ --color-background: var(--background);
73
+ --color-foreground: var(--foreground);
74
+ --color-card: var(--card);
75
+ --color-card-foreground: var(--card-foreground);
76
+ --color-popover: var(--popover);
77
+ --color-popover-foreground: var(--popover-foreground);
78
+ --color-primary: var(--primary);
79
+ --color-primary-foreground: var(--primary-foreground);
80
+ --color-secondary: var(--secondary);
81
+ --color-secondary-foreground: var(--secondary-foreground);
82
+ --color-muted: var(--muted);
83
+ --color-muted-foreground: var(--muted-foreground);
84
+ --color-accent: var(--accent);
85
+ --color-accent-foreground: var(--accent-foreground);
86
+ --color-destructive: var(--destructive);
87
+ --color-destructive-foreground: var(--destructive-foreground);
88
+ --color-border: var(--border);
89
+ --color-input: var(--input);
90
+ --color-ring: var(--ring);
91
+ --color-sidebar: var(--sidebar);
92
+ --color-sidebar-foreground: var(--sidebar-foreground);
93
+ --color-sidebar-primary: var(--sidebar-primary);
94
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
95
+ --color-sidebar-accent: var(--sidebar-accent);
96
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
97
+ --color-sidebar-border: var(--sidebar-border);
98
+ --color-sidebar-ring: var(--sidebar-ring);
32
99
  }