@kyro-cms/admin 0.9.1 → 0.9.2

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 (37) hide show
  1. package/dist/index.cjs +1196 -1727
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +4 -3
  4. package/dist/index.d.ts +4 -3
  5. package/dist/index.js +891 -1422
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/components/ActionBar.tsx +25 -174
  9. package/src/components/Admin.tsx +1 -3
  10. package/src/components/AuditLogsPage.tsx +2 -13
  11. package/src/components/AutoForm.tsx +160 -265
  12. package/src/components/DetailView.tsx +38 -66
  13. package/src/components/FieldRenderer.tsx +1 -1
  14. package/src/components/ListView.tsx +26 -198
  15. package/src/components/MediaGallery.tsx +117 -175
  16. package/src/components/RestPlayground.tsx +54 -47
  17. package/src/components/fields/BlocksField.tsx +8 -10
  18. package/src/components/fields/RelationshipBlockField.tsx +2 -3
  19. package/src/components/fields/RelationshipField.tsx +2 -3
  20. package/src/components/fix_imports.cjs +23 -0
  21. package/src/components/fix_imports2.cjs +19 -0
  22. package/src/components/replace_svgs.cjs +63 -0
  23. package/src/components/ui/Dropdown.tsx +7 -2
  24. package/src/components/ui/Modal.tsx +24 -27
  25. package/src/components/ui/PromptModal.tsx +2 -10
  26. package/src/components/ui/SlidePanel.tsx +2 -10
  27. package/src/components/ui/SplitButton.tsx +107 -0
  28. package/src/components/ui/Toaster.tsx +0 -1
  29. package/src/components/ui/icons.tsx +1 -0
  30. package/src/components/users/UsersList.tsx +8 -85
  31. package/src/hooks/useAutoFormState.ts +89 -161
  32. package/src/hooks/useQueue.ts +60 -0
  33. package/src/layouts/AdminLayout.astro +22 -2
  34. package/src/layouts/AuthLayout.astro +66 -18
  35. package/src/lib/autoform-store.ts +6 -2
  36. package/src/lib/globals.ts +5 -3
  37. package/src/pages/auth/register.astro +5 -2
@@ -0,0 +1,63 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const dir = '/Users/macbook/Dev/Web/Astro/kyro-cms/admin/src/components';
5
+ const getFiles = (dir) => {
6
+ const dirents = fs.readdirSync(dir, { withFileTypes: true });
7
+ const files = dirents.map((dirent) => {
8
+ const res = path.resolve(dir, dirent.name);
9
+ return dirent.isDirectory() ? getFiles(res) : res;
10
+ });
11
+ return Array.prototype.concat(...files).filter(f => f.endsWith('.tsx'));
12
+ };
13
+
14
+ const files = getFiles(dir);
15
+
16
+ const replacements = [
17
+ { match: /<svg[^>]*>\s*<path[^>]*d="M12 4v16m8-8H4"[^>]*\/>\s*<\/svg>/g, replace: '<Plus className="w-4 h-4" />', icon: 'Plus' },
18
+ { match: /<svg[^>]*>\s*<path[^>]*d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12"[^>]*\/>\s*<\/svg>/g, replace: '<Upload className="w-4 h-4" />', icon: 'Upload' },
19
+ { match: /<svg[^>]*>\s*<path[^>]*d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"[^>]*\/>\s*<\/svg>/g, replace: '<Copy className="w-4 h-4" />', icon: 'Copy' },
20
+ { match: /<svg[^>]*>\s*<path[^>]*d="M19 7l-\.867 12\.142A2 2 0 0116\.138 21H7\.862a2 2 0 01-1\.995-1\.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"[^>]*\/>\s*<\/svg>/g, replace: '<Trash2 className="w-4 h-4" />', icon: 'Trash2' },
21
+ { match: /<svg[^>]*>\s*<path[^>]*d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"[^>]*\/>\s*<\/svg>/g, replace: '<Lock className="w-4 h-4" />', icon: 'Lock' },
22
+ { match: /<svg[^>]*>\s*<path[^>]*d="M10 18a8 8 0 100-16 8 8 0 000 16zm3\.707-9\.293a1 1 0 00-1\.414-1\.414L9 10\.586 7\.707 9\.293a1 1 0 00-1\.414 1\.414l2 2a1 1 0 001\.414 0l4-4z"[^>]*\/>\s*<\/svg>/g, replace: '<CheckCircle2 className="w-4 h-4" />', icon: 'CheckCircle2' },
23
+ { match: /<svg[^>]*>\s*<path[^>]*d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1\.414-9\.414a2 2 0 112\.828 2\.828L11\.828 15H9v-2\.828l8\.586-8\.586z"[^>]*\/>\s*<\/svg>/g, replace: '<Edit2 className="w-4 h-4" />', icon: 'Edit2' },
24
+ { match: /<svg[^>]*>\s*<path[^>]*d="M10 18a8 8 0 100-16 8 8 0 000 16zM8\.707 7\.293a1 1 0 00-1\.414 1\.414L8\.586 10l-1\.293 1\.293a1 1 0 101\.414 1\.414L10 11\.414l1\.293 1\.293a1 1 0 001\.414-1\.414L11\.414 10l1\.293-1\.293a1 1 0 00-1\.414-1\.414L10 8\.586 8\.707 7\.293z"[^>]*\/>\s*<\/svg>/g, replace: '<XCircle className="w-4 h-4" />', icon: 'XCircle' },
25
+ { match: /<svg[^>]*>\s*<path[^>]*d="M6 18L18 6M6 6l12 12"[^>]*\/>\s*<\/svg>/g, replace: '<X className="w-4 h-4" />', icon: 'X' },
26
+ { match: /<svg[^>]*>\s*<path[^>]*d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"[^>]*\/>\s*<\/svg>/g, replace: '<Search className="w-4 h-4" />', icon: 'Search' },
27
+ { match: /<svg[^>]*>\s*<path[^>]*d="M5 13l4 4L19 7"[^>]*\/>\s*<\/svg>/g, replace: '<Check className="w-4 h-4" />', icon: 'Check' },
28
+ { match: /<svg[^>]*>\s*<path[^>]*d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2\.586a1 1 0 01-\.293\.707l-6\.414 6\.414a1 1 0 00-\.293\.707V17l-4 4v-6\.586a1 1 0 00-\.293-\.707L3\.293 7\.293A1 1 0 013 6\.586V4z"[^>]*\/>\s*<\/svg>/g, replace: '<Filter className="w-4 h-4" />', icon: 'Filter' },
29
+ { match: /<svg[^>]*>\s*<path[^>]*d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2"[^>]*\/>\s*<\/svg>/g, replace: '<Columns3 className="w-4 h-4" />', icon: 'Columns3' },
30
+ { match: /<svg[^>]*>\s*<path[^>]*d="M12 5v14M5 12h14"[^>]*\/>\s*<\/svg>/g, replace: '<Plus className="w-4 h-4" />', icon: 'Plus' },
31
+ { match: /<svg[^>]*>\s*<path[^>]*d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"[^>]*\/>\s*<\/svg>/g, replace: '<Archive className="w-4 h-4" />', icon: 'Archive' },
32
+ { match: /<svg[^>]*>\s*<path[^>]*d="M5 15l7-7 7 7"[^>]*\/>\s*<\/svg>/g, replace: '<ChevronUp className="w-4 h-4" />', icon: 'ChevronUp' },
33
+ { match: /<svg[^>]*>\s*<path[^>]*d="M15 19l-7-7 7-7"[^>]*\/>\s*<\/svg>/g, replace: '<ChevronRight className="w-4 h-4" />', icon: 'ChevronRight' },
34
+ { match: /<svg[^>]*>\s*<path[^>]*d="M20 6L9 17l-5-5"[^>]*\/>\s*<\/svg>/g, replace: '<Check className="w-4 h-4" />', icon: 'Check' },
35
+ { match: /<svg[^>]*>\s*<path[^>]*d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"[^>]*\/>\s*<\/svg>/g, replace: '<ExternalLink className="w-4 h-4" />', icon: 'ExternalLink' },
36
+ { match: /<svg[^>]*>\s*<path[^>]*d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"[^>]*\/>\s*<\/svg>/g, replace: '<File className="w-4 h-4" />', icon: 'File' },
37
+ { match: /<svg[^>]*>\s*<path[^>]*d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"[^>]*\/>\s*<\/svg>/g, replace: '<Clipboard className="w-4 h-4" />', icon: 'Clipboard' },
38
+ { match: /<svg[^>]*>\s*<path[^>]*d="M18 6L6 18M6 6l12 12"[^>]*\/>\s*<\/svg>/g, replace: '<X className="w-4 h-4" />', icon: 'X' },
39
+ { match: /<svg[^>]*>\s*<path[^>]*d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2H5a2 2 0 00-2 2v1m2 2a2 2 0 11-4 0 2 2 0 014 0zm2 2h\.008v\.008H5v-\.008z"[^>]*\/>\s*<\/svg>/g, replace: '<Server className="w-8 h-8 text-[var(--kyro-sidebar-text-active)]" />', icon: 'Server' }
40
+ ];
41
+
42
+ const results = {};
43
+
44
+ for (const file of files) {
45
+ let content = fs.readFileSync(file, 'utf8');
46
+ let changed = false;
47
+ let addedIcons = new Set();
48
+
49
+ for (const r of replacements) {
50
+ if (content.match(r.match)) {
51
+ content = content.replace(r.match, r.replace);
52
+ addedIcons.add(r.icon);
53
+ changed = true;
54
+ }
55
+ }
56
+
57
+ if (changed) {
58
+ results[file] = Array.from(addedIcons);
59
+ fs.writeFileSync(file, content);
60
+ }
61
+ }
62
+
63
+ console.log(JSON.stringify(results, null, 2));
@@ -4,12 +4,14 @@ interface DropdownProps {
4
4
  trigger: ReactNode;
5
5
  children: ReactNode;
6
6
  align?: "left" | "right";
7
+ direction?: "up" | "down";
7
8
  }
8
9
 
9
10
  export function Dropdown({
10
11
  trigger,
11
12
  children,
12
13
  align = "right",
14
+ direction = "up",
13
15
  }: DropdownProps) {
14
16
  const [open, setOpen] = useState(false);
15
17
  const ref = useRef<HTMLDivElement>(null);
@@ -34,8 +36,11 @@ export function Dropdown({
34
36
  </div>
35
37
  {open && (
36
38
  <div
37
- className={`absolute z-[100] mt-2 min-w-[200px] py-2 bg-[var(--kyro-surface)] rounded-2xl shadow-2xl border border-[var(--kyro-border)] animate-in fade-in zoom-in-95 duration-100 ${align === "right" ? "right-0 bottom-full mb-2" : "left-0 bottom-full mb-2"
38
- }`}
39
+ className={`absolute z-[100] min-w-[200px] py-2 bg-[var(--kyro-surface)] rounded-2xl shadow-2xl border border-[var(--kyro-border)] animate-in fade-in zoom-in-95 duration-100 ${
40
+ align === "right" ? "right-0" : "left-0"
41
+ } ${
42
+ direction === "down" ? "top-full mt-2" : "bottom-full mb-2"
43
+ }`}
39
44
  onClick={() => setOpen(false)}
40
45
  >
41
46
  {children}
@@ -1,3 +1,4 @@
1
+ import { X } from "./icons";
1
2
  import React, { useEffect, type ReactNode } from "react";
2
3
  import { createPortal } from "react-dom";
3
4
 
@@ -17,8 +18,8 @@ interface ModalProps {
17
18
  title: string;
18
19
  children: ReactNode;
19
20
  footer?: ReactNode;
20
- size?: "sm" | "md" | "lg";
21
- variant?: "default" | "danger";
21
+ size?: "sm" | "md" | "lg" | "full";
22
+ variant?: "default" | "danger" | "lightbox";
22
23
  }
23
24
 
24
25
  export function Modal({
@@ -52,43 +53,39 @@ export function Modal({
52
53
  sm: "max-w-sm",
53
54
  md: "max-w-md",
54
55
  lg: "max-w-lg",
56
+ full: "w-full h-full max-w-none rounded-none border-0",
55
57
  };
56
58
 
59
+ const isLightbox = variant === "lightbox";
60
+
57
61
  return createPortal(
58
62
  <div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
59
63
  <div
60
- className="absolute inset-0 bg-[var(--kyro-black)]/40 backdrop-blur-md transition-all duration-500"
64
+ className={`absolute inset-0 transition-all duration-500 ${isLightbox ? "bg-black/95 backdrop-blur-none" : "bg-[var(--kyro-black)]/40 backdrop-blur-md"}`}
61
65
  onClick={onClose}
62
66
  />
63
67
  <div
64
- className={`relative w-full ${sizeClasses[size]} bg-[var(--kyro-surface)] rounded-[var(--kyro-radius-lg)] shadow-2xl animate-in fade-in zoom-in-95 duration-300 border ${variant === "danger"
68
+ className={`relative ${sizeClasses[size]} ${isLightbox ? "bg-transparent text-white" : "bg-[var(--kyro-surface)]"} ${!isLightbox && size !== "full" ? "rounded-[var(--kyro-radius-lg)]" : ""} shadow-2xl animate-in fade-in zoom-in-95 duration-300 ${!isLightbox ? "border" : ""} ${variant === "danger"
65
69
  ? "border-red-500/30"
66
70
  : "border-[var(--kyro-border)]"
67
- } overflow-hidden`}
71
+ } flex flex-col overflow-hidden`}
68
72
  >
69
- <div className="flex items-center justify-between px-8 py-6 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/50 backdrop-blur-md">
70
- <h2 className="text-xl font-bold text-[var(--kyro-text-primary)]">
71
- {title}
72
- </h2>
73
- <button
74
- type="button"
75
- onClick={onClose}
76
- className="p-2 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] rounded-xl hover:bg-[var(--kyro-surface)] transition-all duration-200"
77
- >
78
- <svg
79
- width="20"
80
- height="20"
81
- viewBox="0 0 24 24"
82
- fill="none"
83
- stroke="currentColor"
84
- strokeWidth="2.5"
73
+ {!isLightbox && (
74
+ <div className="flex items-center justify-between px-8 py-6 border-b border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/50 backdrop-blur-md">
75
+ <h2 className="text-xl font-bold text-[var(--kyro-text-primary)]">
76
+ {title}
77
+ </h2>
78
+ <button
79
+ type="button"
80
+ onClick={onClose}
81
+ className="p-2 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] rounded-xl hover:bg-[var(--kyro-surface)] transition-all duration-200"
85
82
  >
86
- <path d="M18 6L6 18M6 6l12 12" />
87
- </svg>
88
- </button>
89
- </div>
90
- <div className="px-8 py-8">{children}</div>
91
- {footer && (
83
+ <X className="w-4 h-4" />
84
+ </button>
85
+ </div>
86
+ )}
87
+ <div className={`flex-1 overflow-auto ${isLightbox ? "" : "px-8 py-8"}`}>{children}</div>
88
+ {footer && !isLightbox && (
92
89
  <div className="flex items-center justify-end gap-3 px-8 py-6 border-t border-[var(--kyro-border)] bg-[var(--kyro-surface-accent)]/50">
93
90
  {footer}
94
91
  </div>
@@ -1,3 +1,4 @@
1
+ import { X } from "./icons";
1
2
  import React, { useState } from "react";
2
3
  import { createPortal } from "react-dom";
3
4
 
@@ -47,16 +48,7 @@ export function PromptModal({
47
48
  onClick={onClose}
48
49
  className="p-1 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] rounded-lg hover:bg-[var(--kyro-surface-accent)] transition-colors"
49
50
  >
50
- <svg
51
- width="20"
52
- height="20"
53
- viewBox="0 0 24 24"
54
- fill="none"
55
- stroke="currentColor"
56
- strokeWidth="2"
57
- >
58
- <path d="M18 6L6 18M6 6l12 12" />
59
- </svg>
51
+ <X className="w-4 h-4" />
60
52
  </button>
61
53
  </div>
62
54
  <form onSubmit={handleSubmit}>
@@ -1,3 +1,4 @@
1
+ import { X } from "./icons";
1
2
  import React, { useEffect, useRef, useState, type ReactNode } from "react";
2
3
  import { createPortal } from "react-dom";
3
4
 
@@ -78,16 +79,7 @@ export function SlidePanel({
78
79
  onClick={onClose}
79
80
  className="p-1.5 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-colors"
80
81
  >
81
- <svg
82
- width="16"
83
- height="16"
84
- viewBox="0 0 24 24"
85
- fill="none"
86
- stroke="currentColor"
87
- strokeWidth="2"
88
- >
89
- <path d="M18 6L6 18M6 6l12 12" />
90
- </svg>
82
+ <X className="w-4 h-4" />
91
83
  </button>
92
84
  </div>
93
85
  <div className="flex-1 overflow-y-auto p-4">{children}</div>
@@ -0,0 +1,107 @@
1
+ import React from "react";
2
+ import type { ReactNode } from "react";
3
+ import { Dropdown } from "./Dropdown";
4
+ import { Spinner } from "./Spinner";
5
+
6
+ export type SplitButtonStatus = "draft" | "published" | "scheduled" | "archived";
7
+ export type SplitButtonSaveStatus = "idle" | "saving" | "saved" | "error";
8
+
9
+ export interface SplitButtonProps {
10
+ /** Current publish status of the document */
11
+ status: SplitButtonStatus;
12
+ /** Live save operation status driven by the parent */
13
+ saveStatus: SplitButtonSaveStatus;
14
+ /** True when the form has unsaved changes vs last persisted version */
15
+ hasChanges: boolean;
16
+ /** Called when the main publish button is clicked */
17
+ onPublish: () => void;
18
+ /** Publish-variant items for the chevron dropdown (Save as Draft, Schedule) */
19
+ children?: ReactNode;
20
+ disabled?: boolean;
21
+ direction?: "up" | "down";
22
+ }
23
+
24
+ export function SplitButton({
25
+ status,
26
+ saveStatus,
27
+ hasChanges,
28
+ onPublish,
29
+ children,
30
+ disabled,
31
+ direction = "down",
32
+ }: SplitButtonProps) {
33
+ const isPublishedIdle =
34
+ status === "published" && !hasChanges && saveStatus !== "saving" && saveStatus !== "error";
35
+
36
+ const isDisabled = disabled || saveStatus === "saving" || isPublishedIdle;
37
+
38
+ // ── button colour ──────────────────────────────────────────────────────────
39
+ const btnBase =
40
+ "kyro-btn kyro-btn-sm text-[11px] font-regular tracking-widest transition-all duration-300 rounded-lg";
41
+
42
+ const getBtnClass = () => {
43
+ if (saveStatus === "saving") return `${btnBase} bg-[var(--kyro-primary)]/70 border-[var(--kyro-primary)]/70 text-[var(--kyro-sidebar-text-active)] cursor-wait`;
44
+ if (saveStatus === "saved") return `${btnBase} bg-[var(--kyro-success)] border-[var(--kyro-success)] text-[var(--kyro-sidebar-text-active)]`;
45
+ if (saveStatus === "error") return `${btnBase} bg-[var(--kyro-error)] border-[var(--kyro-error)] text-[var(--kyro-sidebar-text-active)]`;
46
+ if (isPublishedIdle) return `${btnBase} bg-[var(--kyro-gray-200)] border-[var(--kyro-gray-200)] text-[var(--kyro-text-muted)] cursor-not-allowed`;
47
+ // has changes → accent
48
+ return `${btnBase} bg-[var(--kyro-primary)] border-[var(--kyro-primary)] hover:bg-[var(--kyro-primary-hover)]`;
49
+ };
50
+
51
+ const chevronBase =
52
+ "kyro-btn kyro-btn-md px-2 rounded-l-none border-l-[1px] border-white/20 transition-all duration-300";
53
+
54
+ const getChevronClass = () => {
55
+ if (saveStatus === "saving") return `${chevronBase} bg-[var(--kyro-primary)]/70 text-[var(--kyro-sidebar-text-active)] border-[var(--kyro-primary)]/70`;
56
+ if (saveStatus === "saved") return `${chevronBase} bg-[var(--kyro-success)] text-[var(--kyro-sidebar-text-active)] border-[var(--kyro-success)]`;
57
+ if (saveStatus === "error") return `${chevronBase} bg-[var(--kyro-error)] text-[var(--kyro-sidebar-text-active)] border-[var(--kyro-error)]`;
58
+ if (isPublishedIdle) return `${chevronBase} bg-[var(--kyro-gray-200)] text-[var(--kyro-text-muted)] border-[var(--kyro-gray-200)]`;
59
+ return `${chevronBase} bg-[var(--kyro-primary)] text-[var(--kyro-sidebar-text-active)] border-[var(--kyro-primary)] hover:bg-[var(--kyro-primary-hover)]`;
60
+ };
61
+
62
+ // ── label ──────────────────────────────────────────────────────────────────
63
+ const getLabel = () => {
64
+ if (saveStatus === "saving") return "Publishing...";
65
+ if (saveStatus === "saved") return "Published ✓";
66
+ if (saveStatus === "error") return "Retry";
67
+ if (isPublishedIdle) return "Published";
68
+ return "Publish Changes";
69
+ };
70
+
71
+ return (
72
+ <div className="inline-flex items-center">
73
+ {/* Main publish button */}
74
+ <button
75
+ type="button"
76
+ onClick={onPublish}
77
+ disabled={isDisabled}
78
+ className={`${getBtnClass()} ${!children ? "rounded-r-lg border-r border-[var(--kyro-border)]" : ""}`}
79
+ >
80
+ {saveStatus === "saving" && <Spinner size="sm" className="inline mr-1.5" />}
81
+ {isPublishedIdle && (
82
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" className="inline mr-1">
83
+ <polyline points="20 6 9 17 4 12" />
84
+ </svg>
85
+ )}
86
+ {getLabel()}
87
+ </button>
88
+
89
+ {/* Chevron → publish-variant actions only */}
90
+ {children && (
91
+ <Dropdown
92
+ trigger={
93
+ <button type="button" className={getChevronClass()} disabled={saveStatus === "saving"}>
94
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
95
+ <path d="m6 9 6 6 6-6" />
96
+ </svg>
97
+ </button>
98
+ }
99
+ direction={direction}
100
+ >
101
+ {children}
102
+ </Dropdown>
103
+ )}
104
+ </div>
105
+ );
106
+ }
107
+
@@ -2,7 +2,6 @@ import { useToastStore } from "../../lib/stores";
2
2
  import { Toast } from "./Toast";
3
3
 
4
4
  export function Toaster() {
5
- console.log('Toaster mounted');
6
5
  const toasts = useToastStore((state) => state.toasts);
7
6
  const removeToast = useToastStore((state) => state.removeToast);
8
7
 
@@ -110,3 +110,4 @@ export { Grid3X3 as IconGrid3X3 } from "lucide-react";
110
110
  // Direct re-exports for files that still use original lucide-react names
111
111
  export { Activity as Activity, AlignLeft as AlignLeft, Archive as Archive, ArrowDown as ArrowDown, ArrowRight as ArrowRight, ArrowUpRight as ArrowUpRight, Blocks as Blocks, Box as Box, Calendar as Calendar, Check as Check, ChevronDown as ChevronDown, ChevronLeft as ChevronLeft, ChevronRight as ChevronRight, ChevronUp as ChevronUp, Clock as Clock, Code as Code, Columns3 as Columns3, Copy as Copy, Crop as Crop, Download as Download, ExternalLink as ExternalLink, Eye as Eye, EyeOff as EyeOff, File as File, File as FileIcon, FileText as FileText, Globe as Globe, Film as Film, Filter as Filter, Folder as Folder, FolderInput as FolderInput, FolderPlus as FolderPlus, GripVertical as GripVertical, Heading1 as Heading1, Image as Image, Info as Info, Key as Key, LayoutDashboard as LayoutDashboard, Link as Link, Link2 as Link2, List as List, ListOrdered as ListOrdered, Lock as Lock, Mail as Mail, Maximize2 as Maximize2, Menu as Menu, Minus as Minus, Monitor as Monitor, MousePointerClick as MousePointerClick, Music as Music, Palette as Palette, Pause as Pause, Play as Play, Plus as Plus, RefreshCcw as RefreshCcw, RefreshCw as RefreshCw, Save as Save, Search as Search, Send as Send, Settings as Settings, Shield as Shield, Sparkles as Sparkles, Star as Star, Tag as Tag, Terminal as Terminal, ToggleLeft as ToggleLeft, ToggleRight as ToggleRight, Trash2 as Trash2, TrendingUp as TrendingUp, Type as Type, User as User, UserPlus as UserPlus, Users as Users, Video as Video, Webhook as Webhook, X as X, Zap as Zap, CircleHelp as HelpCircle } from "lucide-react";
112
112
  export { CircleCheck as CheckCircle2, Grid3X3 as Grid, House as Home, LayoutPanelTop as Layout, LoaderCircle as Loader2, LockOpen as Unlock, CirclePlay as PlayCircle, TriangleAlert as AlertTriangle, CodeXml as Code2, CloudDownload as DownloadCloud, EllipsisVertical as MoreVertical, ShieldCheck as ShieldCheck, ShieldAlert as ShieldAlert, Pencil as Edit2, Moon as Moon, Sun as Sun, LogOut as LogOut, Database as Database, Hexagon as Hexagon, Network as Network, Book as Book, Bold as Bold, Italic as Italic, Underline as Underline, Strikethrough as Strikethrough, Undo as Undo, Redo as Redo, Dot as Dot, Grid3X3, Laptop as Laptop, Smartphone as Smartphone } from "lucide-react";
113
+ export { Server as Server, XCircle as XCircle, Clipboard as Clipboard, Upload as Upload } from "lucide-react";
@@ -1,3 +1,4 @@
1
+ import { Plus, Lock, CheckCircle2, Edit2, Trash2, XCircle, X } from "../ui/icons";
1
2
  import React, { useState } from "react";
2
3
  import { useUIStore } from "../../lib/stores";
3
4
 
@@ -144,19 +145,7 @@ export function UsersList({
144
145
  href={`${adminPath}/users/new`}
145
146
  className="mt-2 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"
146
147
  >
147
- <svg
148
- className="w-3.5 h-3.5"
149
- fill="none"
150
- stroke="currentColor"
151
- viewBox="0 0 24 24"
152
- >
153
- <path
154
- strokeLinecap="round"
155
- strokeLinejoin="round"
156
- strokeWidth="2.5"
157
- d="M12 5v14M5 12h14"
158
- />
159
- </svg>
148
+ <Plus className="w-4 h-4" />
160
149
  Add User
161
150
  </a>
162
151
  </div>
@@ -214,32 +203,12 @@ export function UsersList({
214
203
  <td className="px-6 py-5">
215
204
  {user.locked ? (
216
205
  <span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-bold bg-red-50 text-red-600">
217
- <svg
218
- className="w-3 h-3"
219
- fill="currentColor"
220
- viewBox="0 0 20 20"
221
- >
222
- <path
223
- fillRule="evenodd"
224
- d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z"
225
- clipRule="evenodd"
226
- />
227
- </svg>
206
+ <Lock className="w-4 h-4" />
228
207
  Locked
229
208
  </span>
230
209
  ) : (
231
210
  <span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-lg text-xs font-bold bg-green-50 text-green-600">
232
- <svg
233
- className="w-3 h-3"
234
- fill="currentColor"
235
- viewBox="0 0 20 20"
236
- >
237
- <path
238
- fillRule="evenodd"
239
- d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
240
- clipRule="evenodd"
241
- />
242
- </svg>
211
+ <CheckCircle2 className="w-4 h-4" />
243
212
  Active
244
213
  </span>
245
214
  )}
@@ -254,19 +223,7 @@ export function UsersList({
254
223
  className="inline-flex items-center justify-center w-8 h-8 rounded-md text-[#64748b] hover:bg-gray-100 hover:text-[#0b1222] transition-colors"
255
224
  onClick={(e) => e.stopPropagation()}
256
225
  >
257
- <svg
258
- className="w-4 h-4"
259
- fill="none"
260
- stroke="currentColor"
261
- viewBox="0 0 24 24"
262
- >
263
- <path
264
- strokeLinecap="round"
265
- strokeLinejoin="round"
266
- strokeWidth="2"
267
- d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
268
- />
269
- </svg>
226
+ <Edit2 className="w-4 h-4" />
270
227
  </a>
271
228
  <button
272
229
  onClick={(e) => {
@@ -275,19 +232,7 @@ export function UsersList({
275
232
  }}
276
233
  className="inline-flex items-center justify-center w-8 h-8 rounded-md text-[#64748b] hover:bg-red-50 hover:text-red-600 transition-colors"
277
234
  >
278
- <svg
279
- className="w-4 h-4"
280
- fill="none"
281
- stroke="currentColor"
282
- viewBox="0 0 24 24"
283
- >
284
- <path
285
- strokeLinecap="round"
286
- strokeLinejoin="round"
287
- strokeWidth="2"
288
- d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
289
- />
290
- </svg>
235
+ <Trash2 className="w-4 h-4" />
291
236
  </button>
292
237
  </div>
293
238
  </td>
@@ -300,35 +245,13 @@ export function UsersList({
300
245
 
301
246
  {errorMsg && (
302
247
  <div className="surface-tile p-4 flex items-center gap-3 border border-red-200">
303
- <svg
304
- className="w-5 h-5 text-red-500 flex-shrink-0"
305
- fill="currentColor"
306
- viewBox="0 0 20 20"
307
- >
308
- <path
309
- fillRule="evenodd"
310
- d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
311
- clipRule="evenodd"
312
- />
313
- </svg>
248
+ <XCircle className="w-4 h-4" />
314
249
  <p className="text-sm font-medium text-red-500">{errorMsg}</p>
315
250
  <button
316
251
  onClick={() => setErrorMsg(null)}
317
252
  className="ml-auto text-red-400 hover:text-red-600"
318
253
  >
319
- <svg
320
- className="w-4 h-4"
321
- fill="none"
322
- stroke="currentColor"
323
- viewBox="0 0 24 24"
324
- >
325
- <path
326
- strokeLinecap="round"
327
- strokeLinejoin="round"
328
- strokeWidth="2"
329
- d="M6 18L18 6M6 6l12 12"
330
- />
331
- </svg>
254
+ <X className="w-4 h-4" />
332
255
  </button>
333
256
  </div>
334
257
  )}