@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.
- package/.astro/content.d.ts +154 -0
- package/.astro/settings.json +5 -0
- package/.astro/types.d.ts +2 -0
- package/astro.config.mjs +28 -0
- package/bun.lock +1374 -0
- package/dist/client/_astro/AdminLayout.DkDpng53.css +1 -0
- package/dist/client/_astro/AutoForm.3eJCmCJp.js +1 -0
- package/dist/client/_astro/client.DyczpTbx.js +9 -0
- package/dist/client/_astro/index.B02hbnpo.js +1 -0
- package/dist/client/fonts/Serotiva-Black.woff2 +0 -0
- package/dist/client/fonts/Serotiva-Bold.woff2 +0 -0
- package/dist/client/fonts/Serotiva-Medium.woff2 +0 -0
- package/dist/client/fonts/Serotiva-Regular.woff2 +0 -0
- package/dist/client/fonts/Serotiva-SemiBold.woff2 +0 -0
- package/dist/server/chunks/AdminLayout_D-_JeUqC.mjs +26 -0
- package/dist/server/chunks/_id__BzI_o0qT.mjs +50 -0
- package/dist/server/chunks/_id__Cd-jOuY3.mjs +238 -0
- package/dist/server/chunks/_id__DvbD--iR.mjs +992 -0
- package/dist/server/chunks/_id__vpVaEo16.mjs +128 -0
- package/dist/server/chunks/_virtual_astro_server-island-manifest_CQQ1F5PF.mjs +7 -0
- package/dist/server/chunks/_virtual_astro_session-driver_Bk3Q189E.mjs +4 -0
- package/dist/server/chunks/astro-component_Dbx3T2Nh.mjs +37 -0
- package/dist/server/chunks/audit-logs_DrnUMRvY.mjs +74 -0
- package/dist/server/chunks/config_CPXslElD.mjs +4221 -0
- package/dist/server/chunks/dataStore_Dl7cA2Qp.mjs +89 -0
- package/dist/server/chunks/index_CVqOkerS.mjs +2960 -0
- package/dist/server/chunks/index_CX8SQ4BF.mjs +55 -0
- package/dist/server/chunks/index_CYofDU51.mjs +58 -0
- package/dist/server/chunks/index_DdNRhuaM.mjs +55 -0
- package/dist/server/chunks/index_DupPvtIF.mjs +42 -0
- package/dist/server/chunks/index_YTS_M-B9.mjs +263 -0
- package/dist/server/chunks/index_YeCzuVps.mjs +53 -0
- package/dist/server/chunks/login_DLyqMRO8.mjs +93 -0
- package/dist/server/chunks/logout_CSbt5wea.mjs +50 -0
- package/dist/server/chunks/me_C04jlYhH.mjs +41 -0
- package/dist/server/chunks/new_BbQ9b55M.mjs +92 -0
- package/dist/server/chunks/node_9bvTewss.mjs +1014 -0
- package/dist/server/chunks/noop-entrypoint_BOlrdqWF.mjs +3 -0
- package/dist/server/chunks/sequence_9cl7AJy-.mjs +2503 -0
- package/dist/server/chunks/server_peBx9VXG.mjs +8117 -0
- package/dist/server/chunks/sharp_pmJ7nHES.mjs +142 -0
- package/dist/server/chunks/users_Dzddy_YR.mjs +137 -0
- package/dist/server/entry.mjs +5 -0
- package/dist/server/virtual_astro_middleware.mjs +48 -0
- package/package.json +33 -0
- package/public/fonts/Serotiva-Black.woff2 +0 -0
- package/public/fonts/Serotiva-Bold.woff2 +0 -0
- package/public/fonts/Serotiva-Medium.woff2 +0 -0
- package/public/fonts/Serotiva-Regular.woff2 +0 -0
- package/public/fonts/Serotiva-SemiBold.woff2 +0 -0
- package/src/collections/auth/index.ts +155 -0
- package/src/components/ActionBar.tsx +215 -0
- package/src/components/Admin.tsx +214 -0
- package/src/components/AutoForm.tsx +1123 -0
- package/src/components/BulkActionsBar.tsx +80 -0
- package/src/components/CreateView.tsx +99 -0
- package/src/components/DetailView.tsx +329 -0
- package/src/components/Icons.tsx +23 -0
- package/src/components/ListView.tsx +192 -0
- package/src/components/StatusBadge.tsx +76 -0
- package/src/components/ThemeProvider.tsx +155 -0
- package/src/components/VersionHistoryPanel.tsx +205 -0
- package/src/components/fields/CheckboxField.tsx +37 -0
- package/src/components/fields/DateField.tsx +42 -0
- package/src/components/fields/NumberField.tsx +44 -0
- package/src/components/fields/RelationshipField.tsx +87 -0
- package/src/components/fields/SelectField.tsx +56 -0
- package/src/components/fields/TextField.tsx +49 -0
- package/src/components/index.ts +30 -0
- package/src/components/layout/Breadcrumbs.tsx +36 -0
- package/src/components/layout/Header.tsx +37 -0
- package/src/components/layout/Layout.tsx +25 -0
- package/src/components/layout/Sidebar.tsx +462 -0
- package/src/components/ui/Badge.tsx +14 -0
- package/src/components/ui/Button.tsx +41 -0
- package/src/components/ui/Dropdown.tsx +82 -0
- package/src/components/ui/Modal.tsx +135 -0
- package/src/components/ui/SlidePanel.tsx +73 -0
- package/src/components/ui/Spinner.tsx +24 -0
- package/src/components/ui/Toast.tsx +78 -0
- package/src/layouts/AdminLayout.astro +197 -0
- package/src/lib/config.ts +68 -0
- package/src/lib/dataStore.ts +111 -0
- package/src/middleware.ts +48 -0
- package/src/pages/[collection]/[id].astro +176 -0
- package/src/pages/[collection]/index.astro +180 -0
- package/src/pages/api/[collection]/[id].ts +258 -0
- package/src/pages/api/[collection]/index.ts +289 -0
- package/src/pages/api/auth/[id].ts +142 -0
- package/src/pages/api/auth/audit-logs.ts +80 -0
- package/src/pages/api/auth/login.ts +101 -0
- package/src/pages/api/auth/logout.ts +48 -0
- package/src/pages/api/auth/me.ts +36 -0
- package/src/pages/api/auth/users.ts +150 -0
- package/src/pages/audit/index.astro +110 -0
- package/src/pages/index.astro +225 -0
- package/src/pages/roles/index.astro +114 -0
- package/src/pages/users/[id].astro +174 -0
- package/src/pages/users/index.astro +142 -0
- package/src/pages/users/new.astro +91 -0
- package/src/styles/main.css +1449 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DateField as DateFieldType } from '@kyro-cms/core';
|
|
2
|
+
|
|
3
|
+
interface DateFieldComponentProps {
|
|
4
|
+
field: DateFieldType;
|
|
5
|
+
value?: string;
|
|
6
|
+
onChange?: (value: string) => void;
|
|
7
|
+
error?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function DateField({ field, value = '', onChange, error, disabled }: DateFieldComponentProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-1">
|
|
14
|
+
{field.label && (
|
|
15
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
16
|
+
{field.label}
|
|
17
|
+
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
18
|
+
</label>
|
|
19
|
+
)}
|
|
20
|
+
<input
|
|
21
|
+
type={field.time ? 'datetime-local' : 'date'}
|
|
22
|
+
value={value}
|
|
23
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
24
|
+
disabled={disabled || field.admin?.readOnly}
|
|
25
|
+
min={field.minDate}
|
|
26
|
+
max={field.maxDate}
|
|
27
|
+
required={field.required}
|
|
28
|
+
className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
|
|
29
|
+
error
|
|
30
|
+
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
|
31
|
+
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
|
32
|
+
} ${disabled || field.admin?.readOnly ? 'bg-gray-50 text-gray-500' : 'bg-white'}`}
|
|
33
|
+
/>
|
|
34
|
+
{field.admin?.description && !error && (
|
|
35
|
+
<p className="text-xs text-gray-500">{field.admin.description}</p>
|
|
36
|
+
)}
|
|
37
|
+
{error && (
|
|
38
|
+
<p className="text-xs text-red-600">{error}</p>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { NumberField as NumberFieldType } from '@kyro-cms/core';
|
|
2
|
+
|
|
3
|
+
interface NumberFieldComponentProps {
|
|
4
|
+
field: NumberFieldType;
|
|
5
|
+
value?: number;
|
|
6
|
+
onChange?: (value: number) => void;
|
|
7
|
+
error?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function NumberField({ field, value, onChange, error, disabled }: NumberFieldComponentProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-1">
|
|
14
|
+
{field.label && (
|
|
15
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
16
|
+
{field.label}
|
|
17
|
+
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
18
|
+
</label>
|
|
19
|
+
)}
|
|
20
|
+
<input
|
|
21
|
+
type="number"
|
|
22
|
+
value={value ?? ''}
|
|
23
|
+
onChange={(e) => onChange?.(parseFloat(e.target.value) || 0)}
|
|
24
|
+
placeholder={field.admin?.placeholder}
|
|
25
|
+
disabled={disabled || field.admin?.readOnly}
|
|
26
|
+
min={field.min}
|
|
27
|
+
max={field.max}
|
|
28
|
+
step={field.step || (field.integer ? 1 : 'any')}
|
|
29
|
+
required={field.required}
|
|
30
|
+
className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
|
|
31
|
+
error
|
|
32
|
+
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
|
33
|
+
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
|
34
|
+
} ${disabled || field.admin?.readOnly ? 'bg-gray-50 text-gray-500' : 'bg-white'}`}
|
|
35
|
+
/>
|
|
36
|
+
{field.admin?.description && !error && (
|
|
37
|
+
<p className="text-xs text-gray-500">{field.admin.description}</p>
|
|
38
|
+
)}
|
|
39
|
+
{error && (
|
|
40
|
+
<p className="text-xs text-red-600">{error}</p>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { RelationshipField as RelationshipFieldType } from '@kyro-cms/core';
|
|
3
|
+
|
|
4
|
+
interface RelationshipFieldComponentProps {
|
|
5
|
+
field: RelationshipFieldType;
|
|
6
|
+
value?: string | string[];
|
|
7
|
+
onChange?: (value: string | string[]) => void;
|
|
8
|
+
error?: string;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function RelationshipField({ field, value, onChange, error, disabled }: RelationshipFieldComponentProps) {
|
|
13
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
14
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
15
|
+
|
|
16
|
+
const relationTo = Array.isArray(field.relationTo) ? field.relationTo : [field.relationTo];
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="space-y-1">
|
|
20
|
+
{field.label && (
|
|
21
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
22
|
+
{field.label}
|
|
23
|
+
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
24
|
+
</label>
|
|
25
|
+
)}
|
|
26
|
+
<div className="relative">
|
|
27
|
+
<input
|
|
28
|
+
type="text"
|
|
29
|
+
value={searchQuery}
|
|
30
|
+
onChange={(e) => {
|
|
31
|
+
setSearchQuery(e.target.value);
|
|
32
|
+
setIsOpen(true);
|
|
33
|
+
}}
|
|
34
|
+
onFocus={() => setIsOpen(true)}
|
|
35
|
+
onBlur={() => setTimeout(() => setIsOpen(false), 200)}
|
|
36
|
+
placeholder={`Search ${relationTo.join(' or ')}...`}
|
|
37
|
+
disabled={disabled || field.admin?.readOnly}
|
|
38
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:border-blue-500 focus:ring-blue-500"
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
{isOpen && (
|
|
42
|
+
<div className="absolute z-10 w-full mt-1 bg-white border border-gray-200 rounded-md shadow-lg max-h-60 overflow-auto">
|
|
43
|
+
<div className="p-2 text-sm text-gray-500 text-center">
|
|
44
|
+
Search results will appear here
|
|
45
|
+
</div>
|
|
46
|
+
{/* TODO: Implement actual search with API integration */}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
|
|
50
|
+
{/* Selected value display */}
|
|
51
|
+
{value && (
|
|
52
|
+
<div className="mt-2 flex flex-wrap gap-2">
|
|
53
|
+
{(Array.isArray(value) ? value : [value]).map((v, i) => (
|
|
54
|
+
<span
|
|
55
|
+
key={i}
|
|
56
|
+
className="inline-flex items-center gap-1 px-2 py-1 bg-blue-50 text-blue-700 text-sm rounded-md"
|
|
57
|
+
>
|
|
58
|
+
{v}
|
|
59
|
+
{!disabled && !field.admin?.readOnly && (
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
onClick={() => {
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
onChange?.(value.filter((_, idx) => idx !== i));
|
|
65
|
+
} else {
|
|
66
|
+
onChange?.('');
|
|
67
|
+
}
|
|
68
|
+
}}
|
|
69
|
+
className="ml-1 text-blue-500 hover:text-blue-700"
|
|
70
|
+
>
|
|
71
|
+
×
|
|
72
|
+
</button>
|
|
73
|
+
)}
|
|
74
|
+
</span>
|
|
75
|
+
))}
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
{field.admin?.description && !error && (
|
|
80
|
+
<p className="text-xs text-gray-500">{field.admin.description}</p>
|
|
81
|
+
)}
|
|
82
|
+
{error && (
|
|
83
|
+
<p className="text-xs text-red-600">{error}</p>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SelectField as SelectFieldType } from '@kyro-cms/core';
|
|
2
|
+
|
|
3
|
+
interface SelectFieldComponentProps {
|
|
4
|
+
field: SelectFieldType;
|
|
5
|
+
value?: string | string[];
|
|
6
|
+
onChange?: (value: string | string[]) => void;
|
|
7
|
+
error?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function SelectField({ field, value, onChange, error, disabled }: SelectFieldComponentProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-1">
|
|
14
|
+
{field.label && (
|
|
15
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
16
|
+
{field.label}
|
|
17
|
+
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
18
|
+
</label>
|
|
19
|
+
)}
|
|
20
|
+
<select
|
|
21
|
+
value={field.hasMany ? (Array.isArray(value) ? value.join(',') : '') : (value || '')}
|
|
22
|
+
onChange={(e) => {
|
|
23
|
+
if (field.hasMany) {
|
|
24
|
+
const selected = e.target.value ? e.target.value.split(',') : [];
|
|
25
|
+
onChange?.(selected);
|
|
26
|
+
} else {
|
|
27
|
+
onChange?.(e.target.value);
|
|
28
|
+
}
|
|
29
|
+
}}
|
|
30
|
+
multiple={field.hasMany}
|
|
31
|
+
disabled={disabled || field.admin?.readOnly}
|
|
32
|
+
required={field.required}
|
|
33
|
+
className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
|
|
34
|
+
error
|
|
35
|
+
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
|
36
|
+
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
|
37
|
+
} ${disabled || field.admin?.readOnly ? 'bg-gray-50 text-gray-500' : 'bg-white'}`}
|
|
38
|
+
>
|
|
39
|
+
{!field.required && (
|
|
40
|
+
<option value="">Select...</option>
|
|
41
|
+
)}
|
|
42
|
+
{field.options.map((option) => (
|
|
43
|
+
<option key={option.value} value={option.value}>
|
|
44
|
+
{option.label}
|
|
45
|
+
</option>
|
|
46
|
+
))}
|
|
47
|
+
</select>
|
|
48
|
+
{field.admin?.description && !error && (
|
|
49
|
+
<p className="text-xs text-gray-500">{field.admin.description}</p>
|
|
50
|
+
)}
|
|
51
|
+
{error && (
|
|
52
|
+
<p className="text-xs text-red-600">{error}</p>
|
|
53
|
+
)}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { TextField as TextFieldType } from '@kyro-cms/core';
|
|
2
|
+
|
|
3
|
+
interface TextFieldComponentProps {
|
|
4
|
+
field: TextFieldType;
|
|
5
|
+
value?: string;
|
|
6
|
+
onChange?: (value: string) => void;
|
|
7
|
+
error?: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function TextField({ field, value = '', onChange, error, disabled }: TextFieldComponentProps) {
|
|
12
|
+
const inputType = field.variant === 'email' ? 'email'
|
|
13
|
+
: field.variant === 'password' ? 'password'
|
|
14
|
+
: field.variant === 'url' ? 'url'
|
|
15
|
+
: 'text';
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="space-y-1">
|
|
19
|
+
{field.label && (
|
|
20
|
+
<label className="block text-sm font-medium text-gray-700">
|
|
21
|
+
{field.label}
|
|
22
|
+
{field.required && <span className="text-red-500 ml-1">*</span>}
|
|
23
|
+
</label>
|
|
24
|
+
)}
|
|
25
|
+
<input
|
|
26
|
+
type={inputType}
|
|
27
|
+
value={value}
|
|
28
|
+
onChange={(e) => onChange?.(e.target.value)}
|
|
29
|
+
placeholder={field.admin?.placeholder}
|
|
30
|
+
disabled={disabled || field.admin?.readOnly}
|
|
31
|
+
minLength={field.minLength}
|
|
32
|
+
maxLength={field.maxLength}
|
|
33
|
+
pattern={field.pattern}
|
|
34
|
+
required={field.required}
|
|
35
|
+
className={`w-full px-3 py-2 border rounded-md text-sm transition-colors ${
|
|
36
|
+
error
|
|
37
|
+
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
|
38
|
+
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500'
|
|
39
|
+
} ${disabled || field.admin?.readOnly ? 'bg-gray-50 text-gray-500' : 'bg-white'}`}
|
|
40
|
+
/>
|
|
41
|
+
{field.admin?.description && !error && (
|
|
42
|
+
<p className="text-xs text-gray-500">{field.admin.description}</p>
|
|
43
|
+
)}
|
|
44
|
+
{error && (
|
|
45
|
+
<p className="text-xs text-red-600">{error}</p>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { Admin } from "./Admin";
|
|
2
|
+
export { ListView } from "./ListView";
|
|
3
|
+
export { DetailView } from "./DetailView";
|
|
4
|
+
export { CreateView } from "./CreateView";
|
|
5
|
+
export { AutoForm } from "./AutoForm";
|
|
6
|
+
export {
|
|
7
|
+
ActionBar,
|
|
8
|
+
type ActionBarProps,
|
|
9
|
+
type DocumentStatus,
|
|
10
|
+
type SaveStatus,
|
|
11
|
+
} from "./ActionBar";
|
|
12
|
+
export { BulkActionsBar } from "./BulkActionsBar";
|
|
13
|
+
export { StatusBadge, CountBadge } from "./StatusBadge";
|
|
14
|
+
export { VersionHistoryPanel } from "./VersionHistoryPanel";
|
|
15
|
+
export {
|
|
16
|
+
ThemeProvider,
|
|
17
|
+
LightThemeProvider,
|
|
18
|
+
DarkThemeProvider,
|
|
19
|
+
useTheme,
|
|
20
|
+
type ThemeMode,
|
|
21
|
+
} from "./ThemeProvider";
|
|
22
|
+
export * from "./layout/Header";
|
|
23
|
+
export * from "./layout/Sidebar";
|
|
24
|
+
export * from "./ui/Button";
|
|
25
|
+
export * from "./ui/Badge";
|
|
26
|
+
export * from "./ui/Spinner";
|
|
27
|
+
export * from "./ui/Toast";
|
|
28
|
+
export { Dropdown, DropdownItem, DropdownSeparator } from "./ui/Dropdown";
|
|
29
|
+
export { Modal, ConfirmModal } from "./ui/Modal";
|
|
30
|
+
export { SlidePanel } from "./ui/SlidePanel";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface BreadcrumbItem {
|
|
2
|
+
label: string;
|
|
3
|
+
href?: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface BreadcrumbsProps {
|
|
7
|
+
items: BreadcrumbItem[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function Breadcrumbs({ items }: BreadcrumbsProps) {
|
|
11
|
+
return (
|
|
12
|
+
<nav className="mb-4" aria-label="Breadcrumb">
|
|
13
|
+
<ol className="flex items-center gap-2 text-sm">
|
|
14
|
+
{items.map((item, index) => (
|
|
15
|
+
<li key={index} className="flex items-center gap-2">
|
|
16
|
+
{index > 0 && (
|
|
17
|
+
<svg className="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
18
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
19
|
+
</svg>
|
|
20
|
+
)}
|
|
21
|
+
{item.href ? (
|
|
22
|
+
<a
|
|
23
|
+
href={item.href}
|
|
24
|
+
className="text-gray-500 hover:text-gray-700 transition-colors"
|
|
25
|
+
>
|
|
26
|
+
{item.label}
|
|
27
|
+
</a>
|
|
28
|
+
) : (
|
|
29
|
+
<span className="text-gray-900 font-medium">{item.label}</span>
|
|
30
|
+
)}
|
|
31
|
+
</li>
|
|
32
|
+
))}
|
|
33
|
+
</ol>
|
|
34
|
+
</nav>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
interface HeaderProps {
|
|
4
|
+
title: string;
|
|
5
|
+
onMenuClick: () => void;
|
|
6
|
+
actions?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Header({ title, onMenuClick, actions }: HeaderProps) {
|
|
10
|
+
return (
|
|
11
|
+
<header className="kyro-header">
|
|
12
|
+
<div className="kyro-header-left">
|
|
13
|
+
<button
|
|
14
|
+
className="kyro-header-menu"
|
|
15
|
+
onClick={onMenuClick}
|
|
16
|
+
aria-label="Toggle menu"
|
|
17
|
+
>
|
|
18
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
19
|
+
<path d="M3 12h18M3 6h18M3 18h18" />
|
|
20
|
+
</svg>
|
|
21
|
+
</button>
|
|
22
|
+
<h1 className="kyro-header-title">{title}</h1>
|
|
23
|
+
</div>
|
|
24
|
+
<div className="kyro-header-right">
|
|
25
|
+
{actions}
|
|
26
|
+
<div className="kyro-header-user">
|
|
27
|
+
<button className="kyro-header-user-btn">
|
|
28
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
29
|
+
<circle cx="12" cy="8" r="4" />
|
|
30
|
+
<path d="M4 20c0-4 4-6 8-6s8 2 8 6" />
|
|
31
|
+
</svg>
|
|
32
|
+
</button>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</header>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type CollectionConfig } from '@kyro-cms/core';
|
|
2
|
+
|
|
3
|
+
interface LayoutProps {
|
|
4
|
+
children: any;
|
|
5
|
+
collections?: CollectionConfig[];
|
|
6
|
+
currentSlug?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function Layout({ children, collections = [], currentSlug }: LayoutProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="min-h-screen bg-gray-50">
|
|
12
|
+
<div className="flex">
|
|
13
|
+
{/* Sidebar placeholder - will be populated by Sidebar component */}
|
|
14
|
+
<div id="sidebar-root" className="hidden lg:block">
|
|
15
|
+
{/* Sidebar rendered here */}
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
{/* Main content */}
|
|
19
|
+
<main className="flex-1 min-w-0">
|
|
20
|
+
{children}
|
|
21
|
+
</main>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
}
|