@kyro-cms/admin 0.1.7 → 0.1.8

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 (71) hide show
  1. package/package.json +5 -3
  2. package/src/components/Admin.tsx +1 -1
  3. package/src/components/AutoForm.tsx +966 -337
  4. package/src/components/CreateView.tsx +1 -1
  5. package/src/components/DetailView.tsx +1 -1
  6. package/src/components/EnhancedListView.tsx +156 -52
  7. package/src/components/ListView.tsx +1 -1
  8. package/src/components/Modal.tsx +65 -8
  9. package/src/components/Sidebar.astro +2 -2
  10. package/src/components/ThemeProvider.tsx +8 -2
  11. package/src/components/blocks/AccordionBlock.tsx +20 -52
  12. package/src/components/blocks/ArrayBlock.tsx +40 -31
  13. package/src/components/blocks/BlockEditModal.tsx +170 -581
  14. package/src/components/blocks/ButtonBlock.tsx +27 -128
  15. package/src/components/blocks/CodeBlock.tsx +88 -40
  16. package/src/components/blocks/ColumnsBlock.tsx +27 -85
  17. package/src/components/blocks/FileBlock.tsx +38 -39
  18. package/src/components/blocks/HeadingBlock.tsx +9 -31
  19. package/src/components/blocks/HeroBlock.tsx +42 -100
  20. package/src/components/blocks/ImageBlock.tsx +6 -7
  21. package/src/components/blocks/LinkBlock.tsx +27 -33
  22. package/src/components/blocks/ListBlock.tsx +47 -26
  23. package/src/components/blocks/RelationshipBlock.tsx +26 -233
  24. package/src/components/blocks/RichTextBlock.tsx +66 -0
  25. package/src/components/blocks/VStackBlock.tsx +23 -37
  26. package/src/components/blocks/VideoBlock.tsx +52 -32
  27. package/src/components/fields/AccordionField.tsx +213 -0
  28. package/src/components/fields/ArrayField.tsx +241 -0
  29. package/src/components/fields/BlocksField.tsx +5 -5
  30. package/src/components/fields/ButtonField.tsx +53 -0
  31. package/src/components/fields/CheckboxField.tsx +7 -3
  32. package/src/components/fields/ChildrenField.tsx +48 -0
  33. package/src/components/fields/CodeField.tsx +154 -94
  34. package/src/components/fields/ColumnsField.tsx +137 -0
  35. package/src/components/fields/DateField.tsx +9 -24
  36. package/src/components/fields/EditorClient.tsx +426 -160
  37. package/src/components/fields/HeadingField.tsx +31 -0
  38. package/src/components/fields/HeroField.tsx +101 -0
  39. package/src/components/fields/JSONField.tsx +7 -27
  40. package/src/components/fields/LinkField.tsx +81 -0
  41. package/src/components/fields/ListField.tsx +74 -0
  42. package/src/components/fields/MarkdownField.tsx +4 -26
  43. package/src/components/fields/NumberField.tsx +9 -27
  44. package/src/components/fields/PortableTextField.tsx +61 -49
  45. package/src/components/fields/RelationshipBlockField.tsx +233 -0
  46. package/src/components/fields/RelationshipField.tsx +59 -13
  47. package/src/components/fields/SelectField.tsx +6 -4
  48. package/src/components/fields/TextField.tsx +9 -24
  49. package/src/components/fields/UploadField.tsx +613 -0
  50. package/src/components/fields/VideoField.tsx +73 -0
  51. package/src/components/fields/extensions/blockComponents.tsx +11 -1
  52. package/src/components/fields/extensions/blocksStore.ts +1 -1
  53. package/src/components/fields/index.ts +12 -1
  54. package/src/components/layout/Layout.tsx +1 -1
  55. package/src/lib/api.ts +163 -0
  56. package/src/lib/config.ts +1 -1
  57. package/src/lib/dataStore.ts +87 -30
  58. package/src/lib/date-utils.ts +69 -0
  59. package/src/lib/db/version-adapter.ts +248 -0
  60. package/src/lib/i18n.tsx +353 -0
  61. package/src/lib/slugify.ts +15 -0
  62. package/src/lib/validation.ts +250 -0
  63. package/src/pages/api/[collection]/[id]/publish.ts +12 -4
  64. package/src/pages/api/[collection]/[id]/versions.ts +39 -9
  65. package/src/pages/api/[collection]/[id].ts +13 -1
  66. package/src/pages/api/[collection]/index.ts +5 -6
  67. package/src/styles/main.css +12 -2
  68. package/src/components/blocks/BlockEditModal.MARKER +0 -12
  69. package/src/components/fields/FileField.tsx +0 -390
  70. package/src/components/fields/HybridContentField.tsx +0 -109
  71. package/src/components/fields/ImageField.tsx +0 -429
@@ -1,5 +1,5 @@
1
1
  import { useState } from "react";
2
- import type { KyroConfig, CollectionConfig } from "@kyro-cms/core";
2
+ import type { KyroConfig, CollectionConfig } from "@kyro-cms/core/client";
3
3
  import { AutoForm } from "./AutoForm";
4
4
  import { Spinner } from "./ui/Spinner";
5
5
 
@@ -3,7 +3,7 @@ import type {
3
3
  KyroConfig,
4
4
  CollectionConfig,
5
5
  GlobalConfig,
6
- } from "@kyro-cms/core";
6
+ } from "@kyro-cms/core/client";
7
7
  import { AutoForm } from "./AutoForm";
8
8
  import { ActionBar, type DocumentStatus, type SaveStatus } from "./ActionBar";
9
9
  import { ConfirmModal } from "./ui/Modal";
@@ -8,6 +8,8 @@ export interface FieldConfig {
8
8
  label?: string;
9
9
  required?: boolean;
10
10
  options?: { value: string; label: string }[];
11
+ fields?: FieldConfig[];
12
+ tabs?: Array<{ label?: string; name?: string; fields?: FieldConfig[] }>;
11
13
  admin?: {
12
14
  hidden?: boolean;
13
15
  readonly?: boolean;
@@ -23,20 +25,21 @@ export interface CollectionConfig {
23
25
  admin?: {
24
26
  description?: string;
25
27
  defaultColumns?: string[];
28
+ useAsTitle?: string;
26
29
  };
27
30
  }
28
31
 
29
32
  interface FilterConfig {
30
33
  field: string;
31
34
  operator:
32
- | "equals"
33
- | "contains"
34
- | "gt"
35
- | "lt"
36
- | "gte"
37
- | "lte"
38
- | "between"
39
- | "in";
35
+ | "equals"
36
+ | "contains"
37
+ | "gt"
38
+ | "lt"
39
+ | "gte"
40
+ | "lte"
41
+ | "between"
42
+ | "in";
40
43
  value: string;
41
44
  }
42
45
 
@@ -91,19 +94,75 @@ export function EnhancedListView({
91
94
  const [showFilters, setShowFilters] = useState(false);
92
95
  const [showColumns, setShowColumns] = useState(false);
93
96
 
97
+ function flattenFields(fields: FieldConfig[]): FieldConfig[] {
98
+ const result: FieldConfig[] = [];
99
+ for (const field of fields) {
100
+ if (!field.name || field.admin?.hidden || field.name === "id") continue;
101
+ if (field.type === "tabs" && field.tabs) {
102
+ for (const tab of field.tabs) {
103
+ if (tab.fields) {
104
+ result.push(...flattenFields(tab.fields));
105
+ }
106
+ }
107
+ } else if (
108
+ (field.type === "row" || field.type === "collapsible") &&
109
+ field.fields
110
+ ) {
111
+ result.push(...flattenFields(field.fields));
112
+ } else {
113
+ result.push(field);
114
+ }
115
+ }
116
+ return result;
117
+ }
118
+
94
119
  const allFields = useMemo(
95
- () =>
96
- collection.fields.filter(
97
- (f) => f.name && !f.admin?.hidden && f.name !== "id",
98
- ),
120
+ () => flattenFields(collection.fields),
99
121
  [collection.fields],
100
122
  );
101
123
 
102
124
  const [visibleColumns, setVisibleColumns] = useState<Set<string>>(() => {
125
+ if (collection.admin?.defaultColumns) {
126
+ return new Set(collection.admin.defaultColumns);
127
+ }
103
128
  const defaultCols = allFields.slice(0, 4).map((f) => f.name);
104
129
  return new Set(defaultCols);
105
130
  });
106
131
 
132
+ const toggleColumn = useCallback((fieldName: string) => {
133
+ setVisibleColumns((prev) => {
134
+ const next = new Set(prev);
135
+ if (next.has(fieldName)) {
136
+ next.delete(fieldName);
137
+ } else {
138
+ next.add(fieldName);
139
+ }
140
+ return next;
141
+ });
142
+ }, []);
143
+
144
+ function resolveSortField(fieldName: string): string {
145
+ const field = allFields.find((f) => f.name === fieldName);
146
+ if (!field) return fieldName;
147
+ if (field.type === "group" && field.fields?.[0]?.name) {
148
+ return `${fieldName}.${field.fields[0].name}`;
149
+ }
150
+ return fieldName;
151
+ }
152
+
153
+ const handleSort = useCallback((fieldName: string) => {
154
+ const resolvedField = resolveSortField(fieldName);
155
+ setSort((prev) => {
156
+ if (prev && prev.field === resolvedField) {
157
+ return {
158
+ field: resolvedField,
159
+ direction: prev.direction === "asc" ? "desc" : "asc",
160
+ };
161
+ }
162
+ return { field: resolvedField, direction: "asc" };
163
+ });
164
+ }, []);
165
+
107
166
  const [deleteConfirm, setDeleteConfirm] = useState<{
108
167
  open: boolean;
109
168
  count: number;
@@ -115,6 +174,34 @@ export function EnhancedListView({
115
174
  [allFields, visibleColumns],
116
175
  );
117
176
 
177
+ const titleField =
178
+ collection.admin?.useAsTitle ||
179
+ allFields.find((f) => f.type !== "group")?.name;
180
+
181
+ function fieldContainsTitle(field: FieldConfig): boolean {
182
+ if (field.name === titleField) return true;
183
+ if (field.type === "group" && field.fields?.[0]?.name === titleField)
184
+ return true;
185
+ return false;
186
+ }
187
+
188
+ function extractFieldValue(doc: any, field: FieldConfig): any {
189
+ if (doc[field.name] !== undefined && doc[field.name] !== null) {
190
+ return doc[field.name];
191
+ }
192
+ if (field.type === "group" && typeof doc[field.name] === "object") {
193
+ if (
194
+ field.fields?.[0]?.name &&
195
+ doc[field.name][field.fields[0].name] !== undefined
196
+ ) {
197
+ return doc[field.name][field.fields[0].name];
198
+ }
199
+ const firstKey = Object.keys(doc[field.name])[0];
200
+ if (firstKey) return doc[field.name][firstKey];
201
+ }
202
+ return null;
203
+ }
204
+
118
205
  const fetchDocs = useCallback(async () => {
119
206
  setLoading(true);
120
207
  try {
@@ -188,7 +275,6 @@ export function EnhancedListView({
188
275
  const totalPages = Math.ceil(totalDocs / limit);
189
276
  const hasActiveFilters = search || filters.length > 0 || sort;
190
277
 
191
-
192
278
  return (
193
279
  <div className="space-y-6">
194
280
  <ConfirmModal
@@ -214,7 +300,8 @@ export function EnhancedListView({
214
300
  )}
215
301
  </p>
216
302
  </div>
217
- <button type="button"
303
+ <button
304
+ type="button"
218
305
  onClick={onCreate}
219
306
  className="flex items-center gap-2 px-5 py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold transition-all hover:opacity-90 active:scale-95 shadow-lg"
220
307
  >
@@ -264,12 +351,14 @@ export function EnhancedListView({
264
351
 
265
352
  <div className="flex items-center gap-2 flex-wrap">
266
353
  {/* Filter Toggle */}
267
- <button type="button"
354
+ <button
355
+ type="button"
268
356
  onClick={() => setShowFilters(!showFilters)}
269
- className={`flex items-center gap-2 px-4 py-2 rounded-xl font-bold text-sm transition-all ${showFilters || filters.length > 0
270
- ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)]"
271
- : "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
272
- }`}
357
+ className={`flex items-center gap-2 px-4 py-2 rounded-xl font-bold text-sm transition-all ${
358
+ showFilters || filters.length > 0
359
+ ? "bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)]"
360
+ : "bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)]"
361
+ }`}
273
362
  >
274
363
  <svg
275
364
  className="w-4 h-4"
@@ -294,7 +383,8 @@ export function EnhancedListView({
294
383
 
295
384
  {/* Column Toggle */}
296
385
  <div className="relative">
297
- <button type="button"
386
+ <button
387
+ type="button"
298
388
  onClick={() => setShowColumns(!showColumns)}
299
389
  className="flex items-center gap-2 px-4 py-2 rounded-xl font-bold text-sm bg-[var(--kyro-surface-accent)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-all"
300
390
  >
@@ -314,7 +404,7 @@ export function EnhancedListView({
314
404
  Columns
315
405
  </button>
316
406
  {showColumns && (
317
- <div className="absolute right-0 top-full mt-2 w-56 surface-tile border border-[var(--kyro-border)] rounded-xl shadow-xl z-50 overflow-hidden">
407
+ <div className="absolute right-0 top-full mt-2 w-56 surface-tile border border-[var(--kyro-border)] rounded-lg shadow-xl z-50 overflow-hidden">
318
408
  <div className="p-3 border-b border-[var(--kyro-border)]">
319
409
  <span className="text-xs font-bold uppercase tracking-wider text-[var(--kyro-text-secondary)]">
320
410
  Toggle Columns
@@ -344,7 +434,8 @@ export function EnhancedListView({
344
434
 
345
435
  {/* Clear All */}
346
436
  {hasActiveFilters && (
347
- <button type="button"
437
+ <button
438
+ type="button"
348
439
  onClick={clearAll}
349
440
  className="px-4 py-2 rounded-xl font-bold text-sm text-red-500 hover:bg-red-50 dark:hover:bg-red-500/10 transition-all"
350
441
  >
@@ -361,7 +452,8 @@ export function EnhancedListView({
361
452
  <h3 className="font-bold text-[var(--kyro-text-primary)]">
362
453
  Advanced Filters
363
454
  </h3>
364
- <button type="button"
455
+ <button
456
+ type="button"
365
457
  onClick={addFilter}
366
458
  className="flex items-center gap-2 px-3 py-1.5 text-sm font-bold text-[var(--kyro-sidebar-active)] hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all"
367
459
  >
@@ -422,7 +514,8 @@ export function EnhancedListView({
422
514
  placeholder="Value..."
423
515
  className="flex-1 min-w-[150px] px-3 py-2 bg-[var(--kyro-bg)] border border-[var(--kyro-border)] rounded-lg text-sm font-medium text-[var(--kyro-text-primary)]"
424
516
  />
425
- <button type="button"
517
+ <button
518
+ type="button"
426
519
  onClick={() => removeFilter(index)}
427
520
  className="p-2 text-[var(--kyro-text-muted)] hover:text-red-500 transition-colors"
428
521
  >
@@ -458,7 +551,8 @@ export function EnhancedListView({
458
551
  {selectedIds.size} selected
459
552
  </span>
460
553
  <div className="flex gap-2">
461
- <button type="button"
554
+ <button
555
+ type="button"
462
556
  onClick={handleBulkDelete}
463
557
  className="flex items-center gap-2 px-4 py-2 bg-red-500 text-white rounded-lg font-bold text-sm hover:bg-red-600 transition-all"
464
558
  >
@@ -477,7 +571,8 @@ export function EnhancedListView({
477
571
  </svg>
478
572
  Delete Selected
479
573
  </button>
480
- <button type="button"
574
+ <button
575
+ type="button"
481
576
  onClick={() => setSelectedIds(new Set())}
482
577
  className="px-4 py-2 text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] font-bold text-sm transition-all"
483
578
  >
@@ -519,7 +614,8 @@ export function EnhancedListView({
519
614
  : `Get started by creating your first ${(collection.singularLabel || collection.label || collectionSlug).toLowerCase()}.`}
520
615
  </p>
521
616
  {!hasActiveFilters && (
522
- <button type="button"
617
+ <button
618
+ type="button"
523
619
  onClick={onCreate}
524
620
  className="mt-4 inline-flex items-center gap-2 px-5 py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-lg font-bold text-sm shadow-md"
525
621
  >
@@ -563,7 +659,8 @@ export function EnhancedListView({
563
659
  onClick={() => handleSort(field.name)}
564
660
  >
565
661
  <div className="flex items-center gap-2">
566
- {checkTabbedValue(displayFields, field.type) ?? (field.label || field.name)}
662
+ {checkTabbedValue(displayFields, field.type) ??
663
+ (field.label || field.name)}
567
664
  {sort?.field === field.name && (
568
665
  <svg
569
666
  className={`w-3 h-3 ${sort.direction === "desc" ? "rotate-180" : ""}`}
@@ -606,29 +703,33 @@ export function EnhancedListView({
606
703
  className="w-4 h-4 rounded border-[var(--kyro-border-strong)] text-[var(--kyro-sidebar-active)] focus:ring-[var(--kyro-sidebar-active)]"
607
704
  />
608
705
  </td>
609
- {displayFields.map((field, i) => (
610
- <td
611
- key={field.name}
612
- className={`px-4 py-3 ${i === 0 ? "font-bold text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)]"}`}
613
- >
614
- {field.type === "select" && doc[field.name]
615
- ? field.options?.find(
616
- (o) => o.value === doc[field.name],
617
- )?.label || (doc[field.name])
618
- : formatCellValue(doc[field.name], field.type)}
619
- </td>
620
- ))}
706
+ {displayFields.map((field) => {
707
+ const rawValue = extractFieldValue(doc, field);
708
+ const cellValue =
709
+ field.type === "select" && rawValue
710
+ ? field.options?.find((o) => o.value === rawValue)
711
+ ?.label || rawValue
712
+ : formatCellValue(rawValue, field.type);
713
+ return (
714
+ <td
715
+ key={field.name}
716
+ className={`px-4 py-3 ${fieldContainsTitle(field) ? "font-bold text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)]"}`}
717
+ >
718
+ {cellValue}
719
+ </td>
720
+ );
721
+ })}
621
722
  {collection.timestamps && (
622
723
  <td className="px-4 py-3 text-sm text-[var(--kyro-text-secondary)]">
623
724
  {doc.createdAt
624
725
  ? new Date(doc.createdAt).toLocaleDateString(
625
- "en-US",
626
- {
627
- month: "short",
628
- day: "numeric",
629
- year: "numeric",
630
- },
631
- )
726
+ "en-US",
727
+ {
728
+ month: "short",
729
+ day: "numeric",
730
+ year: "numeric",
731
+ },
732
+ )
632
733
  : "—"}
633
734
  </td>
634
735
  )}
@@ -637,7 +738,8 @@ export function EnhancedListView({
637
738
  onClick={(e) => e.stopPropagation()}
638
739
  >
639
740
  <div className="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
640
- <button type="button"
741
+ <button
742
+ type="button"
641
743
  onClick={() => onEdit(doc.slug || doc.id)}
642
744
  className="flex items-center gap-2 px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg text-sm font-bold text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-all"
643
745
  title="Edit"
@@ -656,7 +758,8 @@ export function EnhancedListView({
656
758
  />
657
759
  </svg>
658
760
  </button>
659
- <button type="button"
761
+ <button
762
+ type="button"
660
763
  onClick={() => handleDeleteSingle(doc.id)}
661
764
  className="inline-flex items-center justify-center w-8 h-8 rounded-md text-[var(--kyro-text-muted)] hover:bg-red-50 hover:text-red-500 dark:hover:bg-red-500/10 transition-colors"
662
765
  title="Delete"
@@ -719,7 +822,8 @@ export function EnhancedListView({
719
822
  </div>
720
823
  <div className="flex gap-2">
721
824
  {page > 1 && (
722
- <button type="button"
825
+ <button
826
+ type="button"
723
827
  onClick={() => setPage(page - 1)}
724
828
  className="px-4 py-2 border border-[var(--kyro-border)] rounded-lg text-sm font-bold text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] transition-colors"
725
829
  >
@@ -727,7 +831,8 @@ export function EnhancedListView({
727
831
  </button>
728
832
  )}
729
833
  {page < totalPages && (
730
- <button type="button"
834
+ <button
835
+ type="button"
731
836
  onClick={() => setPage(page + 1)}
732
837
  className="px-4 py-2 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-lg text-sm font-bold hover:opacity-90 transition-all"
733
838
  >
@@ -778,7 +883,6 @@ function formatCellValue(value: any, type?: string): string {
778
883
  return String(value).slice(0, 60);
779
884
  }
780
885
 
781
-
782
886
  function checkTabbedValue(data: any[], type: string): string | undefined {
783
887
  if (type !== "tabs") return;
784
888
  const label = data[0]?.tabs[0]?.fields[0]?.label;
@@ -1,5 +1,5 @@
1
1
  import { useState, useEffect } from "react";
2
- import type { CollectionConfig, KyroConfig } from "@kyro-cms/core";
2
+ import type { CollectionConfig, KyroConfig } from "@kyro-cms/core/client";
3
3
  import { Spinner } from "./ui/Spinner";
4
4
  import { ConfirmModal } from "./ui/Modal";
5
5
  import { Search, Plus, Settings } from "lucide-react";
@@ -1,14 +1,19 @@
1
- import {
2
- useEffect,
3
- useState,
4
- useCallback,
1
+ import React, {
5
2
  createContext,
6
3
  useContext,
4
+ useState,
5
+ useCallback,
7
6
  type ReactNode,
8
7
  } from "react";
9
- import { AlertTriangle, HelpCircle } from "lucide-react";
8
+ import {
9
+ Modal as UIModal,
10
+ ModalContent,
11
+ ModalActions,
12
+ ConfirmModal,
13
+ } from "./ui/Modal";
14
+ import { PromptModal } from "./ui/PromptModal";
10
15
 
11
- export { Modal, ModalContent, ModalActions, ConfirmModal } from "./ui/Modal";
16
+ export { UIModal as Modal, UIModal, ModalContent, ModalActions, ConfirmModal };
12
17
 
13
18
  // ============================================================================
14
19
  // Global Modal Context for programmatic access
@@ -25,6 +30,7 @@ interface ModalState {
25
30
  defaultValue?: string;
26
31
  onConfirm?: (value?: string) => void;
27
32
  onCancel?: () => void;
33
+ danger?: boolean;
28
34
  }
29
35
 
30
36
  const initialState: ModalState = {
@@ -34,11 +40,17 @@ const initialState: ModalState = {
34
40
  message: "",
35
41
  onConfirm: () => {},
36
42
  onCancel: () => {},
43
+ danger: false,
37
44
  };
38
45
 
39
46
  interface ModalContextType {
40
47
  showAlert: (title: string, message?: string) => void;
41
- showConfirm: (title: string, message: string, onConfirm: () => void) => void;
48
+ showConfirm: (
49
+ title: string,
50
+ message: string,
51
+ onConfirm: () => void,
52
+ options?: { danger?: boolean },
53
+ ) => void;
42
54
  showPrompt: (
43
55
  title: string,
44
56
  message: string,
@@ -78,12 +90,18 @@ export function ModalProvider({ children }: ModalProviderProps) {
78
90
  }, []);
79
91
 
80
92
  const showConfirm = useCallback(
81
- (title: string, message: string, onConfirm: () => void) => {
93
+ (
94
+ title: string,
95
+ message: string,
96
+ onConfirm: () => void,
97
+ options?: { danger?: boolean },
98
+ ) => {
82
99
  setState({
83
100
  variant: "confirm",
84
101
  open: true,
85
102
  title,
86
103
  message,
104
+ danger: options?.danger,
87
105
  onConfirm: () => {
88
106
  onConfirm();
89
107
  setState((s) => ({ ...s, open: false }));
@@ -144,6 +162,45 @@ export function ModalProvider({ children }: ModalProviderProps) {
144
162
  }}
145
163
  >
146
164
  {children}
165
+ {state.variant === "confirm" && (
166
+ <ConfirmModal
167
+ open={state.open}
168
+ onClose={state.onCancel || closeModal}
169
+ onConfirm={() => state.onConfirm?.()}
170
+ title={state.title}
171
+ message={state.message || ""}
172
+ variant={state.danger ? "danger" : "default"}
173
+ />
174
+ )}
175
+ {state.variant === "alert" && (
176
+ <UIModal
177
+ open={state.open}
178
+ onClose={state.onCancel || closeModal}
179
+ title={state.title}
180
+ size="sm"
181
+ footer={
182
+ <button
183
+ type="button"
184
+ onClick={() => state.onConfirm?.()}
185
+ className="px-4 py-2 rounded-lg font-medium text-sm bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] hover:opacity-90 transition-colors"
186
+ >
187
+ OK
188
+ </button>
189
+ }
190
+ >
191
+ <p className="text-[var(--kyro-text-secondary)]">{state.message}</p>
192
+ </UIModal>
193
+ )}
194
+ {state.variant === "prompt" && (
195
+ <PromptModal
196
+ open={state.open}
197
+ onClose={state.onCancel || closeModal}
198
+ onSubmit={(value) => state.onConfirm?.(value)}
199
+ title={state.title}
200
+ placeholder={state.placeholder}
201
+ defaultValue={state.defaultValue}
202
+ />
203
+ )}
147
204
  </ModalContext.Provider>
148
205
  );
149
206
  }
@@ -116,7 +116,7 @@ function isActive(item: NavItem): boolean {
116
116
  {section.items.map((item) => (
117
117
  <a
118
118
  href={item.href}
119
- class={`flex items-center gap-4 px-6 py-2 rounded-2xl transition-all font-bold ${
119
+ class={`flex items-center gap-4 px-6 py-2 rounded-2xl transition-all font-semibold ${
120
120
  item.icon === "collection"
121
121
  ? currentPath === item.href ||
122
122
  currentPath.startsWith(item.href + "/")
@@ -215,7 +215,7 @@ function isActive(item: NavItem): boolean {
215
215
  </a>
216
216
  <button
217
217
  id="logout-btn"
218
- class="flex justify-center p-2.5 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-xl transition-all shadow-sm active:scale-95 font-bold"
218
+ class="flex justify-center p-2.5 text-red-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-xl transition-all shadow-sm active:scale-95 font-semibold"
219
219
  title="Logout"
220
220
  >
221
221
  <svg
@@ -9,7 +9,7 @@ import {
9
9
  defaultLightTheme,
10
10
  defaultDarkTheme,
11
11
  type ThemeConfig,
12
- } from "@kyro-cms/core";
12
+ } from "@kyro-cms/core/client";
13
13
 
14
14
  export type ThemeMode = "light" | "dark" | "system";
15
15
 
@@ -25,7 +25,13 @@ const ThemeContext = createContext<ThemeContextValue | null>(null);
25
25
  export function useTheme() {
26
26
  const context = useContext(ThemeContext);
27
27
  if (!context) {
28
- throw new Error("useTheme must be used within a ThemeProvider");
28
+ // Return default light theme if used outside of a provider to prevent crashes
29
+ return {
30
+ mode: "light" as ThemeMode,
31
+ theme: defaultLightTheme,
32
+ setMode: () => {},
33
+ setCustomTheme: () => {},
34
+ };
29
35
  }
30
36
  return context;
31
37
  }