@pattern-stack/frontend-patterns 0.2.0-alpha.4 → 0.2.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/atoms/components/data/DataTable/DataTable.d.ts +1 -1
  2. package/dist/atoms/components/data/DataTable/DataTable.d.ts.map +1 -1
  3. package/dist/atoms/components/data/DataTable/DataTable.types.d.ts +28 -0
  4. package/dist/atoms/components/data/DataTable/DataTable.types.d.ts.map +1 -1
  5. package/dist/atoms/components/data/index.d.ts +1 -0
  6. package/dist/atoms/components/data/index.d.ts.map +1 -1
  7. package/dist/atoms/composed/ConnectionStatus/ConnectionStatus.d.ts +16 -0
  8. package/dist/atoms/composed/ConnectionStatus/ConnectionStatus.d.ts.map +1 -0
  9. package/dist/atoms/composed/ConnectionStatus/index.d.ts +3 -0
  10. package/dist/atoms/composed/ConnectionStatus/index.d.ts.map +1 -0
  11. package/dist/atoms/hooks/index.d.ts +2 -0
  12. package/dist/atoms/hooks/index.d.ts.map +1 -1
  13. package/dist/atoms/hooks/useEntityDetail.d.ts +43 -0
  14. package/dist/atoms/hooks/useEntityDetail.d.ts.map +1 -0
  15. package/dist/atoms/hooks/useOnlineStatus.d.ts +16 -0
  16. package/dist/atoms/hooks/useOnlineStatus.d.ts.map +1 -0
  17. package/dist/atoms/primitives/table.d.ts.map +1 -1
  18. package/dist/atoms/services/api/client.d.ts +12 -2
  19. package/dist/atoms/services/api/client.d.ts.map +1 -1
  20. package/dist/atoms/services/auth-service.d.ts +15 -0
  21. package/dist/atoms/services/auth-service.d.ts.map +1 -1
  22. package/dist/atoms/services/index.d.ts +2 -2
  23. package/dist/atoms/services/index.d.ts.map +1 -1
  24. package/dist/atoms/types/auth.d.ts +44 -2
  25. package/dist/atoms/types/auth.d.ts.map +1 -1
  26. package/dist/atoms/types/ui-config.d.ts +11 -0
  27. package/dist/atoms/types/ui-config.d.ts.map +1 -1
  28. package/dist/atoms/utils/entity-card-mapping.d.ts +105 -0
  29. package/dist/atoms/utils/entity-card-mapping.d.ts.map +1 -0
  30. package/dist/atoms/utils/index.d.ts +1 -0
  31. package/dist/atoms/utils/index.d.ts.map +1 -1
  32. package/dist/features/auth/components/ProtectedRoute.d.ts +3 -1
  33. package/dist/features/auth/components/ProtectedRoute.d.ts.map +1 -1
  34. package/dist/features/auth/hooks/useAuth.d.ts.map +1 -1
  35. package/dist/features/auth/providers/NoAuthProvider.d.ts +17 -0
  36. package/dist/features/auth/providers/NoAuthProvider.d.ts.map +1 -0
  37. package/dist/features/auth/providers/index.d.ts +1 -0
  38. package/dist/features/auth/providers/index.d.ts.map +1 -1
  39. package/dist/frontend-patterns.css +1 -1
  40. package/dist/index.d.ts +5 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.es.js +857 -91
  43. package/dist/index.es.js.map +1 -1
  44. package/dist/index.js +854 -88
  45. package/dist/index.js.map +1 -1
  46. package/dist/molecules/layout/AppHeader/AppHeader.d.ts.map +1 -1
  47. package/dist/molecules/layout/FieldGrid/FieldGrid.d.ts +61 -0
  48. package/dist/molecules/layout/FieldGrid/FieldGrid.d.ts.map +1 -0
  49. package/dist/molecules/layout/FieldGrid/index.d.ts +2 -0
  50. package/dist/molecules/layout/FieldGrid/index.d.ts.map +1 -0
  51. package/dist/molecules/layout/index.d.ts +1 -0
  52. package/dist/molecules/layout/index.d.ts.map +1 -1
  53. package/dist/templates/factory.d.ts.map +1 -1
  54. package/package.json +4 -5
package/dist/index.js CHANGED
@@ -77,8 +77,8 @@ const UI_CONFIG = {
77
77
  TOAST_DURATION: 5e3
78
78
  };
79
79
  const env = {
80
- API_BASE_URL: "http://localhost:8080",
81
- OPENAPI_URL: "http://localhost:8080/openapi.json",
80
+ API_BASE_URL: "",
81
+ OPENAPI_URL: "/openapi.json",
82
82
  NODE_ENV: "development",
83
83
  IS_DEVELOPMENT: false,
84
84
  IS_PRODUCTION: false,
@@ -2052,16 +2052,374 @@ function renderField(value, fieldName, fieldType, breakpoint, item, customRender
2052
2052
  function cn(...inputs) {
2053
2053
  return tailwindMerge.twMerge(clsx.clsx(inputs));
2054
2054
  }
2055
- const API_BASE_URL = "http://localhost:8080";
2055
+ const STATUS_COLOR_MAP = {
2056
+ // Success states
2057
+ active: "success",
2058
+ completed: "success",
2059
+ approved: "success",
2060
+ paid: "success",
2061
+ delivered: "success",
2062
+ resolved: "success",
2063
+ closed: "success",
2064
+ // Warning states
2065
+ pending: "warning",
2066
+ processing: "warning",
2067
+ review: "warning",
2068
+ draft: "warning",
2069
+ waiting: "warning",
2070
+ // Error states
2071
+ failed: "error",
2072
+ rejected: "error",
2073
+ cancelled: "error",
2074
+ overdue: "error",
2075
+ blocked: "error",
2076
+ // Info states
2077
+ new: "info",
2078
+ open: "info",
2079
+ in_progress: "info",
2080
+ scheduled: "info",
2081
+ // Neutral (default)
2082
+ inactive: "neutral",
2083
+ archived: "neutral",
2084
+ unknown: "neutral"
2085
+ };
2086
+ function getStatusColor(value) {
2087
+ if (typeof value !== "string") return "neutral";
2088
+ const normalized = value.toLowerCase().replace(/[_-]/g, "_");
2089
+ return STATUS_COLOR_MAP[normalized] ?? "neutral";
2090
+ }
2091
+ const CATEGORY_MAP = {
2092
+ account: 1,
2093
+ customer: 1,
2094
+ client: 1,
2095
+ user: 2,
2096
+ contact: 2,
2097
+ person: 2,
2098
+ order: 3,
2099
+ sale: 3,
2100
+ transaction: 3,
2101
+ product: 4,
2102
+ item: 4,
2103
+ inventory: 4,
2104
+ task: 5,
2105
+ activity: 5,
2106
+ event: 5,
2107
+ file: 6,
2108
+ document: 6,
2109
+ report: 6,
2110
+ tag: 7,
2111
+ category: 7,
2112
+ label: 7,
2113
+ default: 8
2114
+ };
2115
+ function getCategoryForEntity(entityType) {
2116
+ if (!entityType) return 8;
2117
+ const normalized = entityType.toLowerCase();
2118
+ for (const [key, value] of Object.entries(CATEGORY_MAP)) {
2119
+ if (normalized.includes(key)) return value;
2120
+ }
2121
+ return 8;
2122
+ }
2123
+ function getIconForEntity(entityType) {
2124
+ if (!entityType) return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Circle, { className: "w-4 h-4" });
2125
+ const normalized = entityType.toLowerCase();
2126
+ if (normalized.includes("account") || normalized.includes("customer") || normalized.includes("client")) {
2127
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Building2, { className: "w-4 h-4" });
2128
+ }
2129
+ if (normalized.includes("user") || normalized.includes("contact") || normalized.includes("person")) {
2130
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.User, { className: "w-4 h-4" });
2131
+ }
2132
+ if (normalized.includes("product") || normalized.includes("item")) {
2133
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Package, { className: "w-4 h-4" });
2134
+ }
2135
+ if (normalized.includes("file") || normalized.includes("document")) {
2136
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.FileText, { className: "w-4 h-4" });
2137
+ }
2138
+ if (normalized.includes("tag") || normalized.includes("category")) {
2139
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Tag, { className: "w-4 h-4" });
2140
+ }
2141
+ return /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Circle, { className: "w-4 h-4" });
2142
+ }
2143
+ function normalizeImportance(importance) {
2144
+ switch (importance) {
2145
+ case "critical":
2146
+ case "high":
2147
+ case "primary":
2148
+ return "primary";
2149
+ case "medium":
2150
+ case "secondary":
2151
+ return "secondary";
2152
+ default:
2153
+ return "tertiary";
2154
+ }
2155
+ }
2156
+ function isPrimaryImportance(importance) {
2157
+ return normalizeImportance(importance) === "primary";
2158
+ }
2159
+ function formatValueForCard(value, type, format) {
2160
+ if (value === null || value === void 0) return "—";
2161
+ switch (type) {
2162
+ case "money": {
2163
+ const num = Number(value);
2164
+ const currency = (format == null ? void 0 : format.currency) ?? "USD";
2165
+ if (num >= 1e6) {
2166
+ return new Intl.NumberFormat("en-US", {
2167
+ style: "currency",
2168
+ currency,
2169
+ notation: "compact",
2170
+ maximumFractionDigits: 1
2171
+ }).format(num);
2172
+ }
2173
+ if (num >= 1e3) {
2174
+ return new Intl.NumberFormat("en-US", {
2175
+ style: "currency",
2176
+ currency,
2177
+ notation: "compact",
2178
+ maximumFractionDigits: 1
2179
+ }).format(num);
2180
+ }
2181
+ return new Intl.NumberFormat("en-US", {
2182
+ style: "currency",
2183
+ currency,
2184
+ maximumFractionDigits: 0
2185
+ }).format(num);
2186
+ }
2187
+ case "percent": {
2188
+ const num = Number(value);
2189
+ return `${num.toFixed(0)}%`;
2190
+ }
2191
+ case "number": {
2192
+ const num = Number(value);
2193
+ if (num >= 1e6) return `${(num / 1e6).toFixed(1)}M`;
2194
+ if (num >= 1e3) return `${(num / 1e3).toFixed(1)}K`;
2195
+ return String(num);
2196
+ }
2197
+ case "date":
2198
+ case "datetime": {
2199
+ const date = new Date(String(value));
2200
+ if (isNaN(date.getTime())) return String(value);
2201
+ return date.toLocaleDateString("en-US", {
2202
+ month: "short",
2203
+ day: "numeric"
2204
+ });
2205
+ }
2206
+ case "boolean":
2207
+ return value ? "Yes" : "No";
2208
+ default:
2209
+ return String(value);
2210
+ }
2211
+ }
2212
+ function autoDetectMapping(columns) {
2213
+ var _a, _b, _c, _d, _e, _f;
2214
+ const primaryCols = columns.filter((c) => isPrimaryImportance(c.importance));
2215
+ const titleField = ((_a = primaryCols.find((c) => ["text", "entity", "user"].includes(c.type))) == null ? void 0 : _a.field) ?? ((_b = primaryCols[0]) == null ? void 0 : _b.field) ?? ((_c = columns[0]) == null ? void 0 : _c.field);
2216
+ const valueField = (_d = primaryCols.find((c) => c.type === "money")) == null ? void 0 : _d.field;
2217
+ const statusField = (_e = primaryCols.find(
2218
+ (c) => c.type === "status" || c.type === "badge"
2219
+ )) == null ? void 0 : _e.field;
2220
+ const usedForMain = new Set([titleField, valueField, statusField].filter(Boolean));
2221
+ const remainingPrimary = primaryCols.filter((c) => !usedForMain.has(c.field));
2222
+ const subtitleField = (_f = remainingPrimary[0]) == null ? void 0 : _f.field;
2223
+ const usedFields = new Set(
2224
+ [titleField, valueField, statusField, subtitleField].filter(Boolean)
2225
+ );
2226
+ const metadataFields = primaryCols.filter((c) => !usedFields.has(c.field)).slice(0, 3).map((c) => c.field);
2227
+ return {
2228
+ titleField: titleField ?? "id",
2229
+ subtitleField,
2230
+ valueField,
2231
+ statusField,
2232
+ metadataFields,
2233
+ iconConfig: void 0
2234
+ };
2235
+ }
2236
+ function entityToListCardProps(item, columns, mapping, entityType) {
2237
+ const autoMapping = autoDetectMapping(columns);
2238
+ const effectiveMapping = { ...autoMapping, ...mapping };
2239
+ const {
2240
+ titleField,
2241
+ subtitleField,
2242
+ valueField,
2243
+ statusField,
2244
+ metadataFields,
2245
+ iconConfig
2246
+ } = effectiveMapping;
2247
+ const getColumn = (field) => field ? columns.find((c) => c.field === field) : void 0;
2248
+ const title = item[titleField] != null ? String(item[titleField]) : "—";
2249
+ const subtitle = subtitleField && item[subtitleField] != null ? String(item[subtitleField]) : void 0;
2250
+ let value;
2251
+ if (valueField && item[valueField] != null) {
2252
+ const valueCol = getColumn(valueField);
2253
+ const formatted = formatValueForCard(
2254
+ item[valueField],
2255
+ (valueCol == null ? void 0 : valueCol.type) ?? "text",
2256
+ valueCol == null ? void 0 : valueCol.format
2257
+ );
2258
+ value = {
2259
+ text: formatted,
2260
+ variant: "success"
2261
+ // Money typically green
2262
+ };
2263
+ }
2264
+ let badge;
2265
+ if (statusField && item[statusField] != null) {
2266
+ const statusValue = String(item[statusField]);
2267
+ badge = {
2268
+ text: statusValue,
2269
+ variant: "status",
2270
+ status: getStatusColor(statusValue),
2271
+ size: "sm"
2272
+ };
2273
+ }
2274
+ const metadata = metadataFields == null ? void 0 : metadataFields.map((field) => {
2275
+ const col = getColumn(field);
2276
+ if (!col || item[field] == null) return null;
2277
+ return formatValueForCard(item[field], col.type, col.format);
2278
+ }).filter((v) => v !== null);
2279
+ let icon;
2280
+ if (iconConfig) {
2281
+ const iconValue = iconConfig.field ? item[iconConfig.field] : void 0;
2282
+ if (iconConfig.variant === "status" && iconValue) {
2283
+ icon = {
2284
+ icon: iconConfig.icon ?? getIconForEntity(entityType),
2285
+ variant: "status",
2286
+ status: getStatusColor(iconValue),
2287
+ size: "sm"
2288
+ };
2289
+ } else {
2290
+ icon = {
2291
+ icon: iconConfig.icon ?? getIconForEntity(entityType),
2292
+ variant: "category",
2293
+ category: getCategoryForEntity(entityType),
2294
+ size: "sm"
2295
+ };
2296
+ }
2297
+ } else {
2298
+ icon = {
2299
+ icon: getIconForEntity(entityType),
2300
+ variant: "category",
2301
+ category: getCategoryForEntity(entityType),
2302
+ size: "sm"
2303
+ };
2304
+ }
2305
+ return {
2306
+ icon,
2307
+ title,
2308
+ subtitle,
2309
+ metadata,
2310
+ value,
2311
+ badge
2312
+ };
2313
+ }
2314
+ const HEADER_ABBREVIATIONS = {
2315
+ customer: "Cust",
2316
+ account: "Acct",
2317
+ amount: "Amt",
2318
+ quantity: "Qty",
2319
+ priority: "Pri",
2320
+ status: "Status",
2321
+ created: "Created",
2322
+ updated: "Updated",
2323
+ description: "Desc",
2324
+ total: "Total",
2325
+ items: "Items",
2326
+ number: "#",
2327
+ date: "Date",
2328
+ time: "Time"
2329
+ };
2330
+ function abbreviateHeader(label) {
2331
+ const lower = label.toLowerCase();
2332
+ for (const [key, abbrev] of Object.entries(HEADER_ABBREVIATIONS)) {
2333
+ if (lower.includes(key)) return abbrev;
2334
+ }
2335
+ if (label.length > 8) return label.slice(0, 6) + "…";
2336
+ return label;
2337
+ }
2338
+ function generateCompactColumns(columns, options = {}) {
2339
+ const {
2340
+ maxColumns = 5,
2341
+ abbreviateHeaders = true,
2342
+ requiredFields = [],
2343
+ excludeFields = []
2344
+ } = options;
2345
+ const excludeSet = new Set(excludeFields);
2346
+ const requiredSet = new Set(requiredFields);
2347
+ const filteredColumns = columns.filter((col) => {
2348
+ if (excludeSet.has(col.field)) return false;
2349
+ return true;
2350
+ });
2351
+ const required = filteredColumns.filter((c) => requiredSet.has(c.field));
2352
+ const primary = filteredColumns.filter(
2353
+ (c) => !requiredSet.has(c.field) && normalizeImportance(c.importance) === "primary"
2354
+ );
2355
+ const secondary = filteredColumns.filter(
2356
+ (c) => !requiredSet.has(c.field) && normalizeImportance(c.importance) === "secondary"
2357
+ );
2358
+ const selectedColumns = [...required, ...primary, ...secondary].slice(
2359
+ 0,
2360
+ maxColumns
2361
+ );
2362
+ return selectedColumns.map((col) => ({
2363
+ key: col.field,
2364
+ header: abbreviateHeaders ? abbreviateHeader(col.label) : col.label,
2365
+ sortable: col.sortable,
2366
+ type: col.type,
2367
+ format: col.format,
2368
+ // Compact widths
2369
+ width: getCompactWidth(col.type)
2370
+ }));
2371
+ }
2372
+ function getCompactWidth(type) {
2373
+ switch (type) {
2374
+ case "money":
2375
+ return "70px";
2376
+ case "number":
2377
+ case "percent":
2378
+ return "50px";
2379
+ case "status":
2380
+ case "badge":
2381
+ return "80px";
2382
+ case "date":
2383
+ return "70px";
2384
+ case "datetime":
2385
+ return "90px";
2386
+ case "boolean":
2387
+ return "50px";
2388
+ default:
2389
+ return "120px";
2390
+ }
2391
+ }
2392
+ function createMobileCardRenderer(columns, mapping, entityType) {
2393
+ const { ListCard: ListCard2 } = require("../components/data/ListCard");
2394
+ return ({ data, onItemClick }) => /* @__PURE__ */ jsxRuntime.jsx("div", { className: "space-y-2", children: data.map((item, index) => {
2395
+ const cardProps = entityToListCardProps(
2396
+ item,
2397
+ columns,
2398
+ mapping,
2399
+ entityType
2400
+ );
2401
+ return /* @__PURE__ */ jsxRuntime.jsx(
2402
+ ListCard2,
2403
+ {
2404
+ ...cardProps,
2405
+ onClick: onItemClick ? () => onItemClick(item) : void 0
2406
+ },
2407
+ item.id ?? index
2408
+ );
2409
+ }) });
2410
+ }
2056
2411
  let globalAuthService = null;
2057
2412
  function setGlobalAuthService(authService) {
2058
2413
  globalAuthService = authService;
2059
2414
  }
2415
+ function getGlobalAuthService() {
2416
+ return globalAuthService;
2417
+ }
2060
2418
  class ApiClient {
2061
2419
  constructor(config = {}) {
2062
2420
  __publicField(this, "instance");
2063
2421
  this.instance = axios.create({
2064
- baseURL: config.baseURL || API_BASE_URL,
2422
+ baseURL: config.baseURL || "",
2065
2423
  timeout: config.timeout || 1e4,
2066
2424
  headers: {
2067
2425
  "Content-Type": "application/json",
@@ -2101,11 +2459,15 @@ class ApiClient {
2101
2459
  this.instance.interceptors.response.use(
2102
2460
  (response) => response,
2103
2461
  async (error) => {
2104
- var _a;
2105
- if (((_a = error.response) == null ? void 0 : _a.status) === 401) {
2462
+ var _a, _b, _c, _d;
2463
+ const status = (_a = error.response) == null ? void 0 : _a.status;
2464
+ if (!error.response) {
2465
+ return Promise.reject(error);
2466
+ }
2467
+ if (status === 401 || status === 403) {
2106
2468
  if (globalAuthService) {
2107
2469
  const tokenData = globalAuthService.getTokenData();
2108
- if ((tokenData == null ? void 0 : tokenData.refreshToken) && !error.config._retry) {
2470
+ if (status === 401 && (tokenData == null ? void 0 : tokenData.refreshToken) && !error.config._retry) {
2109
2471
  error.config._retry = true;
2110
2472
  try {
2111
2473
  await globalAuthService.refreshToken();
@@ -2113,17 +2475,18 @@ class ApiClient {
2113
2475
  error.config.headers.Authorization = `Bearer ${newTokenData == null ? void 0 : newTokenData.token}`;
2114
2476
  return this.instance.request(error.config);
2115
2477
  } catch {
2116
- globalAuthService.clearAuth();
2117
- window.location.reload();
2118
2478
  }
2119
- } else {
2120
- globalAuthService.clearAuth();
2121
- window.location.reload();
2479
+ }
2480
+ globalAuthService.clearAuth();
2481
+ const errorInfo = { status, message: (_c = (_b = error.response) == null ? void 0 : _b.data) == null ? void 0 : _c.detail };
2482
+ const handled = (_d = globalAuthService.onAuthError) == null ? void 0 : _d.call(globalAuthService, errorInfo);
2483
+ if (!handled) {
2484
+ window.location.href = "/login";
2122
2485
  }
2123
2486
  } else {
2124
2487
  localStorage.removeItem("auth_token");
2125
2488
  localStorage.removeItem("auth_user");
2126
- window.location.reload();
2489
+ window.location.href = "/login";
2127
2490
  }
2128
2491
  }
2129
2492
  return Promise.reject(error);
@@ -2163,6 +2526,9 @@ class ApiClient {
2163
2526
  return response.data;
2164
2527
  }
2165
2528
  }
2529
+ function createApiClient(config = {}) {
2530
+ return new ApiClient(config);
2531
+ }
2166
2532
  const apiClient = new ApiClient();
2167
2533
  function useToast() {
2168
2534
  const [toasts, setToasts] = React.useState([]);
@@ -2433,7 +2799,7 @@ function useEntityData(entity, options = {}) {
2433
2799
  const dataQuery = reactQuery.useQuery({
2434
2800
  queryKey: [entity, "list", { limit, offset, filters }],
2435
2801
  queryFn: () => apiClient.get(
2436
- `/api/v1/${entity}/?${dataParams.toString()}`
2802
+ `/api/v1/${entity}?${dataParams.toString()}`
2437
2803
  ),
2438
2804
  enabled
2439
2805
  });
@@ -2461,6 +2827,95 @@ function useEntityData(entity, options = {}) {
2461
2827
  refetch
2462
2828
  };
2463
2829
  }
2830
+ function useEntityDetail(entity, id, options = {}) {
2831
+ var _a;
2832
+ const { view = "detail", sourceId, enabled = true } = options;
2833
+ const metadataParams = new URLSearchParams();
2834
+ metadataParams.set("view", view);
2835
+ if (sourceId) metadataParams.set("source_id", sourceId);
2836
+ const dataQuery = reactQuery.useQuery({
2837
+ queryKey: [entity, "detail", id],
2838
+ queryFn: () => apiClient.get(`/api/v1/${entity}/${id}`),
2839
+ enabled: enabled && !!id
2840
+ });
2841
+ const metadataQuery = reactQuery.useQuery({
2842
+ queryKey: [entity, "metadata", { view, sourceId }],
2843
+ queryFn: () => apiClient.get(
2844
+ `/api/v1/${entity}/fields/metadata?${metadataParams.toString()}`
2845
+ ),
2846
+ enabled,
2847
+ staleTime: 5 * 60 * 1e3
2848
+ // 5 minutes
2849
+ });
2850
+ const refetch = () => {
2851
+ dataQuery.refetch();
2852
+ metadataQuery.refetch();
2853
+ };
2854
+ return {
2855
+ data: dataQuery.data ?? null,
2856
+ fields: ((_a = metadataQuery.data) == null ? void 0 : _a.columns) ?? [],
2857
+ isLoading: dataQuery.isLoading || metadataQuery.isLoading,
2858
+ isLoadingData: dataQuery.isLoading,
2859
+ isLoadingMetadata: metadataQuery.isLoading,
2860
+ error: dataQuery.error ?? metadataQuery.error ?? null,
2861
+ refetch
2862
+ };
2863
+ }
2864
+ function useOnlineStatus(healthEndpoint) {
2865
+ const [isOnline, setIsOnline] = React.useState(
2866
+ typeof navigator !== "undefined" ? navigator.onLine : true
2867
+ );
2868
+ const [isBackendReachable, setIsBackendReachable] = React.useState(true);
2869
+ const [lastBackendContact, setLastBackendContact] = React.useState(
2870
+ null
2871
+ );
2872
+ React.useEffect(() => {
2873
+ const handleOnline = () => setIsOnline(true);
2874
+ const handleOffline = () => {
2875
+ setIsOnline(false);
2876
+ setIsBackendReachable(false);
2877
+ };
2878
+ window.addEventListener("online", handleOnline);
2879
+ window.addEventListener("offline", handleOffline);
2880
+ return () => {
2881
+ window.removeEventListener("online", handleOnline);
2882
+ window.removeEventListener("offline", handleOffline);
2883
+ };
2884
+ }, []);
2885
+ const checkBackend = React.useCallback(async () => {
2886
+ if (!isOnline) {
2887
+ setIsBackendReachable(false);
2888
+ return false;
2889
+ }
2890
+ try {
2891
+ const endpoint = healthEndpoint || "/api/v1/health";
2892
+ const response = await fetch(endpoint, {
2893
+ method: "GET",
2894
+ cache: "no-store"
2895
+ });
2896
+ const reachable = response.ok;
2897
+ setIsBackendReachable(reachable);
2898
+ if (reachable) {
2899
+ setLastBackendContact(Date.now());
2900
+ }
2901
+ return reachable;
2902
+ } catch {
2903
+ setIsBackendReachable(false);
2904
+ return false;
2905
+ }
2906
+ }, [isOnline, healthEndpoint]);
2907
+ React.useEffect(() => {
2908
+ if (isOnline) {
2909
+ checkBackend();
2910
+ }
2911
+ }, [isOnline, checkBackend]);
2912
+ return {
2913
+ isOnline,
2914
+ isBackendReachable,
2915
+ lastBackendContact,
2916
+ checkBackend
2917
+ };
2918
+ }
2464
2919
  const buttonVariants = classVarianceAuthority.cva(
2465
2920
  "inline-flex items-center justify-center whitespace-nowrap rounded-lg text-sm font-medium ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98]",
2466
2921
  {
@@ -4151,7 +4606,9 @@ const TableHead$1 = React__namespace.forwardRef(({ className, ...props }, ref) =
4151
4606
  {
4152
4607
  ref,
4153
4608
  className: cn(
4154
- "h-12 px-4 xs:px-2 2xs:px-1 text-left align-middle font-semibold text-gray-700 text-xs uppercase tracking-wider [&:has([role=checkbox])]:pr-0",
4609
+ "h-12 px-4 xs:px-2 2xs:px-1 text-left align-middle font-semibold text-gray-700 text-xs uppercase tracking-wider",
4610
+ // Compact padding for checkbox columns
4611
+ "[&:has(input[type=checkbox])]:px-2 [&:has(input[type=checkbox])]:w-8",
4155
4612
  className
4156
4613
  ),
4157
4614
  ...props
@@ -4163,7 +4620,9 @@ const TableCell$1 = React__namespace.forwardRef(({ className, ...props }, ref) =
4163
4620
  {
4164
4621
  ref,
4165
4622
  className: cn(
4166
- "p-4 xs:p-2 2xs:p-1 align-middle [&:has([role=checkbox])]:pr-0",
4623
+ "p-4 xs:p-2 2xs:p-1 align-middle",
4624
+ // Compact padding for checkbox columns
4625
+ "[&:has(input[type=checkbox])]:px-2 [&:has(input[type=checkbox])]:w-8",
4167
4626
  className
4168
4627
  ),
4169
4628
  ...props
@@ -4213,7 +4672,12 @@ function DataTable({
4213
4672
  ui,
4214
4673
  error = null,
4215
4674
  errorTitle,
4216
- errorRetry
4675
+ errorRetry,
4676
+ selectable = false,
4677
+ selectedIds,
4678
+ onSelectionChange,
4679
+ getRowId,
4680
+ selectAllMode = "page"
4217
4681
  }) {
4218
4682
  const [searchTerm, setSearchTerm] = React.useState("");
4219
4683
  const [sortColumn, setSortColumn] = React.useState(null);
@@ -4276,6 +4740,85 @@ function DataTable({
4276
4740
  return true;
4277
4741
  });
4278
4742
  }, [normalizedColumns, currentBreakpoint, responsive]);
4743
+ const defaultGetRowId = React.useCallback((row) => {
4744
+ if ("id" in row && typeof row.id === "string") return row.id;
4745
+ if ("id" in row && typeof row.id === "number") return String(row.id);
4746
+ throw new Error("DataTable: selectable requires getRowId prop or row.id field");
4747
+ }, []);
4748
+ const effectiveGetRowId = getRowId ?? defaultGetRowId;
4749
+ const selectableData = selectAllMode === "all" ? sortedData : paginatedData;
4750
+ const selectableIds = React.useMemo(
4751
+ () => {
4752
+ if (!selectable) return /* @__PURE__ */ new Set();
4753
+ return new Set(selectableData.map((row) => effectiveGetRowId(row)));
4754
+ },
4755
+ [selectable, selectableData, effectiveGetRowId]
4756
+ );
4757
+ const selectedInScope = React.useMemo(
4758
+ () => {
4759
+ if (!selectable || !selectedIds) return /* @__PURE__ */ new Set();
4760
+ return new Set([...selectedIds].filter((id) => selectableIds.has(id)));
4761
+ },
4762
+ [selectable, selectedIds, selectableIds]
4763
+ );
4764
+ const isAllSelected = selectableIds.size > 0 && selectedInScope.size === selectableIds.size;
4765
+ const isIndeterminate = selectedInScope.size > 0 && selectedInScope.size < selectableIds.size;
4766
+ const handleSelectAll = React.useCallback(() => {
4767
+ if (!onSelectionChange) return;
4768
+ if (isAllSelected) {
4769
+ const newSelection = new Set([...selectedIds ?? []].filter((id) => !selectableIds.has(id)));
4770
+ onSelectionChange(newSelection);
4771
+ } else {
4772
+ const newSelection = /* @__PURE__ */ new Set([...selectedIds ?? [], ...selectableIds]);
4773
+ onSelectionChange(newSelection);
4774
+ }
4775
+ }, [isAllSelected, selectedIds, selectableIds, onSelectionChange]);
4776
+ const handleSelectRow = React.useCallback((rowId) => {
4777
+ if (!onSelectionChange) return;
4778
+ const newSelection = new Set(selectedIds);
4779
+ if (newSelection.has(rowId)) {
4780
+ newSelection.delete(rowId);
4781
+ } else {
4782
+ newSelection.add(rowId);
4783
+ }
4784
+ onSelectionChange(newSelection);
4785
+ }, [selectedIds, onSelectionChange]);
4786
+ const selectionColumn = React.useMemo(() => {
4787
+ return {
4788
+ key: "__selection__",
4789
+ header: /* @__PURE__ */ jsxRuntime.jsx(
4790
+ Checkbox$1,
4791
+ {
4792
+ checked: isAllSelected,
4793
+ indeterminate: isIndeterminate,
4794
+ onCheckedChange: handleSelectAll,
4795
+ "aria-label": isAllSelected ? "Deselect all rows" : "Select all rows",
4796
+ size: "sm"
4797
+ }
4798
+ ),
4799
+ cell: (row) => {
4800
+ const rowId = effectiveGetRowId(row);
4801
+ const isSelected = (selectedIds == null ? void 0 : selectedIds.has(rowId)) ?? false;
4802
+ return /* @__PURE__ */ jsxRuntime.jsx(
4803
+ Checkbox$1,
4804
+ {
4805
+ checked: isSelected,
4806
+ onCheckedChange: () => handleSelectRow(rowId),
4807
+ "aria-label": isSelected ? `Deselect row ${rowId}` : `Select row ${rowId}`,
4808
+ size: "sm",
4809
+ onClick: (e) => e.stopPropagation()
4810
+ }
4811
+ );
4812
+ },
4813
+ width: "32px",
4814
+ sortable: false,
4815
+ filterable: false
4816
+ };
4817
+ }, [isAllSelected, isIndeterminate, selectedIds, effectiveGetRowId, handleSelectAll, handleSelectRow]);
4818
+ const effectiveColumns = React.useMemo(() => {
4819
+ if (!selectable) return visibleColumns;
4820
+ return [selectionColumn, ...visibleColumns];
4821
+ }, [selectable, selectionColumn, visibleColumns]);
4279
4822
  const handleSort = (columnKey) => {
4280
4823
  if (sortColumn === columnKey) {
4281
4824
  if (sortDirection === "asc") {
@@ -4338,8 +4881,8 @@ function DataTable({
4338
4881
  children: [
4339
4882
  showSearch && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative flex-1 max-w-sm", children: /* @__PURE__ */ jsxRuntime.jsx(Skeleton$1, { className: "h-10 w-full" }) }) }),
4340
4883
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded border overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsxs(Table$1, { children: [
4341
- /* @__PURE__ */ jsxRuntime.jsx(TableHeader$1, { children: /* @__PURE__ */ jsxRuntime.jsx(TableRow$1, { children: visibleColumns.map((column) => /* @__PURE__ */ jsxRuntime.jsx(TableHead$1, { style: { width: column.width }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsx(Skeleton$1, { className: "h-4 w-20" }) }) }, column.key)) }) }),
4342
- /* @__PURE__ */ jsxRuntime.jsx(TableBody$1, { children: Array.from({ length: loadingItemCount }, (_, index) => /* @__PURE__ */ jsxRuntime.jsx(TableRow$1, { children: visibleColumns.map((column) => /* @__PURE__ */ jsxRuntime.jsx(TableCell$1, { children: /* @__PURE__ */ jsxRuntime.jsx(Skeleton$1, { className: "h-4 w-full max-w-32" }) }, column.key)) }, index)) })
4884
+ /* @__PURE__ */ jsxRuntime.jsx(TableHeader$1, { children: /* @__PURE__ */ jsxRuntime.jsx(TableRow$1, { children: effectiveColumns.map((column) => /* @__PURE__ */ jsxRuntime.jsx(TableHead$1, { style: { width: column.width }, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center gap-2", children: /* @__PURE__ */ jsxRuntime.jsx(Skeleton$1, { className: "h-4 w-20" }) }) }, column.key)) }) }),
4885
+ /* @__PURE__ */ jsxRuntime.jsx(TableBody$1, { children: Array.from({ length: loadingItemCount }, (_, index) => /* @__PURE__ */ jsxRuntime.jsx(TableRow$1, { children: effectiveColumns.map((column) => /* @__PURE__ */ jsxRuntime.jsx(TableCell$1, { children: /* @__PURE__ */ jsxRuntime.jsx(Skeleton$1, { className: "h-4 w-full max-w-32" }) }, column.key)) }, index)) })
4343
4886
  ] }) }),
4344
4887
  showPagination && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between", children: [
4345
4888
  /* @__PURE__ */ jsxRuntime.jsx(Skeleton$1, { className: "h-4 w-48" }),
@@ -4401,7 +4944,7 @@ function DataTable({
4401
4944
  data: paginatedData,
4402
4945
  onItemClick: onRowClick
4403
4946
  }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "rounded border overflow-hidden", children: /* @__PURE__ */ jsxRuntime.jsxs(Table$1, { children: [
4404
- /* @__PURE__ */ jsxRuntime.jsx(TableHeader$1, { children: /* @__PURE__ */ jsxRuntime.jsx(TableRow$1, { children: visibleColumns.map((column) => {
4947
+ /* @__PURE__ */ jsxRuntime.jsx(TableHeader$1, { children: /* @__PURE__ */ jsxRuntime.jsx(TableRow$1, { children: effectiveColumns.map((column) => {
4405
4948
  const width = column.width;
4406
4949
  return /* @__PURE__ */ jsxRuntime.jsx(
4407
4950
  TableHead$1,
@@ -4424,7 +4967,7 @@ function DataTable({
4424
4967
  /* @__PURE__ */ jsxRuntime.jsx(TableBody$1, { children: paginatedData.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx(TableRow$1, { children: /* @__PURE__ */ jsxRuntime.jsx(
4425
4968
  TableCell$1,
4426
4969
  {
4427
- colSpan: visibleColumns.length,
4970
+ colSpan: effectiveColumns.length,
4428
4971
  className: "text-center py-8 text-muted-foreground",
4429
4972
  children: emptyMessage
4430
4973
  }
@@ -4436,7 +4979,7 @@ function DataTable({
4436
4979
  (hover || onRowClick) && "hover:bg-muted/50"
4437
4980
  ),
4438
4981
  onClick: () => onRowClick == null ? void 0 : onRowClick(item),
4439
- children: visibleColumns.map((column) => /* @__PURE__ */ jsxRuntime.jsx(TableCell$1, { children: renderCell(column, item) }, column.key))
4982
+ children: effectiveColumns.map((column) => /* @__PURE__ */ jsxRuntime.jsx(TableCell$1, { children: renderCell(column, item) }, column.key))
4440
4983
  },
4441
4984
  index
4442
4985
  )) })
@@ -5629,6 +6172,75 @@ const ActivityFeed = ({
5629
6172
  ] }) })
5630
6173
  ] });
5631
6174
  };
6175
+ const statusConfig = {
6176
+ online: {
6177
+ icon: lucideReact.Cloud,
6178
+ label: "Online",
6179
+ color: "text-green-600 dark:text-green-400",
6180
+ bgColor: "bg-green-100 dark:bg-green-900/30"
6181
+ },
6182
+ degraded: {
6183
+ icon: lucideReact.CloudOff,
6184
+ label: "Server Offline",
6185
+ color: "text-yellow-600 dark:text-yellow-400",
6186
+ bgColor: "bg-yellow-100 dark:bg-yellow-900/30"
6187
+ },
6188
+ offline: {
6189
+ icon: lucideReact.WifiOff,
6190
+ label: "Offline",
6191
+ color: "text-red-600 dark:text-red-400",
6192
+ bgColor: "bg-red-100 dark:bg-red-900/30"
6193
+ }
6194
+ };
6195
+ function ConnectionStatus({
6196
+ healthEndpoint,
6197
+ variant = "badge",
6198
+ className,
6199
+ showWhenOnline = false
6200
+ }) {
6201
+ const { isOnline, isBackendReachable } = useOnlineStatus(healthEndpoint);
6202
+ const status = !isOnline ? "offline" : !isBackendReachable ? "degraded" : "online";
6203
+ if (status === "online" && !showWhenOnline) {
6204
+ return null;
6205
+ }
6206
+ const config = statusConfig[status];
6207
+ const Icon2 = config.icon;
6208
+ if (variant === "badge") {
6209
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6210
+ "div",
6211
+ {
6212
+ className: cn(
6213
+ "inline-flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium",
6214
+ config.bgColor,
6215
+ config.color,
6216
+ className
6217
+ ),
6218
+ children: [
6219
+ /* @__PURE__ */ jsxRuntime.jsx(Icon2, { className: "h-3 w-3" }),
6220
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: config.label })
6221
+ ]
6222
+ }
6223
+ );
6224
+ }
6225
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6226
+ "div",
6227
+ {
6228
+ className: cn(
6229
+ "flex items-center gap-2 px-3 py-2 rounded-lg",
6230
+ config.bgColor,
6231
+ className
6232
+ ),
6233
+ children: [
6234
+ /* @__PURE__ */ jsxRuntime.jsx(Icon2, { className: cn("h-4 w-4", config.color) }),
6235
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex flex-col", children: [
6236
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: cn("text-sm font-medium", config.color), children: config.label }),
6237
+ status === "degraded" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: "Working with cached data" }),
6238
+ status === "offline" && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground", children: "Check your connection" })
6239
+ ] })
6240
+ ]
6241
+ }
6242
+ );
6243
+ }
5632
6244
  const FormField = ({
5633
6245
  label,
5634
6246
  description,
@@ -8970,7 +9582,10 @@ const AppHeader = ({ className }) => {
8970
9582
  placeholder: "Search models, tests, jobs, and components..."
8971
9583
  }
8972
9584
  ) }),
8973
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex items-center space-x-4", children: /* @__PURE__ */ jsxRuntime.jsx(UserMenu, { category: 8 }) })
9585
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center space-x-4", children: [
9586
+ /* @__PURE__ */ jsxRuntime.jsx(ConnectionStatus, {}),
9587
+ /* @__PURE__ */ jsxRuntime.jsx(UserMenu, { category: 8 })
9588
+ ] })
8974
9589
  ] }) })
8975
9590
  }
8976
9591
  );
@@ -9261,6 +9876,55 @@ const ListToolbar = ({
9261
9876
  }
9262
9877
  );
9263
9878
  };
9879
+ const FieldGrid = ({
9880
+ data,
9881
+ fields,
9882
+ sections,
9883
+ columns = 2,
9884
+ showLabels = true,
9885
+ compact = false,
9886
+ showEmpty = true,
9887
+ className
9888
+ }) => {
9889
+ const gridCols = {
9890
+ 1: "grid-cols-1",
9891
+ 2: "grid-cols-1 md:grid-cols-2",
9892
+ 3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3"
9893
+ };
9894
+ const renderValue = (field) => {
9895
+ const value = data[field.field];
9896
+ if (value === null || value === void 0 || value === "") {
9897
+ if (!showEmpty) return null;
9898
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "-" });
9899
+ }
9900
+ const format = field.format;
9901
+ return renderField(value, field.field, field.type, "lg", data, void 0, format);
9902
+ };
9903
+ const renderField_ = (field) => {
9904
+ const value = data[field.field];
9905
+ if (!showEmpty && (value === null || value === void 0 || value === "")) {
9906
+ return null;
9907
+ }
9908
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("space-y-1", compact ? "py-1" : "py-2"), children: [
9909
+ showLabels && /* @__PURE__ */ jsxRuntime.jsx("dt", { className: "text-sm font-medium text-muted-foreground", children: field.label }),
9910
+ /* @__PURE__ */ jsxRuntime.jsx("dd", { className: "text-sm text-foreground", children: renderValue(field) })
9911
+ ] }, field.field);
9912
+ };
9913
+ const renderFields = (fieldList) => /* @__PURE__ */ jsxRuntime.jsx("dl", { className: cn("grid gap-x-6", gridCols[columns]), children: fieldList.map(renderField_) });
9914
+ if (sections && sections.length > 0) {
9915
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("space-y-6", className), children: sections.map((section) => /* @__PURE__ */ jsxRuntime.jsxs(Card, { className: cn(compact ? "p-4" : "p-6"), children: [
9916
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mb-4", children: [
9917
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold text-foreground", children: section.title }),
9918
+ section.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground mt-1", children: section.description })
9919
+ ] }),
9920
+ renderFields(section.fields)
9921
+ ] }, section.title)) });
9922
+ }
9923
+ if (fields && fields.length > 0) {
9924
+ return /* @__PURE__ */ jsxRuntime.jsx(Card, { className: cn(compact ? "p-4" : "p-6", className), children: renderFields(fields) });
9925
+ }
9926
+ return null;
9927
+ };
9264
9928
  const Pagination = ({
9265
9929
  currentPage,
9266
9930
  totalPages,
@@ -9940,9 +10604,11 @@ function ProtectedRoute({
9940
10604
  children,
9941
10605
  requirePermission,
9942
10606
  requireRole,
9943
- fallback
10607
+ fallback,
10608
+ loginPath = "/login"
9944
10609
  }) {
9945
10610
  const { isAuthenticated, isLoading, hasPermission, hasRole } = useAuthContext();
10611
+ const location = reactRouterDom.useLocation();
9946
10612
  if (isLoading) {
9947
10613
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-screen flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
9948
10614
  /* @__PURE__ */ jsxRuntime.jsx("div", { className: "animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto" }),
@@ -9950,7 +10616,7 @@ function ProtectedRoute({
9950
10616
  ] }) });
9951
10617
  }
9952
10618
  if (!isAuthenticated) {
9953
- return /* @__PURE__ */ jsxRuntime.jsx(LoginForm, {});
10619
+ return /* @__PURE__ */ jsxRuntime.jsx(reactRouterDom.Navigate, { to: loginPath, state: { from: location }, replace: true });
9954
10620
  }
9955
10621
  if (requirePermission && !hasPermission(requirePermission)) {
9956
10622
  return fallback || /* @__PURE__ */ jsxRuntime.jsx("div", { className: "min-h-screen flex items-center justify-center", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "text-center", children: [
@@ -9977,17 +10643,39 @@ function LogoutButton() {
9977
10643
  /* @__PURE__ */ jsxRuntime.jsx(Button, { variant: "outline", size: "sm", onClick: logout, children: "Logout" })
9978
10644
  ] });
9979
10645
  }
10646
+ const AUTH_TOKEN_KEY = "auth_token";
10647
+ const DEFAULT_ENDPOINTS = {
10648
+ login: "/auth/login",
10649
+ register: "/auth/register",
10650
+ refresh: "/auth/refresh",
10651
+ me: "/auth/me",
10652
+ logout: "/auth/logout"
10653
+ };
9980
10654
  class AuthService {
9981
10655
  constructor(config) {
9982
10656
  __publicField(this, "config");
10657
+ __publicField(this, "apiClient");
9983
10658
  __publicField(this, "refreshPromise", null);
10659
+ __publicField(this, "endpoints");
9984
10660
  this.config = {
10661
+ mode: "pattern-stack",
9985
10662
  tokenStorage: "localStorage",
9986
10663
  tokenRefreshBuffer: 5,
9987
10664
  // 5 minutes before expiry
9988
10665
  autoRefresh: true,
9989
10666
  ...config
9990
10667
  };
10668
+ this.endpoints = {
10669
+ ...DEFAULT_ENDPOINTS,
10670
+ ...config.endpoints
10671
+ };
10672
+ this.apiClient = createApiClient({ baseURL: this.config.apiUrl || "" });
10673
+ }
10674
+ /**
10675
+ * Get the auth mode
10676
+ */
10677
+ get mode() {
10678
+ return this.config.mode || "pattern-stack";
9991
10679
  }
9992
10680
  getStorageKey(key) {
9993
10681
  return `auth_${key}`;
@@ -10071,21 +10759,33 @@ class AuthService {
10071
10759
  this.removeItem("user");
10072
10760
  }
10073
10761
  async login(credentials) {
10074
- const response = await apiClient.post(`${this.config.apiUrl}${this.config.endpoints.login}`, credentials);
10075
10762
  let token;
10076
10763
  let refreshToken;
10077
10764
  let expiresIn;
10078
- if ("access_token" in response) {
10079
- const backendResponse = response;
10080
- token = backendResponse.access_token;
10081
- refreshToken = backendResponse.refresh_token;
10765
+ let user;
10766
+ if (this.mode === "pattern-stack") {
10767
+ const response = await this.apiClient.post(
10768
+ this.endpoints.login,
10769
+ credentials
10770
+ );
10771
+ token = response.access_token;
10772
+ refreshToken = response.refresh_token;
10773
+ user = response.user;
10774
+ } else if (this.mode === "custom" && this.config.responseMapper) {
10775
+ const rawResponse = await this.apiClient.post(
10776
+ this.endpoints.login,
10777
+ credentials
10778
+ );
10779
+ const mapped = this.config.responseMapper(rawResponse);
10780
+ token = mapped.token;
10781
+ refreshToken = mapped.refreshToken;
10782
+ expiresIn = mapped.expiresIn;
10783
+ user = mapped.user;
10082
10784
  } else {
10083
- const oldResponse = response;
10084
- token = oldResponse.token;
10085
- refreshToken = oldResponse.refreshToken;
10086
- expiresIn = oldResponse.expiresIn;
10785
+ throw new Error(
10786
+ `Invalid auth mode: ${this.mode}. Use 'pattern-stack' or 'custom' with responseMapper.`
10787
+ );
10087
10788
  }
10088
- const user = response.user;
10089
10789
  const expiresAt = expiresIn ? Date.now() + expiresIn * 1e3 : void 0;
10090
10790
  this.setTokenData({ token, refreshToken, expiresAt });
10091
10791
  this.setStoredUser(user);
@@ -10111,21 +10811,27 @@ class AuthService {
10111
10811
  }
10112
10812
  async performTokenRefresh(currentRefreshToken) {
10113
10813
  try {
10114
- const response = await apiClient.post(`${this.config.apiUrl}${this.config.endpoints.refresh}`, {
10115
- refresh_token: currentRefreshToken
10116
- });
10117
10814
  let token;
10118
10815
  let newRefreshToken;
10119
10816
  let expiresIn;
10120
- if ("access_token" in response) {
10121
- const backendResponse = response;
10122
- token = backendResponse.access_token;
10817
+ if (this.mode === "pattern-stack") {
10818
+ const response = await this.apiClient.post(
10819
+ this.endpoints.refresh,
10820
+ { refresh_token: currentRefreshToken }
10821
+ );
10822
+ token = response.access_token;
10123
10823
  newRefreshToken = currentRefreshToken;
10824
+ } else if (this.mode === "custom" && this.config.responseMapper) {
10825
+ const rawResponse = await this.apiClient.post(
10826
+ this.endpoints.refresh,
10827
+ { refresh_token: currentRefreshToken }
10828
+ );
10829
+ const mapped = this.config.responseMapper(rawResponse);
10830
+ token = mapped.token;
10831
+ newRefreshToken = mapped.refreshToken || currentRefreshToken;
10832
+ expiresIn = mapped.expiresIn;
10124
10833
  } else {
10125
- const oldResponse = response;
10126
- token = oldResponse.token;
10127
- newRefreshToken = oldResponse.refreshToken;
10128
- expiresIn = oldResponse.expiresIn;
10834
+ throw new Error(`Invalid auth mode for token refresh: ${this.mode}`);
10129
10835
  }
10130
10836
  const expiresAt = expiresIn ? Date.now() + expiresIn * 1e3 : void 0;
10131
10837
  this.setTokenData({
@@ -10167,36 +10873,39 @@ class AuthService {
10167
10873
  const tokenData = this.getTokenData();
10168
10874
  if (!(tokenData == null ? void 0 : tokenData.token)) return null;
10169
10875
  try {
10170
- return await apiClient.get(
10171
- `${this.config.apiUrl}${this.config.endpoints.me}`
10172
- );
10876
+ return await this.apiClient.get(this.endpoints.me);
10173
10877
  } catch {
10174
10878
  return null;
10175
10879
  }
10176
10880
  }
10177
10881
  async logout() {
10178
10882
  const tokenData = this.getTokenData();
10179
- if (this.config.endpoints.logout && (tokenData == null ? void 0 : tokenData.token)) {
10883
+ if (this.endpoints.logout && (tokenData == null ? void 0 : tokenData.token)) {
10180
10884
  try {
10181
- await apiClient.post(
10182
- `${this.config.apiUrl}${this.config.endpoints.logout}`
10183
- );
10885
+ await this.apiClient.post(this.endpoints.logout);
10184
10886
  } catch {
10185
10887
  }
10186
10888
  }
10187
10889
  this.clearAuth();
10188
10890
  }
10891
+ /**
10892
+ * Get the configured onAuthError handler
10893
+ */
10894
+ get onAuthError() {
10895
+ return this.config.onAuthError;
10896
+ }
10189
10897
  }
10190
10898
  function AuthProvider({
10191
10899
  children,
10192
10900
  config
10193
10901
  }) {
10902
+ var _a;
10194
10903
  const [user, setUser] = React.useState(null);
10195
10904
  const [isLoading, setIsLoading] = React.useState(true);
10196
10905
  const [permissions, setPermissions] = React.useState([]);
10197
10906
  const authService = React.useMemo(() => {
10198
10907
  const defaultConfig = {
10199
- apiUrl: "http://localhost:8080",
10908
+ apiUrl: (config == null ? void 0 : config.apiUrl) ?? "",
10200
10909
  endpoints: {
10201
10910
  login: "/auth/login",
10202
10911
  register: "/auth/register",
@@ -10211,7 +10920,7 @@ function AuthProvider({
10211
10920
  enabled: false
10212
10921
  }
10213
10922
  };
10214
- const mergedConfig = config ? { ...defaultConfig, ...config } : defaultConfig;
10923
+ const mergedConfig = config ? { ...defaultConfig, ...config, apiUrl: config.apiUrl ?? defaultConfig.apiUrl } : defaultConfig;
10215
10924
  const service = new AuthService(mergedConfig);
10216
10925
  setGlobalAuthService(service);
10217
10926
  return service;
@@ -10229,10 +10938,12 @@ function AuthProvider({
10229
10938
  try {
10230
10939
  await authService.refreshToken();
10231
10940
  setUser(storedUser);
10232
- try {
10233
- const freshUser = await authService.getCurrentUser();
10234
- if (freshUser) setUser(freshUser);
10235
- } catch {
10941
+ if (!(config == null ? void 0 : config.offlineFirst)) {
10942
+ try {
10943
+ const freshUser = await authService.getCurrentUser();
10944
+ if (freshUser) setUser(freshUser);
10945
+ } catch {
10946
+ }
10236
10947
  }
10237
10948
  } catch {
10238
10949
  authService.clearAuth();
@@ -10242,10 +10953,12 @@ function AuthProvider({
10242
10953
  }
10243
10954
  } else {
10244
10955
  setUser(storedUser);
10245
- try {
10246
- const freshUser = await authService.getCurrentUser();
10247
- if (freshUser) setUser(freshUser);
10248
- } catch {
10956
+ if (!(config == null ? void 0 : config.offlineFirst)) {
10957
+ try {
10958
+ const freshUser = await authService.getCurrentUser();
10959
+ if (freshUser) setUser(freshUser);
10960
+ } catch {
10961
+ }
10249
10962
  }
10250
10963
  }
10251
10964
  }
@@ -10258,48 +10971,56 @@ function AuthProvider({
10258
10971
  };
10259
10972
  initAuth();
10260
10973
  }, [authService]);
10261
- const login = async (credentials) => {
10262
- var _a;
10974
+ const login = React.useCallback(async (credentials) => {
10263
10975
  setIsLoading(true);
10264
10976
  try {
10265
- const user2 = await authService.login(credentials);
10266
- setUser(user2);
10267
- if (((_a = config == null ? void 0 : config.permissions) == null ? void 0 : _a.enabled) && "permissions" in user2) {
10268
- const userPermissions = user2.permissions;
10269
- setPermissions(
10270
- Array.isArray(userPermissions) ? userPermissions : []
10271
- );
10272
- }
10273
- } finally {
10977
+ const loggedInUser = await authService.login(credentials);
10978
+ reactDom.flushSync(() => {
10979
+ var _a2;
10980
+ setUser(loggedInUser);
10981
+ if (((_a2 = config == null ? void 0 : config.permissions) == null ? void 0 : _a2.enabled) && "permissions" in loggedInUser) {
10982
+ const userPermissions = loggedInUser.permissions;
10983
+ setPermissions(
10984
+ Array.isArray(userPermissions) ? userPermissions : []
10985
+ );
10986
+ }
10987
+ setIsLoading(false);
10988
+ });
10989
+ } catch (error) {
10274
10990
  setIsLoading(false);
10991
+ throw error;
10275
10992
  }
10276
- };
10277
- const logout = async () => {
10993
+ }, [authService, (_a = config == null ? void 0 : config.permissions) == null ? void 0 : _a.enabled]);
10994
+ const logout = React.useCallback(async () => {
10278
10995
  setIsLoading(true);
10279
10996
  try {
10280
10997
  await authService.logout();
10281
- setUser(null);
10282
- setPermissions([]);
10998
+ reactDom.flushSync(() => {
10999
+ setUser(null);
11000
+ setPermissions([]);
11001
+ setIsLoading(false);
11002
+ });
10283
11003
  } catch (error) {
10284
11004
  console.error("Logout error:", error);
10285
11005
  authService.clearAuth();
10286
- setUser(null);
10287
- setPermissions([]);
10288
- } finally {
10289
- setIsLoading(false);
11006
+ reactDom.flushSync(() => {
11007
+ setUser(null);
11008
+ setPermissions([]);
11009
+ setIsLoading(false);
11010
+ });
10290
11011
  }
10291
- };
11012
+ }, [authService]);
10292
11013
  const refreshToken = async () => {
10293
11014
  await authService.refreshToken();
10294
11015
  };
10295
11016
  const hasPermission = (permission) => {
10296
- var _a;
10297
- if (!((_a = config == null ? void 0 : config.permissions) == null ? void 0 : _a.enabled)) return true;
11017
+ var _a2;
11018
+ if (!((_a2 = config == null ? void 0 : config.permissions) == null ? void 0 : _a2.enabled)) return true;
10298
11019
  return permissions.includes(permission);
10299
11020
  };
10300
11021
  const hasRole = (role) => {
10301
- var _a;
10302
- if (!((_a = config == null ? void 0 : config.permissions) == null ? void 0 : _a.enabled)) return true;
11022
+ var _a2;
11023
+ if (!((_a2 = config == null ? void 0 : config.permissions) == null ? void 0 : _a2.enabled)) return true;
10303
11024
  if (user && "role" in user) {
10304
11025
  return user.role === role;
10305
11026
  }
@@ -10508,6 +11229,32 @@ function MockAuthProvider({
10508
11229
  };
10509
11230
  return /* @__PURE__ */ jsxRuntime.jsx(AuthContext.Provider, { value, children });
10510
11231
  }
11232
+ function NoAuthProvider({ children }) {
11233
+ const noOpLogin = async () => {
11234
+ console.warn("NoAuthProvider: login() called but auth is disabled");
11235
+ };
11236
+ const noOpLogout = async () => {
11237
+ console.warn("NoAuthProvider: logout() called but auth is disabled");
11238
+ };
11239
+ const noOpRefresh = async () => {
11240
+ console.warn("NoAuthProvider: refreshToken() called but auth is disabled");
11241
+ };
11242
+ const value = {
11243
+ user: null,
11244
+ isAuthenticated: true,
11245
+ // Always authenticated in no-auth mode
11246
+ isLoading: false,
11247
+ permissions: [],
11248
+ login: noOpLogin,
11249
+ logout: noOpLogout,
11250
+ refreshToken: noOpRefresh,
11251
+ hasPermission: () => true,
11252
+ // All permissions granted
11253
+ hasRole: () => true
11254
+ // All roles granted
11255
+ };
11256
+ return /* @__PURE__ */ jsxRuntime.jsx(AuthContext.Provider, { value, children });
11257
+ }
10511
11258
  const MockAuthContext = React.createContext(
10512
11259
  void 0
10513
11260
  );
@@ -13647,7 +14394,13 @@ function createReactApp(config) {
13647
14394
  tree = /* @__PURE__ */ jsxRuntime.jsx(NavigationProvider, { initialNavigation: navigation, children: /* @__PURE__ */ jsxRuntime.jsx(SidebarProvider, { children: tree }) });
13648
14395
  }
13649
14396
  if (enableAuth) {
13650
- tree = /* @__PURE__ */ jsxRuntime.jsx(AuthProvider, { config: auth, children: tree });
14397
+ const authMode = (auth == null ? void 0 : auth.mode) || "pattern-stack";
14398
+ if (authMode === "none") {
14399
+ setGlobalAuthService(null);
14400
+ tree = /* @__PURE__ */ jsxRuntime.jsx(NoAuthProvider, { children: tree });
14401
+ } else {
14402
+ tree = /* @__PURE__ */ jsxRuntime.jsx(AuthProvider, { config: auth, children: tree });
14403
+ }
13651
14404
  }
13652
14405
  if (enableQuery) {
13653
14406
  tree = /* @__PURE__ */ jsxRuntime.jsxs(reactQuery.QueryClientProvider, { client: queryClient, children: [
@@ -13665,7 +14418,8 @@ function createReactApp(config) {
13665
14418
  routes.push({ path, component });
13666
14419
  },
13667
14420
  render() {
13668
- const shouldProtect = enableAuth && (auth == null ? void 0 : auth.requireAuth) !== false;
14421
+ const authMode = (auth == null ? void 0 : auth.mode) || "pattern-stack";
14422
+ const shouldProtect = enableAuth && authMode !== "none" && (auth == null ? void 0 : auth.requireAuth) !== false;
13669
14423
  const publicPaths = (auth == null ? void 0 : auth.publicPaths) ?? ["/login", "/register", "/forgot-password"];
13670
14424
  const isPublicPath = (path) => {
13671
14425
  return publicPaths.some((p) => path === p || path.startsWith(p + "/"));
@@ -16852,12 +17606,14 @@ function capitalize(str) {
16852
17606
  exports.APIDataTemplate = APIDataTemplate;
16853
17607
  exports.API_ENDPOINTS = API_ENDPOINTS;
16854
17608
  exports.APP_NAME = APP_NAME;
17609
+ exports.AUTH_TOKEN_KEY = AUTH_TOKEN_KEY;
16855
17610
  exports.Accordion = Accordion;
16856
17611
  exports.ActivityFeed = ActivityFeed;
16857
17612
  exports.AdminCRUDTemplate = AdminCRUDTemplate;
16858
17613
  exports.AdminDashboardTemplate = AdminDashboardTemplate;
16859
17614
  exports.AdminDetailTemplate = AdminDetailTemplate;
16860
17615
  exports.Alert = Alert;
17616
+ exports.ApiClient = ApiClient;
16861
17617
  exports.AppHeader = AppHeader;
16862
17618
  exports.AppLayout = AppLayout;
16863
17619
  exports.AuthDivider = AuthDivider;
@@ -16884,6 +17640,7 @@ exports.Checkbox = Checkbox;
16884
17640
  exports.ColorSwatch = ColorSwatch;
16885
17641
  exports.ComponentShowcasePage = ComponentShowcasePage;
16886
17642
  exports.ComponentShowcaseTemplate = ComponentShowcaseTemplate;
17643
+ exports.ConnectionStatus = ConnectionStatus;
16887
17644
  exports.DASHBOARD_CHART_HEIGHTS = DASHBOARD_CHART_HEIGHTS;
16888
17645
  exports.DASHBOARD_CONTAINER_HEIGHTS = DASHBOARD_CONTAINER_HEIGHTS;
16889
17646
  exports.DASHBOARD_HEIGHT_CLASSES = DASHBOARD_HEIGHT_CLASSES;
@@ -16924,6 +17681,7 @@ exports.EmptyState = EmptyState;
16924
17681
  exports.EnhancedDataTemplate = EnhancedDataTemplate;
16925
17682
  exports.EntityIcon = EntityIcon;
16926
17683
  exports.ErrorBoundary = ErrorBoundary2;
17684
+ exports.FieldGrid = FieldGrid;
16927
17685
  exports.FileUpload = FileUpload;
16928
17686
  exports.FormField = FormField;
16929
17687
  exports.FormGroup = FormGroup;
@@ -16943,6 +17701,7 @@ exports.MockAuthProvider = MockAuthProvider;
16943
17701
  exports.Modal = Modal;
16944
17702
  exports.NavMenu = NavMenu;
16945
17703
  exports.NavigationProvider = NavigationProvider;
17704
+ exports.NoAuthProvider = NoAuthProvider;
16946
17705
  exports.PageTemplate = PageTemplate;
16947
17706
  exports.PageTitle = PageTitle;
16948
17707
  exports.Pagination = Pagination;
@@ -17006,19 +17765,24 @@ exports.apiClient = apiClient;
17006
17765
  exports.breakpoints = breakpoints;
17007
17766
  exports.cn = cn;
17008
17767
  exports.createAPIDataTemplate = createAPIDataTemplate;
17768
+ exports.createApiClient = createApiClient;
17769
+ exports.createMobileCardRenderer = createMobileCardRenderer;
17009
17770
  exports.createReactApp = createReactApp;
17010
17771
  exports.createSimpleApp = createSimpleApp;
17011
17772
  exports.defaultFieldRenderers = defaultFieldRenderers;
17012
17773
  exports.detectBulkOperationType = detectBulkOperationType;
17013
17774
  exports.detectUIConfig = detectUIConfig;
17775
+ exports.entityToListCardProps = entityToListCardProps;
17014
17776
  exports.env = env;
17015
17777
  exports.formatNumberWithTooltip = formatNumberWithTooltip;
17016
17778
  exports.generateBulkOperationName = generateBulkOperationName;
17779
+ exports.generateCompactColumns = generateCompactColumns;
17017
17780
  exports.getAnimationClasses = getAnimationClasses;
17018
17781
  exports.getChartHeight = getChartHeight;
17019
17782
  exports.getContainerHeightClass = getContainerHeightClass;
17020
17783
  exports.getCurrentBreakpoint = getCurrentBreakpoint;
17021
17784
  exports.getFieldType = getFieldType;
17785
+ exports.getGlobalAuthService = getGlobalAuthService;
17022
17786
  exports.getIcon = getIcon;
17023
17787
  exports.getNavigationItems = getNavigationItems;
17024
17788
  exports.getResponsiveClasses = getResponsiveClasses;
@@ -17042,6 +17806,7 @@ exports.useCategoryColors = useCategoryColors;
17042
17806
  exports.useCreateExample = useCreateExample;
17043
17807
  exports.useDeleteExample = useDeleteExample;
17044
17808
  exports.useEntityData = useEntityData;
17809
+ exports.useEntityDetail = useEntityDetail;
17045
17810
  exports.useErrorBoundary = useErrorBoundary;
17046
17811
  exports.useFieldMetadata = useFieldMetadata;
17047
17812
  exports.useGetExample = useGetExample;
@@ -17053,6 +17818,7 @@ exports.useMaxMediaQuery = useMaxMediaQuery;
17053
17818
  exports.useMediaQuery = useMediaQuery;
17054
17819
  exports.useMockAuth = useMockAuth;
17055
17820
  exports.useNavigation = useNavigation;
17821
+ exports.useOnlineStatus = useOnlineStatus;
17056
17822
  exports.useOverflowDetection = useOverflowDetection;
17057
17823
  exports.usePermissions = usePermissions;
17058
17824
  exports.useResponsiveClasses = useResponsiveClasses;