@flaredev/cli 0.1.2 → 0.3.0

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 (89) hide show
  1. package/bin/flare.js +9 -1
  2. package/dist/index.js +985 -337
  3. package/package.json +3 -2
  4. package/templates/app/app/admin/page.tsx +9 -61
  5. package/templates/app/app/api/openapi.json/route.ts +23 -0
  6. package/templates/app/app/api/reference/route.ts +36 -0
  7. package/templates/app/app/dashboard/account/page.tsx +40 -0
  8. package/templates/app/app/{admin → dashboard}/actions.ts +109 -11
  9. package/templates/app/app/dashboard/import-actions.ts +91 -0
  10. package/templates/app/app/dashboard/layout.tsx +42 -0
  11. package/templates/app/app/dashboard/observability/page.tsx +174 -0
  12. package/templates/app/app/dashboard/page.tsx +123 -9
  13. package/templates/app/app/forgot-password/page.tsx +22 -0
  14. package/templates/app/app/globals.css +109 -4
  15. package/templates/app/app/layout.tsx +28 -26
  16. package/templates/app/app/page.tsx +126 -13
  17. package/templates/app/app/reset-password/page.tsx +31 -0
  18. package/templates/app/app/sign-in/page.tsx +49 -33
  19. package/templates/app/app/sign-up/page.tsx +42 -23
  20. package/templates/app/app/two-factor/page.tsx +25 -0
  21. package/templates/app/app/verify-email/page.tsx +33 -0
  22. package/templates/app/components/account/security-settings.tsx +522 -0
  23. package/templates/app/components/auth/auth-shell.tsx +284 -0
  24. package/templates/app/components/auth/otp-input.tsx +95 -0
  25. package/templates/app/components/auth/password-field.tsx +116 -0
  26. package/templates/app/components/auth/password-forms.tsx +76 -0
  27. package/templates/app/components/auth/provider-icons.tsx +39 -0
  28. package/templates/app/components/auth/sign-in-flow.tsx +306 -0
  29. package/templates/app/components/auth/sign-up-form.tsx +93 -0
  30. package/templates/app/components/auth/social-buttons.tsx +56 -0
  31. package/templates/app/components/auth/two-factor-form.tsx +139 -0
  32. package/templates/app/components/auth/ui.tsx +96 -0
  33. package/templates/app/components/auth/use-resend.ts +44 -0
  34. package/templates/app/components/auth/verify-email-form.tsx +135 -0
  35. package/templates/app/components/{admin/admin-header.tsx → dashboard/dashboard-header.tsx} +78 -78
  36. package/templates/app/components/{admin/resource-nav.tsx → dashboard/dashboard-sidebar.tsx} +66 -9
  37. package/templates/app/components/dashboard/export-button.tsx +41 -0
  38. package/templates/app/components/dashboard/fields/country-field.tsx +152 -0
  39. package/templates/app/components/{admin → dashboard}/fields/field-widget.tsx +186 -128
  40. package/templates/app/components/{admin → dashboard}/fields/file-field.tsx +302 -302
  41. package/templates/app/components/dashboard/fields/multi-select-field.tsx +46 -0
  42. package/templates/app/components/dashboard/fields/phone-field.tsx +86 -0
  43. package/templates/app/components/dashboard/import-dialog.tsx +487 -0
  44. package/templates/app/components/dashboard/page-header.tsx +57 -0
  45. package/templates/app/components/dashboard/resource-form-dialog.tsx +80 -0
  46. package/templates/app/components/{admin → dashboard}/resource-form-page.tsx +61 -55
  47. package/templates/app/components/{admin → dashboard}/resource-form.tsx +165 -139
  48. package/templates/app/components/dashboard/resource-stats.tsx +53 -0
  49. package/templates/app/components/{admin → dashboard}/resource-table.tsx +331 -251
  50. package/templates/app/components/{admin → dashboard}/row-actions.tsx +144 -111
  51. package/templates/app/components/dashboard/stat-card.tsx +69 -0
  52. package/templates/app/components/dashboard/table-selection.tsx +185 -0
  53. package/templates/app/components/dashboard/traffic-chart.tsx +32 -0
  54. package/templates/app/components/marketing/product-preview.tsx +70 -0
  55. package/templates/app/components/marketing/site-header.tsx +39 -0
  56. package/templates/app/components/sign-out-button.tsx +1 -1
  57. package/templates/app/components/ui/checkbox.tsx +25 -0
  58. package/templates/app/components/ui/radio-group.tsx +29 -0
  59. package/templates/app/db/auth-schema.ts +167 -118
  60. package/templates/app/db/flare-schema.ts +33 -1
  61. package/templates/app/lib/analytics.ts +121 -0
  62. package/templates/app/lib/api-docs.ts +16 -0
  63. package/templates/app/lib/audit.ts +80 -0
  64. package/templates/app/lib/auth-client.ts +16 -1
  65. package/templates/app/lib/auth-config.ts +38 -0
  66. package/templates/app/lib/auth-emails.ts +94 -0
  67. package/templates/app/lib/auth.ts +115 -6
  68. package/templates/app/lib/cache.ts +83 -83
  69. package/templates/app/lib/csv.ts +24 -0
  70. package/templates/app/lib/dashboard-nav.ts +15 -0
  71. package/templates/app/lib/{admin.ts → dashboard.ts} +138 -103
  72. package/templates/app/lib/password-rules.ts +159 -0
  73. package/templates/app/lib/site.ts +56 -0
  74. package/templates/app/lib/theme.ts +79 -0
  75. package/templates/app/migrations/0000_init.sql +47 -4
  76. package/templates/app/migrations/meta/0000_snapshot.json +311 -1
  77. package/templates/app/migrations/meta/_journal.json +1 -1
  78. package/templates/app/vite.config.ts +5 -3
  79. package/templates/app/worker-configuration.d.ts +15764 -0
  80. package/templates/locks/package-lock.json +9189 -0
  81. package/templates/locks/pnpm-lock.yaml +6347 -0
  82. package/templates/app/app/admin/layout.tsx +0 -32
  83. package/templates/app/components/auth-form.tsx +0 -120
  84. package/templates/app/lib/admin-nav.ts +0 -15
  85. /package/templates/app/components/{admin → dashboard}/fields/date-field.tsx +0 -0
  86. /package/templates/app/components/{admin → dashboard}/fields/relation-field.tsx +0 -0
  87. /package/templates/app/components/{admin → dashboard}/query.ts +0 -0
  88. /package/templates/app/components/{admin → dashboard}/resource-icon.tsx +0 -0
  89. /package/templates/app/components/{admin → dashboard}/resource-table-toolbar.tsx +0 -0
@@ -0,0 +1,152 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { CheckIcon, ChevronsUpDownIcon, XIcon } from "lucide-react";
5
+ import { countries, type Country } from "@flaredev/core";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
8
+ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
9
+ import { cn } from "@/lib/utils";
10
+
11
+ /** Countries named in the viewer's language (falls back to English). */
12
+ function useCountries(): Country[] {
13
+ return useMemo(() => countries(typeof navigator === "undefined" ? "en" : navigator.language.split("-")[0]), []);
14
+ }
15
+
16
+ interface ListProps {
17
+ value: string;
18
+ onSelect: (country: Country) => void;
19
+ /** Show dialling codes next to names (for the phone input). */
20
+ withDialCode?: boolean;
21
+ }
22
+
23
+ /** The searchable list itself: type a name, an ISO code or a dialling code. */
24
+ function CountryList({ value, onSelect, withDialCode }: ListProps) {
25
+ const list = useCountries();
26
+ return (
27
+ <Command>
28
+ <CommandInput placeholder={withDialCode ? "Search country or code…" : "Search countries…"} />
29
+ <CommandList>
30
+ <CommandEmpty>No country found.</CommandEmpty>
31
+ <CommandGroup>
32
+ {list.map((country) => (
33
+ <CommandItem
34
+ key={country.code}
35
+ // cmdk filters on this: name, ISO code and dialling code all match.
36
+ value={`${country.name} ${country.code} +${country.dialCode}`}
37
+ onSelect={() => onSelect(country)}
38
+ >
39
+ <span aria-hidden="true" className="text-base leading-none">{country.flag}</span>
40
+ <span className="truncate">{country.name}</span>
41
+ {withDialCode && <span className="ml-auto text-xs text-muted-foreground tabular-nums">+{country.dialCode}</span>}
42
+ <CheckIcon className={cn(withDialCode ? "" : "ml-auto", country.code === value ? "opacity-100" : "opacity-0")} />
43
+ </CommandItem>
44
+ ))}
45
+ </CommandGroup>
46
+ </CommandList>
47
+ </Command>
48
+ );
49
+ }
50
+
51
+ interface FieldProps {
52
+ id: string;
53
+ value: string;
54
+ onChange: (value: string) => void;
55
+ invalid: boolean;
56
+ disabled?: boolean;
57
+ required: boolean;
58
+ placeholder?: string;
59
+ }
60
+
61
+ /** A `country` field: one ISO 3166-1 alpha-2 code, chosen from a searchable list. */
62
+ export function CountryField({ id, value, onChange, invalid, disabled, required, placeholder }: FieldProps) {
63
+ const [open, setOpen] = useState(false);
64
+ const list = useCountries();
65
+ const selected = list.find((country) => country.code === value.toUpperCase());
66
+
67
+ return (
68
+ <div className="flex min-w-0 items-center gap-1">
69
+ <Popover open={open} onOpenChange={setOpen}>
70
+ <PopoverTrigger asChild>
71
+ <Button
72
+ id={id}
73
+ type="button"
74
+ variant="outline"
75
+ role="combobox"
76
+ aria-expanded={open}
77
+ aria-invalid={invalid || undefined}
78
+ disabled={disabled}
79
+ className={cn("min-w-0 flex-1 justify-between font-normal", !selected && "text-muted-foreground")}
80
+ >
81
+ <span className="flex min-w-0 items-center gap-2">
82
+ {selected && <span aria-hidden="true" className="text-base leading-none">{selected.flag}</span>}
83
+ <span className="truncate">{selected ? selected.name : (placeholder ?? "Choose a country")}</span>
84
+ </span>
85
+ <ChevronsUpDownIcon className="opacity-50" />
86
+ </Button>
87
+ </PopoverTrigger>
88
+ <PopoverContent className="w-(--radix-popover-trigger-width) min-w-64 p-0" align="start">
89
+ <CountryList
90
+ value={selected?.code ?? ""}
91
+ onSelect={(country) => {
92
+ onChange(country.code);
93
+ setOpen(false);
94
+ }}
95
+ />
96
+ </PopoverContent>
97
+ </Popover>
98
+ {selected && !required && (
99
+ <Button type="button" variant="ghost" size="icon" aria-label="Clear country" onClick={() => onChange("")} disabled={disabled}>
100
+ <XIcon />
101
+ </Button>
102
+ )}
103
+ </div>
104
+ );
105
+ }
106
+
107
+ interface CodeProps {
108
+ value: string;
109
+ onChange: (country: Country) => void;
110
+ disabled?: boolean;
111
+ invalid: boolean;
112
+ /** Accessible name, e.g. "Country code for Phone". */
113
+ label: string;
114
+ }
115
+
116
+ /** The compact country-code button at the start of a phone input. */
117
+ export function DialCodePicker({ value, onChange, disabled, invalid, label }: CodeProps) {
118
+ const [open, setOpen] = useState(false);
119
+ const list = useCountries();
120
+ const selected = list.find((country) => country.code === value);
121
+
122
+ return (
123
+ <Popover open={open} onOpenChange={setOpen}>
124
+ <PopoverTrigger asChild>
125
+ <Button
126
+ type="button"
127
+ variant="outline"
128
+ role="combobox"
129
+ aria-expanded={open}
130
+ aria-label={selected ? `${label}: ${selected.name} +${selected.dialCode}` : label}
131
+ aria-invalid={invalid || undefined}
132
+ disabled={disabled}
133
+ className="shrink-0 gap-1.5 rounded-r-none border-r-0 px-2.5 font-normal tabular-nums"
134
+ >
135
+ <span aria-hidden="true" className="text-base leading-none">{selected?.flag ?? "🌐"}</span>
136
+ <span>+{selected?.dialCode ?? ""}</span>
137
+ <ChevronsUpDownIcon className="opacity-50" />
138
+ </Button>
139
+ </PopoverTrigger>
140
+ <PopoverContent className="w-72 p-0" align="start">
141
+ <CountryList
142
+ value={value}
143
+ withDialCode
144
+ onSelect={(country) => {
145
+ onChange(country);
146
+ setOpen(false);
147
+ }}
148
+ />
149
+ </PopoverContent>
150
+ </Popover>
151
+ );
152
+ }
@@ -1,128 +1,186 @@
1
- "use client";
2
-
3
- import { optionLabel, type FileField as FileFieldDef, type StoredField } from "@flaredev/core";
4
- import { Input } from "@/components/ui/input";
5
- import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
6
- import { Switch } from "@/components/ui/switch";
7
- import { Textarea } from "@/components/ui/textarea";
8
- import { DateField, DateTimeField } from "./date-field";
9
- import { FileField } from "./file-field";
10
- import { RelationField } from "./relation-field";
11
-
12
- export interface RelationMeta {
13
- name: string;
14
- label: string;
15
- pluralLabel: string;
16
- slug: string;
17
- titleField: string;
18
- }
19
-
20
- export interface WidgetProps {
21
- id: string;
22
- name: string;
23
- /** Resource name and field key, for upload actions. */
24
- resourceName: string;
25
- fieldKey: string;
26
- field: StoredField & { label: string };
27
- value: string | boolean;
28
- onChange: (value: string | boolean) => void;
29
- invalid: boolean;
30
- disabled?: boolean;
31
- /** belongsTo: the related resource and the current value's title. */
32
- relation?: RelationMeta & { initialTitle?: string };
33
- }
34
-
35
- const NONE = "__none__";
36
-
37
- /** The input for a field, chosen from its kind in the descriptor. */
38
- export function FieldWidget(props: WidgetProps) {
39
- const { id, name, field, value, onChange, invalid, disabled, relation, resourceName, fieldKey } = props;
40
- const text = typeof value === "string" ? value : "";
41
- const common = { id, name, disabled, "aria-invalid": invalid || undefined, placeholder: field.placeholder };
42
-
43
- switch (field.kind) {
44
- case "text":
45
- return <Textarea {...common} value={text} rows={4} onChange={(event) => onChange(event.target.value)} />;
46
-
47
- case "int":
48
- case "float":
49
- return (
50
- <Input
51
- {...common}
52
- type="text"
53
- inputMode={field.kind === "int" ? "numeric" : "decimal"}
54
- className="tabular-nums"
55
- value={text}
56
- onChange={(event) => onChange(event.target.value)}
57
- />
58
- );
59
-
60
- case "boolean":
61
- return (
62
- <Switch
63
- id={id}
64
- name={name}
65
- disabled={disabled}
66
- aria-invalid={invalid || undefined}
67
- checked={value === true}
68
- onCheckedChange={(checked) => onChange(checked)}
69
- />
70
- );
71
-
72
- case "enum":
73
- return (
74
- // No selection is `undefined`, so the placeholder shows; "None" clears an optional value.
75
- <Select value={text || undefined} onValueChange={(next) => onChange(next === NONE ? "" : next)} disabled={disabled} name={name}>
76
- <SelectTrigger id={id} aria-invalid={invalid || undefined} className="w-full">
77
- <SelectValue placeholder={field.placeholder ?? `Choose ${field.label.toLowerCase()}`} />
78
- </SelectTrigger>
79
- <SelectContent>
80
- <SelectGroup>
81
- {!field.required && <SelectItem value={NONE}>None</SelectItem>}
82
- {field.options.map((option) => (
83
- <SelectItem key={option} value={option}>
84
- {optionLabel(field, option)}
85
- </SelectItem>
86
- ))}
87
- </SelectGroup>
88
- </SelectContent>
89
- </Select>
90
- );
91
-
92
- case "date":
93
- return (
94
- <DateField id={id} value={text} onChange={onChange} invalid={invalid} disabled={disabled} required={field.required} placeholder={field.placeholder} />
95
- );
96
-
97
- case "datetime":
98
- return (
99
- <DateTimeField id={id} value={text} onChange={onChange} invalid={invalid} disabled={disabled} required={field.required} placeholder={field.placeholder} />
100
- );
101
-
102
- case "belongsTo":
103
- return relation ? (
104
- <RelationField id={id} value={text} onChange={onChange} invalid={invalid} disabled={disabled} required={field.required} relation={relation} />
105
- ) : (
106
- <Input {...common} value={text} onChange={(event) => onChange(event.target.value)} />
107
- );
108
-
109
- case "file":
110
- return (
111
- <FileField
112
- id={id}
113
- value={text}
114
- onChange={onChange}
115
- invalid={invalid}
116
- disabled={disabled}
117
- field={field as FileFieldDef & { label: string }}
118
- resourceName={resourceName}
119
- fieldKey={fieldKey}
120
- />
121
- );
122
-
123
- default: {
124
- const type = field.kind === "string" && field.format === "email" ? "email" : field.kind === "string" && field.format === "url" ? "url" : "text";
125
- return <Input {...common} type={type} value={text} onChange={(event) => onChange(event.target.value)} />;
126
- }
127
- }
128
- }
1
+ "use client";
2
+
3
+ import { normalizeDomain, optionLabel, slugify, type FileField as FileFieldDef, type MultiSelectField as MultiSelectDef, type StoredField } from "@flaredev/core";
4
+ import { Input } from "@/components/ui/input";
5
+ import { Label } from "@/components/ui/label";
6
+ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
7
+ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
8
+ import { Switch } from "@/components/ui/switch";
9
+ import { Textarea } from "@/components/ui/textarea";
10
+ import { DateField, DateTimeField } from "./date-field";
11
+ import { CountryField } from "./country-field";
12
+ import { FileField } from "./file-field";
13
+ import { MultiSelectField } from "./multi-select-field";
14
+ import { PhoneField } from "./phone-field";
15
+ import { RelationField } from "./relation-field";
16
+
17
+ export interface RelationMeta {
18
+ name: string;
19
+ label: string;
20
+ pluralLabel: string;
21
+ slug: string;
22
+ titleField: string;
23
+ }
24
+
25
+ export interface WidgetProps {
26
+ id: string;
27
+ name: string;
28
+ /** Resource name and field key, for upload actions. */
29
+ resourceName: string;
30
+ fieldKey: string;
31
+ field: StoredField & { label: string };
32
+ value: string | boolean;
33
+ onChange: (value: string | boolean) => void;
34
+ invalid: boolean;
35
+ disabled?: boolean;
36
+ /** belongsTo: the related resource and the current value's title. */
37
+ relation?: RelationMeta & { initialTitle?: string };
38
+ }
39
+
40
+ const NONE = "__none__";
41
+
42
+ /** The input for a field, chosen from its kind in the descriptor. */
43
+ export function FieldWidget(props: WidgetProps) {
44
+ const { id, name, field, value, onChange, invalid, disabled, relation, resourceName, fieldKey } = props;
45
+ const text = typeof value === "string" ? value : "";
46
+ const common = { id, name, disabled, "aria-invalid": invalid || undefined, placeholder: field.placeholder };
47
+
48
+ switch (field.kind) {
49
+ case "text":
50
+ return <Textarea {...common} value={text} rows={4} onChange={(event) => onChange(event.target.value)} />;
51
+
52
+ case "int":
53
+ case "float":
54
+ return (
55
+ <Input
56
+ {...common}
57
+ type="text"
58
+ inputMode={field.kind === "int" ? "numeric" : "decimal"}
59
+ className="tabular-nums"
60
+ value={text}
61
+ onChange={(event) => onChange(event.target.value)}
62
+ />
63
+ );
64
+
65
+ case "boolean":
66
+ return (
67
+ <Switch
68
+ id={id}
69
+ name={name}
70
+ disabled={disabled}
71
+ aria-invalid={invalid || undefined}
72
+ checked={value === true}
73
+ onCheckedChange={(checked) => onChange(checked)}
74
+ />
75
+ );
76
+
77
+ case "enum":
78
+ if (field.widget === "radio") {
79
+ return (
80
+ <RadioGroup id={id} name={name} value={text} onValueChange={onChange} disabled={disabled} aria-invalid={invalid || undefined} className="flex flex-wrap gap-x-5 gap-y-2.5">
81
+ {field.options.map((option) => (
82
+ <div key={option} className="flex items-center gap-2">
83
+ <RadioGroupItem id={`${id}-${option}`} value={option} aria-invalid={invalid || undefined} />
84
+ <Label htmlFor={`${id}-${option}`} className="font-normal">
85
+ {optionLabel(field, option)}
86
+ </Label>
87
+ </div>
88
+ ))}
89
+ </RadioGroup>
90
+ );
91
+ }
92
+ return (
93
+ // No selection is `undefined`, so the placeholder shows; "None" clears an optional value.
94
+ <Select value={text || undefined} onValueChange={(next) => onChange(next === NONE ? "" : next)} disabled={disabled} name={name}>
95
+ <SelectTrigger id={id} aria-invalid={invalid || undefined} className="w-full">
96
+ <SelectValue placeholder={field.placeholder ?? `Choose ${field.label.toLowerCase()}`} />
97
+ </SelectTrigger>
98
+ <SelectContent>
99
+ <SelectGroup>
100
+ {!field.required && <SelectItem value={NONE}>None</SelectItem>}
101
+ {field.options.map((option) => (
102
+ <SelectItem key={option} value={option}>
103
+ {optionLabel(field, option)}
104
+ </SelectItem>
105
+ ))}
106
+ </SelectGroup>
107
+ </SelectContent>
108
+ </Select>
109
+ );
110
+
111
+ case "multiselect":
112
+ return (
113
+ <MultiSelectField id={id} field={field as MultiSelectDef & { label: string }} value={text} onChange={onChange} invalid={invalid} disabled={disabled} />
114
+ );
115
+
116
+ case "date":
117
+ return (
118
+ <DateField id={id} value={text} onChange={onChange} invalid={invalid} disabled={disabled} required={field.required} placeholder={field.placeholder} />
119
+ );
120
+
121
+ case "datetime":
122
+ return (
123
+ <DateTimeField id={id} value={text} onChange={onChange} invalid={invalid} disabled={disabled} required={field.required} placeholder={field.placeholder} />
124
+ );
125
+
126
+ case "belongsTo":
127
+ return relation ? (
128
+ <RelationField id={id} value={text} onChange={onChange} invalid={invalid} disabled={disabled} required={field.required} relation={relation} />
129
+ ) : (
130
+ <Input {...common} value={text} onChange={(event) => onChange(event.target.value)} />
131
+ );
132
+
133
+ case "file":
134
+ return (
135
+ <FileField
136
+ id={id}
137
+ value={text}
138
+ onChange={onChange}
139
+ invalid={invalid}
140
+ disabled={disabled}
141
+ field={field as FileFieldDef & { label: string }}
142
+ resourceName={resourceName}
143
+ fieldKey={fieldKey}
144
+ />
145
+ );
146
+
147
+ case "string":
148
+ switch (field.format) {
149
+ case "tel":
150
+ return <PhoneField id={id} name={name} label={field.label} value={text} onChange={onChange} invalid={invalid} disabled={disabled} placeholder={field.placeholder} />;
151
+ case "country":
152
+ return <CountryField id={id} value={text} onChange={onChange} invalid={invalid} disabled={disabled} required={field.required} placeholder={field.placeholder} />;
153
+ case "color":
154
+ return (
155
+ <div className="flex items-center gap-2">
156
+ <input
157
+ type="color"
158
+ aria-label={`${field.label} picker`}
159
+ value={/^#[0-9a-f]{6}$/i.test(text) ? text : "#000000"}
160
+ onChange={(event) => onChange(event.target.value)}
161
+ disabled={disabled}
162
+ className="h-9 w-11 shrink-0 cursor-pointer rounded-md border border-input bg-transparent p-1"
163
+ />
164
+ <Input {...common} className="font-mono" placeholder={field.placeholder ?? "#f2541d"} value={text} onChange={(event) => onChange(event.target.value)} />
165
+ </div>
166
+ );
167
+ case "slug":
168
+ return (
169
+ <Input {...common} className="font-mono" placeholder={field.placeholder ?? "my-first-post"} value={text} onChange={(event) => onChange(event.target.value)} onBlur={() => text && onChange(slugify(text))} />
170
+ );
171
+ case "domain":
172
+ return (
173
+ <Input {...common} inputMode="url" autoCapitalize="none" spellCheck={false} placeholder={field.placeholder ?? "example.com"} value={text} onChange={(event) => onChange(event.target.value)} onBlur={() => text && onChange(normalizeDomain(text))} />
174
+ );
175
+ case "email":
176
+ return <Input {...common} type="email" autoComplete="email" value={text} onChange={(event) => onChange(event.target.value)} />;
177
+ case "url":
178
+ return <Input {...common} type="url" placeholder={field.placeholder ?? "https://"} value={text} onChange={(event) => onChange(event.target.value)} />;
179
+ default:
180
+ return <Input {...common} value={text} onChange={(event) => onChange(event.target.value)} />;
181
+ }
182
+
183
+ default:
184
+ return <Input {...common} value={text} onChange={(event) => onChange(event.target.value)} />;
185
+ }
186
+ }