@kyro-cms/admin 0.11.6 → 0.12.1
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/dist/index.cjs +25 -25
- package/dist/index.d.cts +4 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +20 -20
- package/package.json +1 -1
- package/src/components/ActionBar.tsx +35 -5
- package/src/components/AutoForm.tsx +97 -40
- package/src/components/DetailView.tsx +41 -0
- package/src/components/FieldRenderer.tsx +11 -0
- package/src/components/GraphQLPlayground.tsx +259 -12
- package/src/components/MediaGallery.tsx +65 -51
- package/src/components/WebhookManager.tsx +338 -188
- package/src/components/autoform/AutoFormHeader.tsx +19 -9
- package/src/components/autoform/ErrorBoundary.tsx +93 -0
- package/src/components/blocks/HeadingSubheadingBlock.tsx +2 -2
- package/src/components/blocks/HeroBlock.tsx +2 -2
- package/src/components/fields/BlocksField.tsx +69 -17
- package/src/components/fields/HeadingSubheadingField.tsx +2 -2
- package/src/components/fields/HeroField.tsx +4 -4
- package/src/components/fields/IconField.tsx +79 -0
- package/src/components/fields/RelationshipField.tsx +2 -2
- package/src/components/fields/UploadField.tsx +7 -5
- package/src/components/fields/extensions/blocksStore.ts +1 -1
- package/src/components/fields/index.ts +1 -0
- package/src/components/ui/BlockDrawer.tsx +39 -0
- package/src/components/ui/IconPickerModal.tsx +80 -0
- package/src/components/ui/ImageFocalEditor.tsx +112 -71
- package/src/components/ui/Modal.tsx +4 -2
- package/src/components/ui/icons.tsx +1 -1
- package/src/hooks/useAutoFormState.ts +51 -1
- package/src/integration.ts +4 -4
- package/src/pages/settings/[slug].astro +1 -1
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
Play,
|
|
24
24
|
Clock,
|
|
25
25
|
Terminal,
|
|
26
|
+
Search,
|
|
26
27
|
} from "./ui/icons";
|
|
27
28
|
|
|
28
29
|
function ChevronDown({ className }: { className?: string }) {
|
|
@@ -246,14 +247,42 @@ function generateSkeletonQuery(field: FieldInfo, schema: SchemaInfo, isMutation:
|
|
|
246
247
|
const returnTypeName = resolveTypeName(field.type);
|
|
247
248
|
const returnType = schema.types.find((t) => t.name === returnTypeName);
|
|
248
249
|
|
|
249
|
-
function
|
|
250
|
+
function buildTypeMap(): Map<string, TypeInfo> {
|
|
251
|
+
const m = new Map<string, TypeInfo>();
|
|
252
|
+
for (const t of schema.types) m.set(t.name, t);
|
|
253
|
+
return m;
|
|
254
|
+
}
|
|
255
|
+
const localTypeMap = buildTypeMap();
|
|
256
|
+
|
|
257
|
+
function collectFieldsWithUnions(fields: FieldInfo[], indent: number, _typeMap: Map<string, TypeInfo>): string[] {
|
|
250
258
|
const lines: string[] = [];
|
|
251
259
|
for (const f of fields) {
|
|
252
260
|
if (f.isDeprecated) continue;
|
|
253
261
|
const typeName = resolveTypeName(f.type);
|
|
262
|
+
const returnInfo = _typeMap.get(typeName);
|
|
254
263
|
const pad = " ".repeat(indent);
|
|
255
264
|
if (isScalarType(typeName) || typeName === "JSON") {
|
|
256
265
|
lines.push(`${pad}${f.name}`);
|
|
266
|
+
} else if (returnInfo && (returnInfo.kind === "UNION" || returnInfo.kind === "INTERFACE")) {
|
|
267
|
+
lines.push(`${pad}${f.name} {`);
|
|
268
|
+
lines.push(`${pad} __typename`);
|
|
269
|
+
for (const pt of returnInfo.possibleTypes || []) {
|
|
270
|
+
const ptInfo = _typeMap.get(pt.name);
|
|
271
|
+
if (!ptInfo?.fields) continue;
|
|
272
|
+
lines.push(`${pad} ... on ${pt.name} {`);
|
|
273
|
+
for (const pf of ptInfo.fields) {
|
|
274
|
+
if (pf.isDeprecated) continue;
|
|
275
|
+
const pfTypeName = resolveTypeName(pf.type);
|
|
276
|
+
const pfPad = " ".repeat(indent + 2);
|
|
277
|
+
if (isScalarType(pfTypeName) || pfTypeName === "JSON") {
|
|
278
|
+
lines.push(`${pfPad}${pf.name}`);
|
|
279
|
+
} else {
|
|
280
|
+
lines.push(`${pfPad}${pf.name} { id }`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
lines.push(`${pad} }`);
|
|
284
|
+
}
|
|
285
|
+
lines.push(`${pad}}`);
|
|
257
286
|
} else {
|
|
258
287
|
lines.push(`${pad}${f.name} { id }`);
|
|
259
288
|
}
|
|
@@ -269,12 +298,12 @@ function generateSkeletonQuery(field: FieldInfo, schema: SchemaInfo, isMutation:
|
|
|
269
298
|
if (docField && isMutation) {
|
|
270
299
|
const docTypeName = resolveTypeName(docField.type);
|
|
271
300
|
const docType = schema.types.find((t) => t.name === docTypeName);
|
|
272
|
-
const docLines =
|
|
301
|
+
const docLines = collectFieldsWithUnions(docType?.fields?.filter((f) => !f.isDeprecated) || [], 1, localTypeMap);
|
|
273
302
|
selection = [" doc {", ...docLines, " }"]
|
|
274
303
|
.concat(messageField ? [" message"] : [])
|
|
275
304
|
.join("\n");
|
|
276
305
|
} else {
|
|
277
|
-
selection =
|
|
306
|
+
selection = collectFieldsWithUnions(returnType?.fields || [], 1, localTypeMap).join("\n");
|
|
278
307
|
}
|
|
279
308
|
|
|
280
309
|
const args = field.args
|
|
@@ -358,6 +387,8 @@ interface TypeInfo {
|
|
|
358
387
|
fields?: FieldInfo[];
|
|
359
388
|
inputFields?: FieldInfo[];
|
|
360
389
|
enumValues?: { name: string; description?: string; isDeprecated: boolean }[];
|
|
390
|
+
possibleTypes?: { name: string; kind: string }[];
|
|
391
|
+
interfaces?: { name: string; kind: string }[];
|
|
361
392
|
isDeprecated?: boolean;
|
|
362
393
|
}
|
|
363
394
|
|
|
@@ -502,6 +533,8 @@ export function GraphQLPlayground({
|
|
|
502
533
|
description
|
|
503
534
|
isDeprecated
|
|
504
535
|
}
|
|
536
|
+
possibleTypes { name kind }
|
|
537
|
+
interfaces { name kind }
|
|
505
538
|
}
|
|
506
539
|
}
|
|
507
540
|
}
|
|
@@ -524,6 +557,93 @@ export function GraphQLPlayground({
|
|
|
524
557
|
if (showDocs && !schema) fetchSchema();
|
|
525
558
|
}, [showDocs, schema, fetchSchema]);
|
|
526
559
|
|
|
560
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
561
|
+
|
|
562
|
+
const typeMap = useMemo(() => {
|
|
563
|
+
const m = new Map<string, TypeInfo>();
|
|
564
|
+
if (!schema) return m;
|
|
565
|
+
for (const t of schema.types) {
|
|
566
|
+
m.set(t.name, t);
|
|
567
|
+
}
|
|
568
|
+
return m;
|
|
569
|
+
}, [schema]);
|
|
570
|
+
|
|
571
|
+
interface SearchResult {
|
|
572
|
+
typeName: string;
|
|
573
|
+
fieldPath: string;
|
|
574
|
+
typeInfo: TypeInfo;
|
|
575
|
+
fieldInfo?: FieldInfo;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const searchResults = useMemo(() => {
|
|
579
|
+
if (!searchQuery.trim() || !schema) return [];
|
|
580
|
+
const q = searchQuery.toLowerCase();
|
|
581
|
+
const results: SearchResult[] = [];
|
|
582
|
+
const seenLabels = new Set<string>();
|
|
583
|
+
|
|
584
|
+
for (const type of schema.types) {
|
|
585
|
+
if (type.name.startsWith("__")) continue;
|
|
586
|
+
const typeMatch = type.name.toLowerCase().includes(q);
|
|
587
|
+
|
|
588
|
+
if (typeMatch && !seenLabels.has(type.name)) {
|
|
589
|
+
seenLabels.add(type.name);
|
|
590
|
+
results.push({ typeName: type.name, fieldPath: type.name, typeInfo: type });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
if (type.fields) {
|
|
594
|
+
for (const field of type.fields) {
|
|
595
|
+
const fieldMatch = field.name.toLowerCase().includes(q);
|
|
596
|
+
if (fieldMatch) {
|
|
597
|
+
const label = `${type.name} > ${field.name}`;
|
|
598
|
+
if (!seenLabels.has(label)) {
|
|
599
|
+
seenLabels.add(label);
|
|
600
|
+
results.push({ typeName: type.name, fieldPath: label, typeInfo: type, fieldInfo: field });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if (q.length >= 2) {
|
|
605
|
+
const rtName = resolveTypeName(field.type);
|
|
606
|
+
const rt = typeMap.get(rtName);
|
|
607
|
+
if (rt?.fields && (fieldMatch || results.some(r => r.fieldInfo === field))) {
|
|
608
|
+
for (const sub of rt.fields) {
|
|
609
|
+
if (sub.name.toLowerCase().includes(q)) {
|
|
610
|
+
const label = `${type.name} > ${field.name} > ${sub.name}`;
|
|
611
|
+
if (!seenLabels.has(label)) {
|
|
612
|
+
seenLabels.add(label);
|
|
613
|
+
results.push({ typeName: type.name, fieldPath: label, typeInfo: type, fieldInfo: field });
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return results;
|
|
624
|
+
}, [searchQuery, schema, typeMap]);
|
|
625
|
+
|
|
626
|
+
const handleSearchSelect = useCallback((result: SearchResult) => {
|
|
627
|
+
const targetType = result.fieldInfo
|
|
628
|
+
? typeMap.get(resolveTypeName(result.fieldInfo.type)) || result.typeInfo
|
|
629
|
+
: result.typeInfo;
|
|
630
|
+
setSelectedType(targetType);
|
|
631
|
+
setSearchQuery("");
|
|
632
|
+
}, [typeMap]);
|
|
633
|
+
|
|
634
|
+
function highlightMatch(text: string, query: string) {
|
|
635
|
+
if (!query.trim()) return text;
|
|
636
|
+
const idx = text.toLowerCase().indexOf(query.toLowerCase());
|
|
637
|
+
if (idx === -1) return text;
|
|
638
|
+
return (
|
|
639
|
+
<>
|
|
640
|
+
{text.slice(0, idx)}
|
|
641
|
+
<mark className="bg-[var(--kyro-primary)]/20 text-[var(--kyro-primary)] rounded px-0.5">{text.slice(idx, idx + query.length)}</mark>
|
|
642
|
+
{text.slice(idx + query.length)}
|
|
643
|
+
</>
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
527
647
|
const handleRun = useCallback(async () => {
|
|
528
648
|
setIsLoading(true);
|
|
529
649
|
setError(null);
|
|
@@ -850,9 +970,23 @@ export function GraphQLPlayground({
|
|
|
850
970
|
<div className="flex-1 overflow-hidden">
|
|
851
971
|
{rightTab === "docs" ? (
|
|
852
972
|
<div className="h-full flex flex-col overflow-hidden bg-[var(--kyro-surface)]">
|
|
853
|
-
<div className="flex items-center
|
|
854
|
-
<
|
|
855
|
-
|
|
973
|
+
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-[var(--kyro-border)]">
|
|
974
|
+
<div className="relative flex-1">
|
|
975
|
+
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-3 h-3 text-[var(--kyro-text-muted)]" />
|
|
976
|
+
<input
|
|
977
|
+
type="text"
|
|
978
|
+
value={searchQuery}
|
|
979
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
980
|
+
placeholder="Search types and fields..."
|
|
981
|
+
className="w-full pl-7 pr-6 py-1 text-[10px] bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-md text-[var(--kyro-text-primary)] placeholder:text-[var(--kyro-text-muted)] focus:outline-none focus:border-[var(--kyro-primary)]"
|
|
982
|
+
/>
|
|
983
|
+
{searchQuery && (
|
|
984
|
+
<button onClick={() => setSearchQuery("")} className="absolute right-1.5 top-1/2 -translate-y-1/2 text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)]">
|
|
985
|
+
<X className="w-3 h-3" />
|
|
986
|
+
</button>
|
|
987
|
+
)}
|
|
988
|
+
</div>
|
|
989
|
+
<button onClick={() => setShowDocs(false)} className="text-[var(--kyro-text-muted)] hover:text-[var(--kyro-text-primary)] shrink-0">
|
|
856
990
|
<X className="w-3.5 h-3.5" />
|
|
857
991
|
</button>
|
|
858
992
|
</div>
|
|
@@ -863,7 +997,39 @@ export function GraphQLPlayground({
|
|
|
863
997
|
</div>
|
|
864
998
|
) : schema ? (
|
|
865
999
|
<div className="space-y-4">
|
|
866
|
-
{
|
|
1000
|
+
{searchQuery ? (
|
|
1001
|
+
<div className="space-y-1">
|
|
1002
|
+
{searchResults.length === 0 ? (
|
|
1003
|
+
<p className="text-[10px] text-[var(--kyro-text-muted)] py-4 text-center">No results for "{searchQuery}"</p>
|
|
1004
|
+
) : (
|
|
1005
|
+
searchResults.map((r, i) => {
|
|
1006
|
+
const targetType = r.fieldInfo ? typeMap.get(resolveTypeName(r.fieldInfo.type)) || r.typeInfo : r.typeInfo;
|
|
1007
|
+
return (
|
|
1008
|
+
<button
|
|
1009
|
+
key={r.fieldPath + i}
|
|
1010
|
+
onClick={() => handleSearchSelect(r)}
|
|
1011
|
+
className="w-full flex items-center justify-between px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left group"
|
|
1012
|
+
>
|
|
1013
|
+
<span className="text-[10px] font-mono text-[var(--kyro-text-muted)]">
|
|
1014
|
+
{r.fieldPath.split(" > ").map((part, j) => (
|
|
1015
|
+
<span key={j}>
|
|
1016
|
+
{j > 0 && <span className="mx-1 opacity-40">›</span>}
|
|
1017
|
+
<span className={j === r.fieldPath.split(" > ").length - 1 ? "text-[var(--kyro-text-primary)] font-semibold" : "text-[var(--kyro-text-muted)]"}>
|
|
1018
|
+
{highlightMatch(part, searchQuery)}
|
|
1019
|
+
</span>
|
|
1020
|
+
</span>
|
|
1021
|
+
))}
|
|
1022
|
+
{targetType && (targetType.kind === "UNION" || targetType.kind === "INTERFACE") && (
|
|
1023
|
+
<span className="ml-2 text-[9px] font-mono text-[var(--kyro-primary)] bg-[var(--kyro-primary)]/10 px-1 py-0.5 rounded">⧉</span>
|
|
1024
|
+
)}
|
|
1025
|
+
</span>
|
|
1026
|
+
<ChevronRight className="w-3 h-3 opacity-30 group-hover:opacity-60" />
|
|
1027
|
+
</button>
|
|
1028
|
+
);
|
|
1029
|
+
})
|
|
1030
|
+
)}
|
|
1031
|
+
</div>
|
|
1032
|
+
) : selectedType ? (
|
|
867
1033
|
<div className="space-y-3">
|
|
868
1034
|
<button
|
|
869
1035
|
onClick={() => setSelectedType(null)}
|
|
@@ -878,6 +1044,46 @@ export function GraphQLPlayground({
|
|
|
878
1044
|
<p className="mt-1.5 text-[11px] text-[var(--kyro-text-secondary)] leading-relaxed">{selectedType.description}</p>
|
|
879
1045
|
)}
|
|
880
1046
|
</div>
|
|
1047
|
+
{selectedType.kind === "UNION" && selectedType.possibleTypes && (
|
|
1048
|
+
<div className="space-y-2">
|
|
1049
|
+
<h4 className="text-[10px] font-semibold tracking-wider text-[var(--kyro-text-muted)] pt-3">Possible Types <span className="text-[8px] font-mono text-[var(--kyro-primary)] bg-[var(--kyro-primary)]/10 px-1 py-0.5 rounded ml-1">⧉ fragment</span></h4>
|
|
1050
|
+
<div className="space-y-1">
|
|
1051
|
+
{selectedType.possibleTypes.map(pt => (
|
|
1052
|
+
<button
|
|
1053
|
+
key={pt.name}
|
|
1054
|
+
onClick={() => {
|
|
1055
|
+
const t = typeMap.get(pt.name);
|
|
1056
|
+
if (t) setSelectedType(t);
|
|
1057
|
+
}}
|
|
1058
|
+
className="w-full flex items-center justify-between px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left group"
|
|
1059
|
+
>
|
|
1060
|
+
<span className="text-[11px] font-medium text-[var(--kyro-text-primary)]">{pt.name}</span>
|
|
1061
|
+
<ChevronRight className="w-3 h-3 opacity-30 group-hover:opacity-60" />
|
|
1062
|
+
</button>
|
|
1063
|
+
))}
|
|
1064
|
+
</div>
|
|
1065
|
+
</div>
|
|
1066
|
+
)}
|
|
1067
|
+
{selectedType.kind === "INTERFACE" && selectedType.possibleTypes && (
|
|
1068
|
+
<div className="space-y-2">
|
|
1069
|
+
<h4 className="text-[10px] font-semibold tracking-wider text-[var(--kyro-text-muted)] pt-3">Implementing Types <span className="text-[8px] font-mono text-[var(--kyro-text-muted)] bg-[var(--kyro-surface)] px-1 py-0.5 rounded border border-[var(--kyro-border)] ml-1">interface</span></h4>
|
|
1070
|
+
<div className="space-y-1">
|
|
1071
|
+
{selectedType.possibleTypes.map(pt => (
|
|
1072
|
+
<button
|
|
1073
|
+
key={pt.name}
|
|
1074
|
+
onClick={() => {
|
|
1075
|
+
const t = typeMap.get(pt.name);
|
|
1076
|
+
if (t) setSelectedType(t);
|
|
1077
|
+
}}
|
|
1078
|
+
className="w-full flex items-center justify-between px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left group"
|
|
1079
|
+
>
|
|
1080
|
+
<span className="text-[11px] font-medium text-[var(--kyro-text-primary)]">{pt.name}</span>
|
|
1081
|
+
<ChevronRight className="w-3 h-3 opacity-30 group-hover:opacity-60" />
|
|
1082
|
+
</button>
|
|
1083
|
+
))}
|
|
1084
|
+
</div>
|
|
1085
|
+
</div>
|
|
1086
|
+
)}
|
|
881
1087
|
{selectedType.fields && (
|
|
882
1088
|
<div className="space-y-2">
|
|
883
1089
|
<h4 className="text-[10px] font-semibold tracking-wider text-[var(--kyro-text-muted)] pt-3">Fields</h4>
|
|
@@ -892,9 +1098,46 @@ export function GraphQLPlayground({
|
|
|
892
1098
|
>
|
|
893
1099
|
<div className="flex items-center justify-between gap-2">
|
|
894
1100
|
<span className="font-semibold text-[11px] text-[var(--kyro-text-primary)]">{f.name}</span>
|
|
895
|
-
<span className="
|
|
1101
|
+
<span className="flex items-center gap-1">
|
|
1102
|
+
{(() => {
|
|
1103
|
+
const rtName = resolveTypeName(f.type);
|
|
1104
|
+
const rt = typeMap.get(rtName);
|
|
1105
|
+
const isUnionish = rt && (rt.kind === "UNION" || rt.kind === "INTERFACE");
|
|
1106
|
+
return isUnionish ? (
|
|
1107
|
+
<span className="text-[9px] font-mono text-[var(--kyro-primary)] bg-[var(--kyro-primary)]/10 px-1.5 py-0.5 rounded flex items-center gap-1">
|
|
1108
|
+
⧉
|
|
1109
|
+
<span className="text-[8px] opacity-70">fragment</span>
|
|
1110
|
+
</span>
|
|
1111
|
+
) : null;
|
|
1112
|
+
})()}
|
|
1113
|
+
<span className="text-[9px] font-mono text-[var(--kyro-primary)] bg-[var(--kyro-primary)]/10 px-1.5 py-0.5 rounded">{renderType(f.type)}</span>
|
|
1114
|
+
</span>
|
|
896
1115
|
</div>
|
|
897
1116
|
{f.description && <p className="text-[10px] text-[var(--kyro-text-secondary)] mt-1">{f.description}</p>}
|
|
1117
|
+
{(() => {
|
|
1118
|
+
const rtName = resolveTypeName(f.type);
|
|
1119
|
+
const rt = typeMap.get(rtName);
|
|
1120
|
+
if (rt && (rt.kind === "UNION" || rt.kind === "INTERFACE") && rt.possibleTypes?.length) {
|
|
1121
|
+
return (
|
|
1122
|
+
<div className="mt-1.5 pl-3 border-l-2 border-[var(--kyro-primary)]/30 space-y-0.5">
|
|
1123
|
+
<span className="text-[8px] font-semibold text-[var(--kyro-text-muted)] uppercase tracking-wider">Possible Types</span>
|
|
1124
|
+
<div className="flex flex-wrap gap-1 mt-0.5">
|
|
1125
|
+
{rt.possibleTypes.map(pt => {
|
|
1126
|
+
const ptInfo = typeMap.get(pt.name);
|
|
1127
|
+
const hasFragment = ptInfo && (ptInfo.kind === "OBJECT" || ptInfo.kind === "INTERFACE");
|
|
1128
|
+
return (
|
|
1129
|
+
<span key={pt.name} className="text-[9px] font-mono text-[var(--kyro-text-muted)] bg-[var(--kyro-surface)] px-1.5 py-0.5 rounded border border-[var(--kyro-border)]">
|
|
1130
|
+
{pt.name}
|
|
1131
|
+
{hasFragment && <span className="ml-1 text-[8px] text-[var(--kyro-primary)]">⧉</span>}
|
|
1132
|
+
</span>
|
|
1133
|
+
);
|
|
1134
|
+
})}
|
|
1135
|
+
</div>
|
|
1136
|
+
</div>
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
return null;
|
|
1140
|
+
})()}
|
|
898
1141
|
{f.args && f.args.length > 0 && (
|
|
899
1142
|
<div className="mt-1.5 pl-3 border-l-2 border-[var(--kyro-border)] space-y-0.5">
|
|
900
1143
|
{f.args.map(a => (
|
|
@@ -934,14 +1177,18 @@ export function GraphQLPlayground({
|
|
|
934
1177
|
</div>
|
|
935
1178
|
<div className="space-y-1.5">
|
|
936
1179
|
<h4 className="text-[10px] font-semibold tracking-wider text-[var(--kyro-text-muted)] pt-3">All Types</h4>
|
|
937
|
-
{schema.types.filter(t => !t.name.startsWith("__") && t.kind === "OBJECT").map(t => (
|
|
1180
|
+
{schema.types.filter(t => !t.name.startsWith("__") && (t.kind === "OBJECT" || t.kind === "UNION" || t.kind === "INTERFACE")).map(t => (
|
|
938
1181
|
<button
|
|
939
1182
|
key={t.name}
|
|
940
1183
|
onClick={() => setSelectedType(t)}
|
|
941
|
-
className="w-full flex items-center justify-between px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left"
|
|
1184
|
+
className="w-full flex items-center justify-between px-3 py-1.5 hover:bg-[var(--kyro-surface-accent)] rounded-lg transition-all text-left group"
|
|
942
1185
|
>
|
|
943
|
-
<
|
|
944
|
-
|
|
1186
|
+
<div className="flex items-center gap-2">
|
|
1187
|
+
<span className="text-[11px] font-medium text-[var(--kyro-text-primary)]">{t.name}</span>
|
|
1188
|
+
{t.kind === "UNION" && <span className="text-[8px] font-mono text-[var(--kyro-primary)] bg-[var(--kyro-primary)]/10 px-1 py-0.5 rounded">union</span>}
|
|
1189
|
+
{t.kind === "INTERFACE" && <span className="text-[8px] font-mono text-[var(--kyro-text-muted)] bg-[var(--kyro-surface)] px-1 py-0.5 rounded border border-[var(--kyro-border)]">interface</span>}
|
|
1190
|
+
</div>
|
|
1191
|
+
<ChevronRight className="w-3 h-3 opacity-30 group-hover:opacity-60" />
|
|
945
1192
|
</button>
|
|
946
1193
|
))}
|
|
947
1194
|
</div>
|
|
@@ -70,6 +70,23 @@ function getAbsoluteUrl(relativeUrl: unknown): string {
|
|
|
70
70
|
return `${window.location.origin}${sanitized}`;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
function getCroppedUrl(item: MediaItem, width?: number): string | null {
|
|
74
|
+
if (!item.metadata?.crop) return null;
|
|
75
|
+
const { x, y, width: cw, height: ch } = item.metadata.crop;
|
|
76
|
+
if (!cw || !ch) return null;
|
|
77
|
+
const base = getAbsoluteUrl(item.url);
|
|
78
|
+
if (!base) return null;
|
|
79
|
+
const params = new URLSearchParams({ url: item.url });
|
|
80
|
+
params.set("cx", String(x));
|
|
81
|
+
params.set("cy", String(y));
|
|
82
|
+
params.set("cw", String(cw));
|
|
83
|
+
params.set("ch", String(ch));
|
|
84
|
+
if (width) params.set("w", String(width));
|
|
85
|
+
const resizePath = `/api/media/resize?${params.toString()}`;
|
|
86
|
+
if (typeof window === "undefined") return resizePath;
|
|
87
|
+
return `${window.location.origin}${resizePath}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
73
90
|
type FilterType =
|
|
74
91
|
| "all"
|
|
75
92
|
| "image"
|
|
@@ -383,12 +400,12 @@ export function MediaGallery({
|
|
|
383
400
|
crop: cropData,
|
|
384
401
|
hotspot: hotspotData
|
|
385
402
|
};
|
|
386
|
-
|
|
403
|
+
|
|
387
404
|
const result = await apiPatch(`/api/media/${panelItem.id}`, { metadata });
|
|
388
405
|
setItems(prev => prev.map(item => item.id === panelItem.id ? result.doc : item));
|
|
389
406
|
setPanelItem(result.doc);
|
|
390
407
|
setShowCrop(false);
|
|
391
|
-
toast.success("
|
|
408
|
+
toast.success("Crop & hotspot saved");
|
|
392
409
|
} catch (err) {
|
|
393
410
|
console.error("Save failed:", err);
|
|
394
411
|
toast.error("Failed to save focal metadata");
|
|
@@ -417,71 +434,68 @@ export function MediaGallery({
|
|
|
417
434
|
})}
|
|
418
435
|
>
|
|
419
436
|
{/* Top Bar */}
|
|
420
|
-
<div className={`flex flex-col
|
|
437
|
+
<div className={`flex flex-col ${pickerMode ? "gap-2 p-2" : "gap-4 md:gap-6 p-4 md:p-6 rounded-md md:rounded-t-xl surface-tile"} border-b border-[var(--kyro-border)] backdrop-blur-md sticky top-0 z-10`}>
|
|
421
438
|
{!pickerMode && (
|
|
422
|
-
<div className="flex items-center
|
|
439
|
+
<div className="flex items-center justify-between w-full">
|
|
423
440
|
<div>
|
|
424
|
-
<h2 className="text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
|
|
441
|
+
<h2 className="text-lg md:text-xl font-bold tracking-tighter text-[var(--kyro-text-primary)]">
|
|
425
442
|
Media Library
|
|
426
443
|
</h2>
|
|
427
|
-
<div className="flex items-center gap-
|
|
444
|
+
<div className="flex items-center gap-2 mt-0.5">
|
|
428
445
|
<span className="text-[10px] font-bold tracking-widest text-[var(--kyro-text-secondary)] opacity-50">
|
|
429
|
-
{total} Items
|
|
446
|
+
{total} Items <span className="hidden sm:inline">· {formatFileSize(stats.totalSize)}</span>
|
|
430
447
|
</span>
|
|
431
448
|
</div>
|
|
432
449
|
</div>
|
|
433
|
-
</div>
|
|
434
|
-
)}
|
|
435
|
-
|
|
436
|
-
<div className={`flex items-center gap-3 flex-wrap lg:flex-nowrap ${pickerMode ? "w-full" : ""}`}>
|
|
437
|
-
<div className="relative group flex-1 min-w-[200px]">
|
|
438
|
-
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
439
|
-
<input
|
|
440
|
-
type="text"
|
|
441
|
-
placeholder="Search assets..."
|
|
442
|
-
value={search}
|
|
443
|
-
onChange={(e) => setSearch(e.target.value)}
|
|
444
|
-
className="w-full pl-11 pr-4 py-2.5 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] transition-all text-xs font-bold"
|
|
445
|
-
/>
|
|
446
|
-
</div>
|
|
447
|
-
|
|
448
|
-
{!pickerMode && (
|
|
449
|
-
<>
|
|
450
|
-
<div className="flex items-center gap-2">
|
|
451
|
-
<div className="flex bg-[var(--kyro-surface-accent)] p-1 rounded-xl border border-[var(--kyro-border)]">
|
|
452
|
-
<button
|
|
453
|
-
onClick={() => setView("grid")}
|
|
454
|
-
className={`p-2 rounded-lg transition-all ${view === "grid" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
|
|
455
|
-
>
|
|
456
|
-
<Grid className="w-4 h-4" />
|
|
457
|
-
</button>
|
|
458
|
-
<button
|
|
459
|
-
onClick={() => setView("list")}
|
|
460
|
-
className={`p-2 rounded-lg transition-all ${view === "list" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
|
|
461
|
-
>
|
|
462
|
-
<FileIcon className="w-4 h-4" />
|
|
463
|
-
</button>
|
|
464
|
-
</div>
|
|
465
450
|
|
|
451
|
+
<div className="flex items-center gap-2">
|
|
452
|
+
<div className="flex bg-[var(--kyro-surface-accent)] p-1 rounded-xl border border-[var(--kyro-border)] hidden sm:flex">
|
|
466
453
|
<button
|
|
467
|
-
onClick={() =>
|
|
468
|
-
className=
|
|
454
|
+
onClick={() => setView("grid")}
|
|
455
|
+
className={`p-1.5 md:p-2 rounded-lg transition-all ${view === "grid" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
|
|
469
456
|
>
|
|
470
|
-
<
|
|
457
|
+
<Grid className="w-4 h-4" />
|
|
458
|
+
</button>
|
|
459
|
+
<button
|
|
460
|
+
onClick={() => setView("list")}
|
|
461
|
+
className={`p-1.5 md:p-2 rounded-lg transition-all ${view === "list" ? "bg-[var(--kyro-surface)] shadow-sm text-[var(--kyro-text-primary)]" : "text-[var(--kyro-text-secondary)] opacity-50 hover:opacity-100"}`}
|
|
462
|
+
>
|
|
463
|
+
<FileIcon className="w-4 h-4" />
|
|
471
464
|
</button>
|
|
472
465
|
</div>
|
|
473
466
|
|
|
467
|
+
<button
|
|
468
|
+
onClick={() => setShowMobileFilters(true)}
|
|
469
|
+
className="md:hidden p-2 rounded-xl bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] transition-colors"
|
|
470
|
+
>
|
|
471
|
+
<Filter className="w-4 h-4" />
|
|
472
|
+
</button>
|
|
473
|
+
|
|
474
474
|
{canUpload && (
|
|
475
475
|
<button
|
|
476
476
|
onClick={() => fileInputRef.current?.click()}
|
|
477
|
-
className="flex items-center gap-2 px-6 py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-xs shadow-lg active:scale-95 transition-all"
|
|
477
|
+
className="flex items-center justify-center gap-2 px-3 md:px-6 py-2 md:py-2.5 bg-[var(--kyro-sidebar-active)] text-[var(--kyro-sidebar-text-active)] rounded-xl font-bold text-xs shadow-lg active:scale-95 transition-all"
|
|
478
478
|
>
|
|
479
|
-
<
|
|
480
|
-
|
|
479
|
+
<span className="md:hidden text-lg leading-none">+</span>
|
|
480
|
+
<Maximize2 className="w-4 h-4 hidden md:block" />
|
|
481
|
+
<span className="hidden md:inline">Upload</span>
|
|
481
482
|
</button>
|
|
482
483
|
)}
|
|
483
|
-
|
|
484
|
-
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
)}
|
|
487
|
+
|
|
488
|
+
<div className={`flex items-center w-full`}>
|
|
489
|
+
<div className="relative group flex-1">
|
|
490
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-[var(--kyro-text-muted)]" />
|
|
491
|
+
<input
|
|
492
|
+
type="text"
|
|
493
|
+
placeholder="Search assets..."
|
|
494
|
+
value={search}
|
|
495
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
496
|
+
className="w-full pl-10 pr-4 py-2.5 bg-[var(--kyro-surface-accent)] border border-[var(--kyro-border)] rounded-xl focus:outline-none focus:ring-2 focus:ring-[var(--kyro-sidebar-active)] transition-all text-xs font-bold"
|
|
497
|
+
/>
|
|
498
|
+
</div>
|
|
485
499
|
</div>
|
|
486
500
|
</div>
|
|
487
501
|
|
|
@@ -610,7 +624,7 @@ export function MediaGallery({
|
|
|
610
624
|
>
|
|
611
625
|
{item.type === "image" ? (
|
|
612
626
|
<img
|
|
613
|
-
src={item.url}
|
|
627
|
+
src={getCroppedUrl(item, 400) || item.url}
|
|
614
628
|
alt={item.title}
|
|
615
629
|
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110"
|
|
616
630
|
loading="lazy"
|
|
@@ -707,7 +721,7 @@ export function MediaGallery({
|
|
|
707
721
|
<div className="w-12 h-12 rounded-xl bg-[var(--kyro-bg)] overflow-hidden border border-[var(--kyro-border)] flex-shrink-0 flex items-center justify-center">
|
|
708
722
|
{item.type === "image" ? (
|
|
709
723
|
<img
|
|
710
|
-
src={item.url}
|
|
724
|
+
src={getCroppedUrl(item, 96) || item.url}
|
|
711
725
|
alt=""
|
|
712
726
|
className="w-full h-full object-cover"
|
|
713
727
|
/>
|
|
@@ -962,7 +976,7 @@ export function MediaGallery({
|
|
|
962
976
|
<div className="aspect-video w-full rounded-2xl bg-[var(--kyro-bg)] border border-[var(--kyro-border)] overflow-hidden relative group">
|
|
963
977
|
{panelItem.type === "image" ? (
|
|
964
978
|
<img
|
|
965
|
-
src={getAbsoluteUrl(panelItem.url)}
|
|
979
|
+
src={getCroppedUrl(panelItem) || getAbsoluteUrl(panelItem.url)}
|
|
966
980
|
alt=""
|
|
967
981
|
className="w-full h-full object-contain p-4"
|
|
968
982
|
/>
|
|
@@ -1152,7 +1166,7 @@ export function MediaGallery({
|
|
|
1152
1166
|
<div className="flex-1 w-full flex items-center justify-center p-12">
|
|
1153
1167
|
{panelItem.type === "image" ? (
|
|
1154
1168
|
<img
|
|
1155
|
-
src={getAbsoluteUrl(panelItem.url)}
|
|
1169
|
+
src={getCroppedUrl(panelItem) || getAbsoluteUrl(panelItem.url)}
|
|
1156
1170
|
alt=""
|
|
1157
1171
|
className="max-h-full max-w-full object-contain shadow-2xl rounded-lg animate-in zoom-in-95 duration-500"
|
|
1158
1172
|
/>
|