@kyro-cms/admin 0.1.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 (102) hide show
  1. package/.astro/content.d.ts +154 -0
  2. package/.astro/settings.json +5 -0
  3. package/.astro/types.d.ts +2 -0
  4. package/astro.config.mjs +28 -0
  5. package/bun.lock +1374 -0
  6. package/dist/client/_astro/AdminLayout.DkDpng53.css +1 -0
  7. package/dist/client/_astro/AutoForm.3eJCmCJp.js +1 -0
  8. package/dist/client/_astro/client.DyczpTbx.js +9 -0
  9. package/dist/client/_astro/index.B02hbnpo.js +1 -0
  10. package/dist/client/fonts/Serotiva-Black.woff2 +0 -0
  11. package/dist/client/fonts/Serotiva-Bold.woff2 +0 -0
  12. package/dist/client/fonts/Serotiva-Medium.woff2 +0 -0
  13. package/dist/client/fonts/Serotiva-Regular.woff2 +0 -0
  14. package/dist/client/fonts/Serotiva-SemiBold.woff2 +0 -0
  15. package/dist/server/chunks/AdminLayout_D-_JeUqC.mjs +26 -0
  16. package/dist/server/chunks/_id__BzI_o0qT.mjs +50 -0
  17. package/dist/server/chunks/_id__Cd-jOuY3.mjs +238 -0
  18. package/dist/server/chunks/_id__DvbD--iR.mjs +992 -0
  19. package/dist/server/chunks/_id__vpVaEo16.mjs +128 -0
  20. package/dist/server/chunks/_virtual_astro_server-island-manifest_CQQ1F5PF.mjs +7 -0
  21. package/dist/server/chunks/_virtual_astro_session-driver_Bk3Q189E.mjs +4 -0
  22. package/dist/server/chunks/astro-component_Dbx3T2Nh.mjs +37 -0
  23. package/dist/server/chunks/audit-logs_DrnUMRvY.mjs +74 -0
  24. package/dist/server/chunks/config_CPXslElD.mjs +4221 -0
  25. package/dist/server/chunks/dataStore_Dl7cA2Qp.mjs +89 -0
  26. package/dist/server/chunks/index_CVqOkerS.mjs +2960 -0
  27. package/dist/server/chunks/index_CX8SQ4BF.mjs +55 -0
  28. package/dist/server/chunks/index_CYofDU51.mjs +58 -0
  29. package/dist/server/chunks/index_DdNRhuaM.mjs +55 -0
  30. package/dist/server/chunks/index_DupPvtIF.mjs +42 -0
  31. package/dist/server/chunks/index_YTS_M-B9.mjs +263 -0
  32. package/dist/server/chunks/index_YeCzuVps.mjs +53 -0
  33. package/dist/server/chunks/login_DLyqMRO8.mjs +93 -0
  34. package/dist/server/chunks/logout_CSbt5wea.mjs +50 -0
  35. package/dist/server/chunks/me_C04jlYhH.mjs +41 -0
  36. package/dist/server/chunks/new_BbQ9b55M.mjs +92 -0
  37. package/dist/server/chunks/node_9bvTewss.mjs +1014 -0
  38. package/dist/server/chunks/noop-entrypoint_BOlrdqWF.mjs +3 -0
  39. package/dist/server/chunks/sequence_9cl7AJy-.mjs +2503 -0
  40. package/dist/server/chunks/server_peBx9VXG.mjs +8117 -0
  41. package/dist/server/chunks/sharp_pmJ7nHES.mjs +142 -0
  42. package/dist/server/chunks/users_Dzddy_YR.mjs +137 -0
  43. package/dist/server/entry.mjs +5 -0
  44. package/dist/server/virtual_astro_middleware.mjs +48 -0
  45. package/package.json +33 -0
  46. package/public/fonts/Serotiva-Black.woff2 +0 -0
  47. package/public/fonts/Serotiva-Bold.woff2 +0 -0
  48. package/public/fonts/Serotiva-Medium.woff2 +0 -0
  49. package/public/fonts/Serotiva-Regular.woff2 +0 -0
  50. package/public/fonts/Serotiva-SemiBold.woff2 +0 -0
  51. package/src/collections/auth/index.ts +155 -0
  52. package/src/components/ActionBar.tsx +215 -0
  53. package/src/components/Admin.tsx +214 -0
  54. package/src/components/AutoForm.tsx +1123 -0
  55. package/src/components/BulkActionsBar.tsx +80 -0
  56. package/src/components/CreateView.tsx +99 -0
  57. package/src/components/DetailView.tsx +329 -0
  58. package/src/components/Icons.tsx +23 -0
  59. package/src/components/ListView.tsx +192 -0
  60. package/src/components/StatusBadge.tsx +76 -0
  61. package/src/components/ThemeProvider.tsx +155 -0
  62. package/src/components/VersionHistoryPanel.tsx +205 -0
  63. package/src/components/fields/CheckboxField.tsx +37 -0
  64. package/src/components/fields/DateField.tsx +42 -0
  65. package/src/components/fields/NumberField.tsx +44 -0
  66. package/src/components/fields/RelationshipField.tsx +87 -0
  67. package/src/components/fields/SelectField.tsx +56 -0
  68. package/src/components/fields/TextField.tsx +49 -0
  69. package/src/components/index.ts +30 -0
  70. package/src/components/layout/Breadcrumbs.tsx +36 -0
  71. package/src/components/layout/Header.tsx +37 -0
  72. package/src/components/layout/Layout.tsx +25 -0
  73. package/src/components/layout/Sidebar.tsx +462 -0
  74. package/src/components/ui/Badge.tsx +14 -0
  75. package/src/components/ui/Button.tsx +41 -0
  76. package/src/components/ui/Dropdown.tsx +82 -0
  77. package/src/components/ui/Modal.tsx +135 -0
  78. package/src/components/ui/SlidePanel.tsx +73 -0
  79. package/src/components/ui/Spinner.tsx +24 -0
  80. package/src/components/ui/Toast.tsx +78 -0
  81. package/src/layouts/AdminLayout.astro +197 -0
  82. package/src/lib/config.ts +68 -0
  83. package/src/lib/dataStore.ts +111 -0
  84. package/src/middleware.ts +48 -0
  85. package/src/pages/[collection]/[id].astro +176 -0
  86. package/src/pages/[collection]/index.astro +180 -0
  87. package/src/pages/api/[collection]/[id].ts +258 -0
  88. package/src/pages/api/[collection]/index.ts +289 -0
  89. package/src/pages/api/auth/[id].ts +142 -0
  90. package/src/pages/api/auth/audit-logs.ts +80 -0
  91. package/src/pages/api/auth/login.ts +101 -0
  92. package/src/pages/api/auth/logout.ts +48 -0
  93. package/src/pages/api/auth/me.ts +36 -0
  94. package/src/pages/api/auth/users.ts +150 -0
  95. package/src/pages/audit/index.astro +110 -0
  96. package/src/pages/index.astro +225 -0
  97. package/src/pages/roles/index.astro +114 -0
  98. package/src/pages/users/[id].astro +174 -0
  99. package/src/pages/users/index.astro +142 -0
  100. package/src/pages/users/new.astro +91 -0
  101. package/src/styles/main.css +1449 -0
  102. package/tsconfig.json +12 -0
@@ -0,0 +1,462 @@
1
+ import React, { useState, useEffect } from "react";
2
+ import type { CollectionConfig, GlobalConfig } from "@kyro-cms/core";
3
+
4
+ interface SidebarProps {
5
+ collections: Record<string, CollectionConfig>;
6
+ globals: Record<string, GlobalConfig>;
7
+ activeCollection: string | null;
8
+ activeGlobal: string | null;
9
+ onCollectionClick: (name: string) => void;
10
+ onGlobalClick: (name: string) => void;
11
+ defaultCollapsed?: boolean;
12
+ onToggleCollapse?: (collapsed: boolean) => void;
13
+ }
14
+
15
+ interface CollectionGroup {
16
+ label: string;
17
+ collections: [string, CollectionConfig][];
18
+ }
19
+
20
+ function groupCollections(
21
+ entries: [string, CollectionConfig][],
22
+ ): CollectionGroup[] {
23
+ const groups: Record<string, [string, CollectionConfig][]> = {
24
+ content: [],
25
+ ecommerce: [],
26
+ other: [],
27
+ };
28
+
29
+ const contentCollections = ["posts", "pages", "media", "categories", "tags"];
30
+ const ecommerceCollections = [
31
+ "products",
32
+ "orders",
33
+ "customers",
34
+ "coupons",
35
+ "inventory",
36
+ ];
37
+
38
+ for (const [name, config] of entries) {
39
+ if (contentCollections.includes(name)) {
40
+ groups.content.push([name, config]);
41
+ } else if (ecommerceCollections.includes(name)) {
42
+ groups.ecommerce.push([name, config]);
43
+ } else {
44
+ groups.other.push([name, config]);
45
+ }
46
+ }
47
+
48
+ const result: CollectionGroup[] = [];
49
+
50
+ if (groups.content.length > 0) {
51
+ result.push({ label: "Content", collections: groups.content });
52
+ }
53
+ if (groups.ecommerce.length > 0) {
54
+ result.push({ label: "E-commerce", collections: groups.ecommerce });
55
+ }
56
+ if (groups.other.length > 0) {
57
+ if (groups.content.length === 0 && groups.ecommerce.length === 0) {
58
+ result.push({ label: "Collections", collections: groups.other });
59
+ } else {
60
+ result.push({ label: "Other", collections: groups.other });
61
+ }
62
+ }
63
+
64
+ return result;
65
+ }
66
+
67
+ function getGlobalIcon(slug: string): React.ReactElement {
68
+ const icons: Record<string, React.ReactElement> = {
69
+ "site-settings": (
70
+ <svg
71
+ width="18"
72
+ height="18"
73
+ viewBox="0 0 24 24"
74
+ fill="none"
75
+ stroke="currentColor"
76
+ strokeWidth="2"
77
+ >
78
+ <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
79
+ <polyline points="9,22 9,12 15,12 15,22" />
80
+ </svg>
81
+ ),
82
+ "seo-settings": (
83
+ <svg
84
+ width="18"
85
+ height="18"
86
+ viewBox="0 0 24 24"
87
+ fill="none"
88
+ stroke="currentColor"
89
+ strokeWidth="2"
90
+ >
91
+ <circle cx="11" cy="11" r="8" />
92
+ <path d="m21 21-4.35-4.35" />
93
+ </svg>
94
+ ),
95
+ "social-settings": (
96
+ <svg
97
+ width="18"
98
+ height="18"
99
+ viewBox="0 0 24 24"
100
+ fill="none"
101
+ stroke="currentColor"
102
+ strokeWidth="2"
103
+ >
104
+ <circle cx="18" cy="5" r="3" />
105
+ <circle cx="6" cy="12" r="3" />
106
+ <circle cx="18" cy="19" r="3" />
107
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
108
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
109
+ </svg>
110
+ ),
111
+ "email-settings": (
112
+ <svg
113
+ width="18"
114
+ height="18"
115
+ viewBox="0 0 24 24"
116
+ fill="none"
117
+ stroke="currentColor"
118
+ strokeWidth="2"
119
+ >
120
+ <path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z" />
121
+ <polyline points="22,6 12,13 2,6" />
122
+ </svg>
123
+ ),
124
+ "storage-settings": (
125
+ <svg
126
+ width="18"
127
+ height="18"
128
+ viewBox="0 0 24 24"
129
+ fill="none"
130
+ stroke="currentColor"
131
+ strokeWidth="2"
132
+ >
133
+ <path d="M22 12.22A2 2 0 0 0 21.78 10H20a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h.22" />
134
+ <path d="M2 12.22A2 2 0 0 1 2.22 10H4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H2.22" />
135
+ <path d="M22 4.28A2 2 0 0 0 21.78 2H20a2 2 0 0 0-2 2v4a2 2 0 0 0 2 2h.22" />
136
+ <path d="M2 4.28A2 2 0 0 1 2.22 2H4a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H2.22" />
137
+ </svg>
138
+ ),
139
+ "access-settings": (
140
+ <svg
141
+ width="18"
142
+ height="18"
143
+ viewBox="0 0 24 24"
144
+ fill="none"
145
+ stroke="currentColor"
146
+ strokeWidth="2"
147
+ >
148
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
149
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
150
+ </svg>
151
+ ),
152
+ "store-settings": (
153
+ <svg
154
+ width="18"
155
+ height="18"
156
+ viewBox="0 0 24 24"
157
+ fill="none"
158
+ stroke="currentColor"
159
+ strokeWidth="2"
160
+ >
161
+ <path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
162
+ <path d="M3 6h18" />
163
+ <path d="M16 10a4 4 0 0 1-8 0" />
164
+ </svg>
165
+ ),
166
+ "payment-settings": (
167
+ <svg
168
+ width="18"
169
+ height="18"
170
+ viewBox="0 0 24 24"
171
+ fill="none"
172
+ stroke="currentColor"
173
+ strokeWidth="2"
174
+ >
175
+ <rect x="1" y="4" width="22" height="16" rx="2" ry="2" />
176
+ <line x1="1" y1="10" x2="23" y2="10" />
177
+ </svg>
178
+ ),
179
+ };
180
+
181
+ return (
182
+ icons[slug] || (
183
+ <svg
184
+ width="18"
185
+ height="18"
186
+ viewBox="0 0 24 24"
187
+ fill="none"
188
+ stroke="currentColor"
189
+ strokeWidth="2"
190
+ >
191
+ <circle cx="12" cy="12" r="3" />
192
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
193
+ </svg>
194
+ )
195
+ );
196
+ }
197
+
198
+ export function Sidebar({
199
+ collections,
200
+ globals,
201
+ activeCollection,
202
+ activeGlobal,
203
+ onCollectionClick,
204
+ onGlobalClick,
205
+ defaultCollapsed = false,
206
+ onToggleCollapse,
207
+ }: SidebarProps) {
208
+ const [collapsed, setCollapsed] = useState(() => {
209
+ if (typeof window !== "undefined") {
210
+ const stored = localStorage.getItem("kyro-sidebar-collapsed");
211
+ return stored ? stored === "true" : defaultCollapsed;
212
+ }
213
+ return defaultCollapsed;
214
+ });
215
+
216
+ useEffect(() => {
217
+ localStorage.setItem("kyro-sidebar-collapsed", String(collapsed));
218
+ onToggleCollapse?.(collapsed);
219
+ }, [collapsed, onToggleCollapse]);
220
+
221
+ const collectionEntries = Object.entries(collections);
222
+ const globalEntries = Object.entries(globals);
223
+ const collectionGroups = groupCollections(collectionEntries);
224
+
225
+ const toggleCollapse = () => {
226
+ setCollapsed((prev) => !prev);
227
+ };
228
+
229
+ return (
230
+ <aside className={`kyro-sidebar ${collapsed ? "collapsed" : ""}`}>
231
+ <div className="kyro-sidebar-logo">
232
+ {!collapsed && <span className="kyro-sidebar-logo-text">Kyro</span>}
233
+ {collapsed && (
234
+ <div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center text-white font-bold text-sm">
235
+ K
236
+ </div>
237
+ )}
238
+ </div>
239
+
240
+ <nav className="kyro-sidebar-nav">
241
+ {collectionGroups.map((group) => (
242
+ <div key={group.label} className="kyro-sidebar-section">
243
+ {!collapsed && (
244
+ <div className="kyro-sidebar-section-title">{group.label}</div>
245
+ )}
246
+ {group.collections.map(([name, config]) => (
247
+ <button
248
+ key={name}
249
+ className={`kyro-sidebar-item ${
250
+ activeCollection === name ? "active" : ""
251
+ }`}
252
+ onClick={() => onCollectionClick(name)}
253
+ title={collapsed ? config.label || name : undefined}
254
+ >
255
+ <CollectionIcon type={name} />
256
+ {!collapsed && <span>{config.label || name}</span>}
257
+ </button>
258
+ ))}
259
+ </div>
260
+ ))}
261
+
262
+ {globalEntries.length > 0 && (
263
+ <div className="kyro-sidebar-section">
264
+ {!collapsed && (
265
+ <div className="kyro-sidebar-section-title">Settings</div>
266
+ )}
267
+ {globalEntries.map(([name, config]) => (
268
+ <button
269
+ key={name}
270
+ className={`kyro-sidebar-item ${
271
+ activeGlobal === name ? "active" : ""
272
+ }`}
273
+ onClick={() => onGlobalClick(name)}
274
+ title={collapsed ? config.label || name : undefined}
275
+ >
276
+ {getGlobalIcon(name)}
277
+ {!collapsed && <span>{config.label || name}</span>}
278
+ </button>
279
+ ))}
280
+ </div>
281
+ )}
282
+ </nav>
283
+
284
+ <div className="kyro-sidebar-footer">
285
+ <button
286
+ className="kyro-sidebar-item"
287
+ onClick={toggleCollapse}
288
+ title={collapsed ? "Expand sidebar" : "Collapse sidebar"}
289
+ >
290
+ <svg
291
+ width="18"
292
+ height="18"
293
+ viewBox="0 0 24 24"
294
+ fill="none"
295
+ stroke="currentColor"
296
+ strokeWidth="2"
297
+ >
298
+ {collapsed ? (
299
+ <path d="M13 17l5-5-5-5M6 17l5-5-5-5" />
300
+ ) : (
301
+ <path d="M11 17l-5-5 5-5M18 17l-5-5 5-5" />
302
+ )}
303
+ </svg>
304
+ {!collapsed && <span>Collapse</span>}
305
+ </button>
306
+ </div>
307
+ </aside>
308
+ );
309
+ }
310
+
311
+ function CollectionIcon({ type }: { type: string }): React.ReactElement {
312
+ const iconMap: Record<string, React.ReactElement> = {
313
+ posts: (
314
+ <svg
315
+ width="18"
316
+ height="18"
317
+ viewBox="0 0 24 24"
318
+ fill="none"
319
+ stroke="currentColor"
320
+ strokeWidth="2"
321
+ >
322
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
323
+ <path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" />
324
+ </svg>
325
+ ),
326
+ pages: (
327
+ <svg
328
+ width="18"
329
+ height="18"
330
+ viewBox="0 0 24 24"
331
+ fill="none"
332
+ stroke="currentColor"
333
+ strokeWidth="2"
334
+ >
335
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
336
+ <path d="M14 2v6h6" />
337
+ </svg>
338
+ ),
339
+ products: (
340
+ <svg
341
+ width="18"
342
+ height="18"
343
+ viewBox="0 0 24 24"
344
+ fill="none"
345
+ stroke="currentColor"
346
+ strokeWidth="2"
347
+ >
348
+ <path d="M6 2L3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z" />
349
+ <path d="M3 6h18M16 10a4 4 0 0 1-8 0" />
350
+ </svg>
351
+ ),
352
+ orders: (
353
+ <svg
354
+ width="18"
355
+ height="18"
356
+ viewBox="0 0 24 24"
357
+ fill="none"
358
+ stroke="currentColor"
359
+ strokeWidth="2"
360
+ >
361
+ <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2" />
362
+ <rect x="8" y="2" width="8" height="4" rx="1" ry="1" />
363
+ </svg>
364
+ ),
365
+ customers: (
366
+ <svg
367
+ width="18"
368
+ height="18"
369
+ viewBox="0 0 24 24"
370
+ fill="none"
371
+ stroke="currentColor"
372
+ strokeWidth="2"
373
+ >
374
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
375
+ <circle cx="9" cy="7" r="4" />
376
+ <path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75" />
377
+ </svg>
378
+ ),
379
+ media: (
380
+ <svg
381
+ width="18"
382
+ height="18"
383
+ viewBox="0 0 24 24"
384
+ fill="none"
385
+ stroke="currentColor"
386
+ strokeWidth="2"
387
+ >
388
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
389
+ <circle cx="8.5" cy="8.5" r="1.5" />
390
+ <path d="M21 15l-5-5L5 21" />
391
+ </svg>
392
+ ),
393
+ categories: (
394
+ <svg
395
+ width="18"
396
+ height="18"
397
+ viewBox="0 0 24 24"
398
+ fill="none"
399
+ stroke="currentColor"
400
+ strokeWidth="2"
401
+ >
402
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" />
403
+ </svg>
404
+ ),
405
+ tags: (
406
+ <svg
407
+ width="18"
408
+ height="18"
409
+ viewBox="0 0 24 24"
410
+ fill="none"
411
+ stroke="currentColor"
412
+ strokeWidth="2"
413
+ >
414
+ <path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
415
+ <line x1="7" y1="7" x2="7.01" y2="7" />
416
+ </svg>
417
+ ),
418
+ coupons: (
419
+ <svg
420
+ width="18"
421
+ height="18"
422
+ viewBox="0 0 24 24"
423
+ fill="none"
424
+ stroke="currentColor"
425
+ strokeWidth="2"
426
+ >
427
+ <path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z" />
428
+ <line x1="7" y1="7" x2="7.01" y2="7" />
429
+ </svg>
430
+ ),
431
+ inventory: (
432
+ <svg
433
+ width="18"
434
+ height="18"
435
+ viewBox="0 0 24 24"
436
+ fill="none"
437
+ stroke="currentColor"
438
+ strokeWidth="2"
439
+ >
440
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
441
+ <polyline points="3.27,6.96 12,12.01 20.73,6.96" />
442
+ <line x1="12" y1="22.08" x2="12" y2="12" />
443
+ </svg>
444
+ ),
445
+ };
446
+
447
+ return (
448
+ iconMap[type] || (
449
+ <svg
450
+ width="18"
451
+ height="18"
452
+ viewBox="0 0 24 24"
453
+ fill="none"
454
+ stroke="currentColor"
455
+ strokeWidth="2"
456
+ >
457
+ <path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
458
+ <path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
459
+ </svg>
460
+ )
461
+ );
462
+ }
@@ -0,0 +1,14 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ interface BadgeProps {
4
+ variant?: 'default' | 'success' | 'warning' | 'danger' | 'info';
5
+ children: ReactNode;
6
+ }
7
+
8
+ export function Badge({ variant = 'default', children }: BadgeProps) {
9
+ return (
10
+ <span className={`kyro-badge kyro-badge-${variant}`}>
11
+ {children}
12
+ </span>
13
+ );
14
+ }
@@ -0,0 +1,41 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
+
3
+ interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
4
+ variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
5
+ size?: 'sm' | 'md' | 'lg';
6
+ loading?: boolean;
7
+ children: ReactNode;
8
+ }
9
+
10
+ export function Button({
11
+ variant = 'secondary',
12
+ size = 'md',
13
+ loading = false,
14
+ children,
15
+ className = '',
16
+ disabled,
17
+ ...props
18
+ }: ButtonProps) {
19
+ const baseClass = 'kyro-btn';
20
+ const variantClass = `kyro-btn-${variant}`;
21
+ const sizeClass = `kyro-btn-${size}`;
22
+
23
+ return (
24
+ <button
25
+ className={`${baseClass} ${variantClass} ${sizeClass} ${className}`}
26
+ disabled={disabled || loading}
27
+ {...props}
28
+ >
29
+ {loading ? (
30
+ <span className="kyro-btn-loading">
31
+ <svg className="kyro-btn-spinner" viewBox="0 0 24 24">
32
+ <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="3" fill="none" strokeDasharray="60 30" />
33
+ </svg>
34
+ </span>
35
+ ) : null}
36
+ <span className={loading ? 'kyro-btn-text-hidden' : ''}>
37
+ {children}
38
+ </span>
39
+ </button>
40
+ );
41
+ }
@@ -0,0 +1,82 @@
1
+ import React, { useState, useRef, useEffect, type ReactNode } from "react";
2
+
3
+ interface DropdownProps {
4
+ trigger: ReactNode;
5
+ children: ReactNode;
6
+ align?: "left" | "right";
7
+ }
8
+
9
+ export function Dropdown({
10
+ trigger,
11
+ children,
12
+ align = "right",
13
+ }: DropdownProps) {
14
+ const [open, setOpen] = useState(false);
15
+ const ref = useRef<HTMLDivElement>(null);
16
+
17
+ useEffect(() => {
18
+ const handleClickOutside = (e: MouseEvent) => {
19
+ if (ref.current && !ref.current.contains(e.target as Node)) {
20
+ setOpen(false);
21
+ }
22
+ };
23
+
24
+ if (open) {
25
+ document.addEventListener("mousedown", handleClickOutside);
26
+ }
27
+ return () => document.removeEventListener("mousedown", handleClickOutside);
28
+ }, [open]);
29
+
30
+ return (
31
+ <div className="relative" ref={ref}>
32
+ <div onClick={() => setOpen(!open)} className="cursor-pointer">
33
+ {trigger}
34
+ </div>
35
+ {open && (
36
+ <div
37
+ className={`absolute z-50 mt-2 min-w-[180px] py-1 bg-white rounded-lg shadow-lg border border-gray-200 ${
38
+ align === "right" ? "right-0" : "left-0"
39
+ }`}
40
+ onClick={() => setOpen(false)}
41
+ >
42
+ {children}
43
+ </div>
44
+ )}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ interface DropdownItemProps {
50
+ children: ReactNode;
51
+ onClick?: () => void;
52
+ icon?: ReactNode;
53
+ danger?: boolean;
54
+ disabled?: boolean;
55
+ }
56
+
57
+ export function DropdownItem({
58
+ children,
59
+ onClick,
60
+ icon,
61
+ danger,
62
+ disabled,
63
+ }: DropdownItemProps) {
64
+ return (
65
+ <button
66
+ onClick={onClick}
67
+ disabled={disabled}
68
+ className={`w-full flex items-center gap-3 px-3 py-2 text-sm text-left transition-colors ${
69
+ danger
70
+ ? "text-red-600 hover:bg-red-50"
71
+ : "text-gray-700 hover:bg-gray-50"
72
+ } ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
73
+ >
74
+ {icon && <span className="w-4 h-4">{icon}</span>}
75
+ {children}
76
+ </button>
77
+ );
78
+ }
79
+
80
+ export function DropdownSeparator() {
81
+ return <div className="my-1 border-t border-gray-100" />;
82
+ }