Skip to content

Commit 2b73404

Browse files
rafavallsguitavano
authored andcommitted
simplify form field internals after review
- IconField: drop import * as LucideIcons (full lib in bundle); use a typed ICON_MAP with explicit imports, and POPULAR_ICONS = Object.keys(ICON_MAP) - MarkdownField: lazy-load marked on first preview tab open instead of static import; show a brief "Loading preview…" while the chunk arrives - UrlField, MarkdownField: drop redundant `local` mirror state — these are not typing buffers; just bind input value to value/onChange directly - SwitchField: use shared FieldLabel (now accepts className) instead of hand-rolled label/description div - Drop two narrating comments from TimeField helpers
1 parent b2a9518 commit 2b73404

2 files changed

Lines changed: 163 additions & 105 deletions

File tree

web/tools/file-explorer/cms-form-extras.tsx

Lines changed: 160 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,71 @@
11
import { format as formatDate } from "date-fns";
2-
import * as LucideIcons from "lucide-react";
32
import {
3+
Award,
4+
Bell,
5+
Bike,
6+
Bookmark,
7+
Bus,
48
Calendar as CalendarIcon,
9+
Camera,
10+
Car,
511
Check,
612
ChevronDown,
713
Clock,
14+
Cloud,
15+
Coffee,
16+
Cookie,
17+
CreditCard,
18+
Download,
819
Eye,
20+
EyeOff,
921
FileText,
22+
Flag,
23+
Gift,
24+
Globe,
25+
Headphones,
26+
Heart,
27+
Home,
28+
Image,
1029
Link as LinkIcon,
1130
Lock,
1231
type LucideIcon,
32+
Mail,
33+
MapPin,
34+
MessageCircle,
35+
Mic,
36+
Minus,
37+
Moon,
38+
Music,
39+
Package,
40+
Phone,
41+
Pizza,
42+
Plane,
43+
Plus,
1344
Search,
45+
Send,
46+
Settings,
47+
Share,
48+
Shield,
49+
ShoppingBag,
50+
ShoppingCart,
51+
Smile,
52+
Star,
53+
Sun,
54+
Tag,
55+
ThumbsUp,
56+
Train,
57+
Trophy,
58+
Truck,
1459
Unlock,
60+
Upload,
61+
User,
62+
Users,
63+
Utensils,
64+
Video,
1565
X,
66+
Zap,
1667
} from "lucide-react";
17-
import { marked } from "marked";
18-
import { useEffect, useMemo, useState } from "react";
68+
import { useEffect, useState } from "react";
1969
import { Button } from "@/components/ui/button.tsx";
2070
import { Calendar } from "@/components/ui/calendar.tsx";
2171
import { Input } from "@/components/ui/input.tsx";
@@ -45,16 +95,7 @@ export function SwitchField({
4595
}) {
4696
return (
4797
<div className="flex items-start justify-between gap-3 rounded-lg border bg-muted/10 px-4 py-3">
48-
<div className="min-w-0 flex-1 space-y-0.5">
49-
<span className="block text-sm font-medium text-foreground">
50-
{label}
51-
</span>
52-
{description && (
53-
<span className="block text-xs leading-snug text-muted-foreground">
54-
{description}
55-
</span>
56-
)}
57-
</div>
98+
<FieldLabel label={label} description={description} className="flex-1" />
5899
<Switch
59100
checked={value}
60101
onCheckedChange={onChange}
@@ -313,17 +354,14 @@ export function UrlField({
313354
value: string;
314355
onChange: (v: string) => void;
315356
}) {
316-
const [local, setLocal] = useState(value);
317-
useEffect(() => setLocal(value), [value]);
318-
319357
let url: URL | null = null;
320358
try {
321-
url = local ? new URL(local) : null;
359+
url = value ? new URL(value) : null;
322360
} catch {
323361
url = null;
324362
}
325363
const isValid = Boolean(url);
326-
const isInternal = local.startsWith("/");
364+
const isInternal = value.startsWith("/");
327365
const favicon =
328366
url?.hostname && !isInternal
329367
? `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`
@@ -348,15 +386,12 @@ export function UrlField({
348386
)}
349387
</span>
350388
<input
351-
value={local}
352-
onChange={(e) => {
353-
setLocal(e.target.value);
354-
onChange(e.target.value);
355-
}}
389+
value={value}
390+
onChange={(e) => onChange(e.target.value)}
356391
placeholder="https://example.com or /internal-path"
357392
className="flex-1 bg-transparent px-3 text-sm outline-none placeholder:text-muted-foreground"
358393
/>
359-
{local && (
394+
{value && (
360395
<span
361396
className={cn(
362397
"mr-3 shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium",
@@ -377,6 +412,20 @@ export function UrlField({
377412

378413
// ─── 6. MarkdownField — split-view editor ────────────────────────────────────
379414

415+
// Marked is only needed when the preview tab is open. Lazy-load and cache.
416+
let markedParser: ((src: string) => string) | null = null;
417+
let markedLoading: Promise<void> | null = null;
418+
419+
function loadMarked(): Promise<void> {
420+
if (markedParser) return Promise.resolve();
421+
if (!markedLoading) {
422+
markedLoading = import("marked").then(({ marked }) => {
423+
markedParser = (s) => marked.parse(s, { async: false }) as string;
424+
});
425+
}
426+
return markedLoading;
427+
}
428+
380429
export function MarkdownField({
381430
label,
382431
description,
@@ -389,16 +438,27 @@ export function MarkdownField({
389438
onChange: (v: string) => void;
390439
}) {
391440
const [tab, setTab] = useState<"write" | "preview">("write");
392-
const [local, setLocal] = useState(value);
393-
useEffect(() => setLocal(value), [value]);
441+
const [, forceUpdate] = useState(0);
394442

395-
const html = useMemo(() => {
443+
useEffect(() => {
444+
if (tab !== "preview" || markedParser) return;
445+
let cancelled = false;
446+
loadMarked().then(() => {
447+
if (!cancelled) forceUpdate((n) => n + 1);
448+
});
449+
return () => {
450+
cancelled = true;
451+
};
452+
}, [tab]);
453+
454+
const html = (() => {
455+
if (!markedParser) return null;
396456
try {
397-
return marked.parse(local || "", { async: false }) as string;
457+
return markedParser(value || "");
398458
} catch {
399459
return "";
400460
}
401-
}, [local]);
461+
})();
402462

403463
return (
404464
<div className="space-y-2">
@@ -437,16 +497,17 @@ export function MarkdownField({
437497
</div>
438498
{tab === "write" ? (
439499
<Textarea
440-
value={local}
441-
onChange={(e) => {
442-
setLocal(e.target.value);
443-
onChange(e.target.value);
444-
}}
500+
value={value}
501+
onChange={(e) => onChange(e.target.value)}
445502
rows={10}
446503
spellCheck={false}
447504
placeholder="# Hello world&#10;&#10;Type some **markdown** here…"
448505
className="min-h-[unset] resize-y rounded-none border-0 text-xs leading-relaxed shadow-none focus-visible:ring-0"
449506
/>
507+
) : html === null ? (
508+
<div className="flex min-h-[200px] items-center justify-center text-xs text-muted-foreground">
509+
Loading preview…
510+
</div>
450511
) : (
451512
<div
452513
className="prose prose-sm min-h-[200px] max-w-none px-4 py-3 text-sm prose-headings:font-semibold prose-headings:mt-3 prose-headings:mb-1.5 prose-p:my-2 prose-ul:my-2 prose-ol:my-2 prose-a:text-primary prose-a:underline prose-code:rounded prose-code:bg-muted prose-code:px-1 prose-code:py-0.5 prose-code:text-xs"
@@ -784,7 +845,6 @@ function parseTime(value: string): { h: string; m: string } | null {
784845
}
785846

786847
function normalizeTimeInput(raw: string): string | null {
787-
// Accept "9", "9:3", "09:30", "1234" → "HH:MM" if valid; null otherwise.
788848
const cleaned = raw.replace(/[^\d:]/g, "");
789849
if (!cleaned) return null;
790850
let h: string;
@@ -842,7 +902,6 @@ export function TimeField({
842902
onChange(normalized);
843903
setDraft(normalized);
844904
} else {
845-
// invalid — revert to last good value
846905
setDraft(value);
847906
}
848907
};
@@ -970,68 +1029,69 @@ function TimeColumn({
9701029

9711030
// ─── 11. IconField — pick from Lucide library ───────────────────────────────
9721031

973-
const POPULAR_ICONS: string[] = [
974-
"Home",
975-
"Search",
976-
"User",
977-
"Users",
978-
"Settings",
979-
"Heart",
980-
"Star",
981-
"Bell",
982-
"Mail",
983-
"Phone",
984-
"MapPin",
985-
"Calendar",
986-
"Clock",
987-
"ShoppingCart",
988-
"ShoppingBag",
989-
"Package",
990-
"Truck",
991-
"CreditCard",
992-
"Tag",
993-
"Gift",
994-
"Zap",
995-
"Award",
996-
"Trophy",
997-
"Flag",
998-
"Bookmark",
999-
"Camera",
1000-
"Image",
1001-
"Video",
1002-
"Music",
1003-
"Headphones",
1004-
"Mic",
1005-
"Globe",
1006-
"Link",
1007-
"Share",
1008-
"Download",
1009-
"Upload",
1010-
"Cloud",
1011-
"Lock",
1012-
"Shield",
1013-
"Eye",
1014-
"EyeOff",
1015-
"Sun",
1016-
"Moon",
1017-
"Coffee",
1018-
"Cookie",
1019-
"Pizza",
1020-
"Utensils",
1021-
"Plane",
1022-
"Car",
1023-
"Bike",
1024-
"Train",
1025-
"Bus",
1026-
"Smile",
1027-
"ThumbsUp",
1028-
"MessageCircle",
1029-
"Send",
1030-
"Plus",
1031-
"Minus",
1032-
"Check",
1033-
"X",
1034-
];
1032+
const ICON_MAP: Record<string, LucideIcon> = {
1033+
Award,
1034+
Bell,
1035+
Bike,
1036+
Bookmark,
1037+
Bus,
1038+
Calendar: CalendarIcon,
1039+
Camera,
1040+
Car,
1041+
Check,
1042+
Clock,
1043+
Cloud,
1044+
Coffee,
1045+
Cookie,
1046+
CreditCard,
1047+
Download,
1048+
Eye,
1049+
EyeOff,
1050+
Flag,
1051+
Gift,
1052+
Globe,
1053+
Headphones,
1054+
Heart,
1055+
Home,
1056+
Image,
1057+
Link: LinkIcon,
1058+
Lock,
1059+
Mail,
1060+
MapPin,
1061+
MessageCircle,
1062+
Mic,
1063+
Minus,
1064+
Moon,
1065+
Music,
1066+
Package,
1067+
Phone,
1068+
Pizza,
1069+
Plane,
1070+
Plus,
1071+
Search,
1072+
Send,
1073+
Settings,
1074+
Share,
1075+
Shield,
1076+
ShoppingBag,
1077+
ShoppingCart,
1078+
Smile,
1079+
Star,
1080+
Sun,
1081+
Tag,
1082+
ThumbsUp,
1083+
Train,
1084+
Trophy,
1085+
Truck,
1086+
Upload,
1087+
User,
1088+
Users,
1089+
Utensils,
1090+
Video,
1091+
X,
1092+
Zap,
1093+
};
1094+
const POPULAR_ICONS = Object.keys(ICON_MAP);
10351095

10361096
export function IconField({
10371097
label,
@@ -1047,9 +1107,7 @@ export function IconField({
10471107
const [open, setOpen] = useState(false);
10481108
const [query, setQuery] = useState("");
10491109

1050-
const Selected = (LucideIcons as unknown as Record<string, LucideIcon>)[
1051-
value
1052-
];
1110+
const Selected = ICON_MAP[value];
10531111
const filtered = query
10541112
? POPULAR_ICONS.filter((n) => n.toLowerCase().includes(query.toLowerCase()))
10551113
: POPULAR_ICONS;
@@ -1093,9 +1151,7 @@ export function IconField({
10931151
</div>
10941152
<div className="grid max-h-64 grid-cols-7 gap-1 overflow-y-auto p-2">
10951153
{filtered.map((name) => {
1096-
const Icon = (
1097-
LucideIcons as unknown as Record<string, LucideIcon>
1098-
)[name];
1154+
const Icon = ICON_MAP[name];
10991155
if (!Icon) return null;
11001156
const active = value === name;
11011157
return (

web/tools/file-explorer/cms-form.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,13 +179,15 @@ function defaultForSchemaType(
179179
export function FieldLabel({
180180
label,
181181
description,
182+
className,
182183
}: {
183184
label: string;
184185
description?: string;
186+
className?: string;
185187
}) {
186188
if (!label) return null;
187189
return (
188-
<div className="min-w-0 space-y-1">
190+
<div className={cn("min-w-0 space-y-1", className)}>
189191
<span className="block truncate text-sm font-medium text-foreground">
190192
{label}
191193
</span>

0 commit comments

Comments
 (0)