@takuhon/ui 0.8.2 → 0.10.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.
@@ -0,0 +1,1621 @@
1
+ // src/admin/index.ts
2
+ import "../tokens-TEMWJS5E.css";
3
+
4
+ // src/admin/errors.ts
5
+ function canonicalPointer(raw) {
6
+ let pointer = raw.trim();
7
+ if (pointer.startsWith("#")) pointer = pointer.slice(1);
8
+ if (pointer !== "" && !pointer.startsWith("/")) pointer = `/${pointer}`;
9
+ return pointer;
10
+ }
11
+ function indexErrors(errors) {
12
+ const index = /* @__PURE__ */ new Map();
13
+ for (const error of errors) {
14
+ const key = canonicalPointer(error.pointer ?? error.path ?? "");
15
+ const existing = index.get(key);
16
+ if (existing) existing.push(error.message);
17
+ else index.set(key, [error.message]);
18
+ }
19
+ return index;
20
+ }
21
+ function indexValidationErrors(errors) {
22
+ return indexErrors(errors);
23
+ }
24
+ function errorsAt(index, pointer) {
25
+ return index.get(canonicalPointer(pointer)) ?? [];
26
+ }
27
+ function hasErrorsUnder(index, prefix) {
28
+ const base = canonicalPointer(prefix);
29
+ for (const key of index.keys()) {
30
+ if (key === base || key.startsWith(`${base}/`)) return true;
31
+ }
32
+ return false;
33
+ }
34
+ function collectErrorsUnder(index, prefix) {
35
+ const base = canonicalPointer(prefix);
36
+ const messages = [];
37
+ for (const [key, list] of index) {
38
+ if (key === base || key.startsWith(`${base}/`)) messages.push(...list);
39
+ }
40
+ return messages;
41
+ }
42
+ var NO_FIELD_ERRORS = /* @__PURE__ */ new Map();
43
+
44
+ // src/admin/primitives/Field.tsx
45
+ import { useId } from "react";
46
+ import styles from "../Field.module-CJPK45H7.module.css";
47
+ import { jsx, jsxs } from "react/jsx-runtime";
48
+ function Field({ label, errors, hint, required, children }) {
49
+ const controlId = useId();
50
+ const hintId = useId();
51
+ const errorId = useId();
52
+ const hasErrors = (errors?.length ?? 0) > 0;
53
+ const describedBy = [hint ? hintId : void 0, hasErrors ? errorId : void 0].filter(Boolean).join(" ") || void 0;
54
+ return /* @__PURE__ */ jsxs("div", { className: styles.field, children: [
55
+ /* @__PURE__ */ jsxs("label", { className: styles.label, htmlFor: controlId, children: [
56
+ label,
57
+ required ? /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: " *" }) : null
58
+ ] }),
59
+ hint ? /* @__PURE__ */ jsx("p", { className: styles.hint, id: hintId, children: hint }) : null,
60
+ children({ controlId, describedBy, invalid: hasErrors }),
61
+ hasErrors ? /* @__PURE__ */ jsx("ul", { className: styles.errors, id: errorId, children: errors.map((message, i) => /* @__PURE__ */ jsx("li", { children: message }, i)) }) : null
62
+ ] });
63
+ }
64
+
65
+ // src/admin/primitives/TextField.tsx
66
+ import styles2 from "../controls.module-CMB7V22N.module.css";
67
+ import { jsx as jsx2 } from "react/jsx-runtime";
68
+ function TextField({
69
+ label,
70
+ value,
71
+ onChange,
72
+ errors,
73
+ hint,
74
+ required,
75
+ placeholder,
76
+ type = "text",
77
+ inputMode
78
+ }) {
79
+ return /* @__PURE__ */ jsx2(Field, { label, errors, hint, required, children: ({ controlId, describedBy, invalid }) => /* @__PURE__ */ jsx2(
80
+ "input",
81
+ {
82
+ id: controlId,
83
+ className: styles2.control,
84
+ type,
85
+ value,
86
+ placeholder,
87
+ inputMode,
88
+ "aria-invalid": invalid || void 0,
89
+ "aria-required": required ? true : void 0,
90
+ "aria-describedby": describedBy,
91
+ onChange: (event) => {
92
+ onChange(event.target.value);
93
+ }
94
+ }
95
+ ) });
96
+ }
97
+
98
+ // src/admin/primitives/TextAreaField.tsx
99
+ import styles3 from "../controls.module-CMB7V22N.module.css";
100
+ import { jsx as jsx3 } from "react/jsx-runtime";
101
+ function TextAreaField({
102
+ label,
103
+ value,
104
+ onChange,
105
+ errors,
106
+ hint,
107
+ required,
108
+ placeholder,
109
+ rows = 4
110
+ }) {
111
+ return /* @__PURE__ */ jsx3(Field, { label, errors, hint, required, children: ({ controlId, describedBy, invalid }) => /* @__PURE__ */ jsx3(
112
+ "textarea",
113
+ {
114
+ id: controlId,
115
+ className: `${styles3.control} ${styles3.textarea}`,
116
+ value,
117
+ rows,
118
+ placeholder,
119
+ "aria-invalid": invalid || void 0,
120
+ "aria-required": required ? true : void 0,
121
+ "aria-describedby": describedBy,
122
+ onChange: (event) => {
123
+ onChange(event.target.value);
124
+ }
125
+ }
126
+ ) });
127
+ }
128
+
129
+ // src/admin/primitives/SelectField.tsx
130
+ import styles4 from "../controls.module-CMB7V22N.module.css";
131
+ import { jsx as jsx4 } from "react/jsx-runtime";
132
+ function SelectField({
133
+ label,
134
+ value,
135
+ options,
136
+ onChange,
137
+ errors,
138
+ hint,
139
+ required
140
+ }) {
141
+ return /* @__PURE__ */ jsx4(Field, { label, errors, hint, required, children: ({ controlId, describedBy, invalid }) => /* @__PURE__ */ jsx4(
142
+ "select",
143
+ {
144
+ id: controlId,
145
+ className: styles4.control,
146
+ value,
147
+ "aria-invalid": invalid || void 0,
148
+ "aria-required": required ? true : void 0,
149
+ "aria-describedby": describedBy,
150
+ onChange: (event) => {
151
+ onChange(event.target.value);
152
+ },
153
+ children: options.map((option) => /* @__PURE__ */ jsx4("option", { value: option.value, children: option.label }, option.value))
154
+ }
155
+ ) });
156
+ }
157
+
158
+ // src/admin/primitives/CheckboxField.tsx
159
+ import { useId as useId2 } from "react";
160
+ import styles5 from "../controls.module-CMB7V22N.module.css";
161
+ import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
162
+ function CheckboxField({ label, checked, onChange }) {
163
+ const id = useId2();
164
+ return /* @__PURE__ */ jsxs2("div", { className: styles5.checkboxRow, children: [
165
+ /* @__PURE__ */ jsx5(
166
+ "input",
167
+ {
168
+ id,
169
+ className: styles5.checkbox,
170
+ type: "checkbox",
171
+ checked,
172
+ onChange: (event) => {
173
+ onChange(event.target.checked);
174
+ }
175
+ }
176
+ ),
177
+ /* @__PURE__ */ jsx5("label", { className: styles5.checkboxLabel, htmlFor: id, children: label })
178
+ ] });
179
+ }
180
+
181
+ // src/admin/primitives/LocaleTabs.tsx
182
+ import { useId as useId3, useRef, useState } from "react";
183
+ import styles6 from "../LocaleTabs.module-IEEC6Q27.module.css";
184
+ import { Fragment, jsx as jsx6, jsxs as jsxs3 } from "react/jsx-runtime";
185
+ function LocaleTabs({
186
+ label,
187
+ value,
188
+ locales,
189
+ onChange,
190
+ multiline = false,
191
+ required,
192
+ hint,
193
+ errors = NO_FIELD_ERRORS,
194
+ pointer,
195
+ formatLocale
196
+ }) {
197
+ const baseId = useId3();
198
+ const hintId = useId3();
199
+ const errorId = useId3();
200
+ const [active, setActive] = useState(() => locales[0] ?? "");
201
+ const tabRefs = useRef({});
202
+ const format = formatLocale ?? ((locale) => locale);
203
+ const labelId = `${baseId}-label`;
204
+ if (locales.length === 0) {
205
+ return /* @__PURE__ */ jsxs3("div", { className: styles6.group, role: "group", "aria-labelledby": labelId, children: [
206
+ /* @__PURE__ */ jsx6("span", { className: styles6.groupLabel, id: labelId, children: label }),
207
+ /* @__PURE__ */ jsx6("p", { className: styles6.hint, children: "No locales are configured yet; add one under Settings." })
208
+ ] });
209
+ }
210
+ const activeLocale = locales.includes(active) ? active : locales[0];
211
+ const text = value?.[activeLocale] ?? "";
212
+ const baseErrors = pointer ? errorsAt(errors, pointer) : [];
213
+ const localeErrors = pointer ? errorsAt(errors, `${pointer}/${activeLocale}`) : [];
214
+ const shownErrors = [...baseErrors, ...localeErrors];
215
+ const hasErrors = shownErrors.length > 0;
216
+ const describedBy = [hint ? hintId : void 0, hasErrors ? errorId : void 0].filter(Boolean).join(" ") || void 0;
217
+ const setText = (next) => {
218
+ const record = { ...value ?? {} };
219
+ if (next === "") delete record[activeLocale];
220
+ else record[activeLocale] = next;
221
+ onChange(Object.keys(record).length === 0 ? void 0 : record);
222
+ };
223
+ const onTabKeyDown = (event) => {
224
+ const current = locales.indexOf(activeLocale);
225
+ let nextIndex;
226
+ switch (event.key) {
227
+ case "ArrowRight":
228
+ case "ArrowDown":
229
+ nextIndex = (current + 1) % locales.length;
230
+ break;
231
+ case "ArrowLeft":
232
+ case "ArrowUp":
233
+ nextIndex = (current - 1 + locales.length) % locales.length;
234
+ break;
235
+ case "Home":
236
+ nextIndex = 0;
237
+ break;
238
+ case "End":
239
+ nextIndex = locales.length - 1;
240
+ break;
241
+ default:
242
+ return;
243
+ }
244
+ event.preventDefault();
245
+ const nextLocale = locales[nextIndex];
246
+ if (nextLocale !== void 0) {
247
+ setActive(nextLocale);
248
+ tabRefs.current[nextLocale]?.focus();
249
+ }
250
+ };
251
+ const controlName = `${label} (${format(activeLocale)})`;
252
+ return /* @__PURE__ */ jsxs3("div", { className: styles6.group, role: "group", "aria-labelledby": labelId, children: [
253
+ /* @__PURE__ */ jsxs3("span", { className: styles6.groupLabel, id: labelId, children: [
254
+ label,
255
+ required ? /* @__PURE__ */ jsx6("span", { "aria-hidden": "true", children: " *" }) : null
256
+ ] }),
257
+ hint ? /* @__PURE__ */ jsx6("p", { className: styles6.hint, id: hintId, children: hint }) : null,
258
+ /* @__PURE__ */ jsx6("div", { className: styles6.tablist, role: "tablist", "aria-label": label, children: locales.map((locale) => {
259
+ const selected = locale === activeLocale;
260
+ const tabHasErrors = pointer ? hasErrorsUnder(errors, `${pointer}/${locale}`) : false;
261
+ return /* @__PURE__ */ jsxs3(
262
+ "button",
263
+ {
264
+ ref: (element) => {
265
+ tabRefs.current[locale] = element;
266
+ },
267
+ type: "button",
268
+ role: "tab",
269
+ id: `${baseId}-tab-${locale}`,
270
+ "aria-selected": selected,
271
+ "aria-controls": `${baseId}-panel`,
272
+ tabIndex: selected ? 0 : -1,
273
+ className: `${styles6.tab} ${selected ? styles6.tabActive : ""}`,
274
+ onClick: () => {
275
+ setActive(locale);
276
+ },
277
+ onKeyDown: onTabKeyDown,
278
+ children: [
279
+ format(locale),
280
+ tabHasErrors ? /* @__PURE__ */ jsxs3(Fragment, { children: [
281
+ /* @__PURE__ */ jsxs3("span", { className: styles6.tabError, "aria-hidden": "true", children: [
282
+ " ",
283
+ "\u25CF"
284
+ ] }),
285
+ /* @__PURE__ */ jsx6("span", { className: styles6.srOnly, children: " (has errors)" })
286
+ ] }) : null
287
+ ]
288
+ },
289
+ locale
290
+ );
291
+ }) }),
292
+ /* @__PURE__ */ jsxs3(
293
+ "div",
294
+ {
295
+ role: "tabpanel",
296
+ id: `${baseId}-panel`,
297
+ "aria-labelledby": `${baseId}-tab-${activeLocale}`,
298
+ className: styles6.panel,
299
+ children: [
300
+ multiline ? /* @__PURE__ */ jsx6(
301
+ "textarea",
302
+ {
303
+ className: `${styles6.control} ${styles6.textarea}`,
304
+ value: text,
305
+ rows: 4,
306
+ "aria-label": controlName,
307
+ "aria-invalid": hasErrors || void 0,
308
+ "aria-required": required ? true : void 0,
309
+ "aria-describedby": describedBy,
310
+ onChange: (event) => {
311
+ setText(event.target.value);
312
+ }
313
+ }
314
+ ) : /* @__PURE__ */ jsx6(
315
+ "input",
316
+ {
317
+ className: styles6.control,
318
+ type: "text",
319
+ value: text,
320
+ "aria-label": controlName,
321
+ "aria-invalid": hasErrors || void 0,
322
+ "aria-required": required ? true : void 0,
323
+ "aria-describedby": describedBy,
324
+ onChange: (event) => {
325
+ setText(event.target.value);
326
+ }
327
+ }
328
+ ),
329
+ hasErrors ? /* @__PURE__ */ jsx6("ul", { className: styles6.errors, id: errorId, children: shownErrors.map((message, i) => /* @__PURE__ */ jsx6("li", { children: message }, i)) }) : null
330
+ ]
331
+ }
332
+ )
333
+ ] });
334
+ }
335
+
336
+ // src/admin/primitives/Repeater.tsx
337
+ import styles7 from "../Repeater.module-MWSEKS4G.module.css";
338
+ import { jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
339
+ function Repeater({
340
+ legend,
341
+ items,
342
+ onChange,
343
+ renderItem,
344
+ createItem,
345
+ keyOf,
346
+ itemLabel,
347
+ addLabel = "Add",
348
+ removeLabel = "Remove",
349
+ moveUpLabel = "Move up",
350
+ moveDownLabel = "Move down",
351
+ emptyHint
352
+ }) {
353
+ const key = keyOf ?? ((_item, index) => String(index));
354
+ const update = (index, next) => {
355
+ const copy = items.slice();
356
+ copy[index] = next;
357
+ onChange(copy);
358
+ };
359
+ const remove = (index) => {
360
+ onChange(items.filter((_item, i) => i !== index));
361
+ };
362
+ const move = (from, to) => {
363
+ if (to < 0 || to >= items.length) return;
364
+ const copy = items.slice();
365
+ const [moved] = copy.splice(from, 1);
366
+ if (moved === void 0) return;
367
+ copy.splice(to, 0, moved);
368
+ onChange(copy);
369
+ };
370
+ return /* @__PURE__ */ jsxs4("fieldset", { className: styles7.fieldset, children: [
371
+ /* @__PURE__ */ jsx7("legend", { className: styles7.legend, children: legend }),
372
+ items.length === 0 && emptyHint ? /* @__PURE__ */ jsx7("p", { className: styles7.empty, children: emptyHint }) : null,
373
+ /* @__PURE__ */ jsx7("ol", { className: styles7.list, children: items.map((item, index) => {
374
+ const caption = itemLabel(item, index);
375
+ return /* @__PURE__ */ jsx7("li", { className: styles7.item, children: /* @__PURE__ */ jsxs4("div", { role: "group", "aria-label": caption, children: [
376
+ /* @__PURE__ */ jsxs4("div", { className: styles7.itemHeader, children: [
377
+ /* @__PURE__ */ jsx7("span", { className: styles7.itemCaption, children: caption }),
378
+ /* @__PURE__ */ jsxs4("div", { className: styles7.itemActions, children: [
379
+ /* @__PURE__ */ jsx7(
380
+ "button",
381
+ {
382
+ type: "button",
383
+ className: styles7.iconButton,
384
+ "aria-label": `${moveUpLabel}: ${caption}`,
385
+ disabled: index === 0,
386
+ onClick: () => {
387
+ move(index, index - 1);
388
+ },
389
+ children: "\u2191"
390
+ }
391
+ ),
392
+ /* @__PURE__ */ jsx7(
393
+ "button",
394
+ {
395
+ type: "button",
396
+ className: styles7.iconButton,
397
+ "aria-label": `${moveDownLabel}: ${caption}`,
398
+ disabled: index === items.length - 1,
399
+ onClick: () => {
400
+ move(index, index + 1);
401
+ },
402
+ children: "\u2193"
403
+ }
404
+ ),
405
+ /* @__PURE__ */ jsx7(
406
+ "button",
407
+ {
408
+ type: "button",
409
+ className: styles7.removeButton,
410
+ "aria-label": `${removeLabel}: ${caption}`,
411
+ onClick: () => {
412
+ remove(index);
413
+ },
414
+ children: removeLabel
415
+ }
416
+ )
417
+ ] })
418
+ ] }),
419
+ /* @__PURE__ */ jsx7("div", { className: styles7.itemBody, children: renderItem(
420
+ item,
421
+ (next) => {
422
+ update(index, next);
423
+ },
424
+ index
425
+ ) })
426
+ ] }) }, key(item, index));
427
+ }) }),
428
+ /* @__PURE__ */ jsx7(
429
+ "button",
430
+ {
431
+ type: "button",
432
+ className: styles7.addButton,
433
+ onClick: () => {
434
+ onChange([...items, createItem()]);
435
+ },
436
+ children: addLabel
437
+ }
438
+ )
439
+ ] });
440
+ }
441
+
442
+ // src/admin/admin-labels.ts
443
+ var EN = {
444
+ "app.title": "takuhon admin",
445
+ "toolbar.label": "Editor actions",
446
+ "mode.label": "Editing mode",
447
+ "mode.form": "Form",
448
+ "mode.advanced": "Advanced (JSON)",
449
+ "action.save": "Save",
450
+ "action.reload": "Reload",
451
+ "action.export": "Export",
452
+ "action.import": "Import",
453
+ "action.add": "Add",
454
+ "action.remove": "Remove",
455
+ "action.moveUp": "Move up",
456
+ "action.moveDown": "Move down",
457
+ "status.saving": "Saving\u2026",
458
+ "status.saved": "Saved.",
459
+ "status.loading": "Loading\u2026",
460
+ "status.conflict": "The profile changed on the server since it was loaded. Reload, then reapply the edits.",
461
+ "status.invalid": "Some fields need attention before saving.",
462
+ "status.fixSummary": "Please fix the following:",
463
+ "status.error": "Something went wrong. Please try again.",
464
+ "status.imported": "Imported. Review the fields, then save to apply.",
465
+ "status.importInvalid": "The imported file is not a valid takuhon document.",
466
+ "section.profile": "Profile",
467
+ "section.about": "About",
468
+ "section.links": "Links",
469
+ "section.careers": "Experience",
470
+ "section.projects": "Projects",
471
+ "section.skills": "Skills",
472
+ "section.settings": "Settings",
473
+ "field.displayName": "Display name",
474
+ "field.tagline": "Tagline",
475
+ "field.bio": "Bio",
476
+ "field.avatarUrl": "Avatar URL",
477
+ "field.avatarAlt": "Avatar alternative text",
478
+ "field.location.country": "Country",
479
+ "field.location.region": "Region",
480
+ "field.location.locality": "Locality",
481
+ "field.location.display": "Location (display)",
482
+ "field.link.type": "Type",
483
+ "field.link.url": "URL",
484
+ "field.link.label": "Label",
485
+ "field.link.iconUrl": "Icon URL",
486
+ "field.link.featured": "Featured",
487
+ "field.career.organization": "Organization",
488
+ "field.career.role": "Role",
489
+ "field.career.description": "Description",
490
+ "field.career.startDate": "Start",
491
+ "field.career.endDate": "End",
492
+ "field.career.isCurrent": "Current position",
493
+ "field.career.url": "URL",
494
+ "field.project.title": "Title",
495
+ "field.project.description": "Description",
496
+ "field.project.url": "URL",
497
+ "field.project.tags": "Tags",
498
+ "field.project.highlighted": "Highlighted",
499
+ "field.project.startDate": "Start",
500
+ "field.project.endDate": "End",
501
+ "field.skill.label": "Label",
502
+ "field.skill.category": "Category",
503
+ "field.settings.defaultLocale": "Default locale",
504
+ "field.settings.fallbackLocale": "Fallback locale",
505
+ "field.settings.availableLocales": "Available locales",
506
+ "field.settings.theme": "Theme",
507
+ "field.settings.showPoweredBy": 'Show the "Powered by takuhon" footer',
508
+ "field.settings.enableJsonLd": "Emit Schema.org JSON-LD",
509
+ "field.settings.enableApi": "Expose the public read API",
510
+ "field.settings.enableAnalytics": "Enable first-party analytics",
511
+ "item.link": "Link",
512
+ "item.career": "Position",
513
+ "item.project": "Project",
514
+ "item.skill": "Skill",
515
+ "empty.links": "No links yet.",
516
+ "empty.careers": "No positions yet.",
517
+ "empty.projects": "No projects yet.",
518
+ "empty.skills": "No skills yet.",
519
+ "hint.avatarNoUpload": "Paste an image URL. Uploading image files is not available yet.",
520
+ "hint.month": "Format: YYYY-MM (e.g. 2024-03).",
521
+ "hint.country": "ISO 3166-1 alpha-2 code, e.g. US.",
522
+ "hint.tags": "Comma-separated.",
523
+ "hint.locales": "Comma-separated BCP-47 tags, e.g. en, ja. Drives the language tabs above.",
524
+ "advanced.hint": "Edit the entire document as JSON. Edits apply only while the JSON is valid.",
525
+ "advanced.invalid": "The JSON is not a valid takuhon document:",
526
+ "option.none": "(none)"
527
+ };
528
+ var DICTIONARIES = {
529
+ en: EN
530
+ };
531
+ function getAdminLabel(key, locale = "en") {
532
+ const exact = DICTIONARIES[locale]?.[key];
533
+ if (exact !== void 0) return exact;
534
+ const base = locale.split("-")[0];
535
+ const baseMatch = base ? DICTIONARIES[base]?.[key] : void 0;
536
+ return baseMatch ?? EN[key];
537
+ }
538
+
539
+ // src/admin/sections/ProfileForm.tsx
540
+ import styles8 from "../sections.module-ZVBKZHDE.module.css";
541
+ import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
542
+ var POINTER = "/profile";
543
+ function isEmptyRecord(record) {
544
+ return !record || Object.keys(record).length === 0;
545
+ }
546
+ function ProfileForm({
547
+ value,
548
+ onChange,
549
+ locales,
550
+ errors = NO_FIELD_ERRORS,
551
+ formatLocale
552
+ }) {
553
+ const updateAvatar = (patch) => {
554
+ const merged = { url: "", ...value.avatar, ...patch };
555
+ const keep = merged.url !== "" || !isEmptyRecord(merged.alt);
556
+ onChange({ ...value, avatar: keep ? merged : void 0 });
557
+ };
558
+ const updateLocation = (patch) => {
559
+ const merged = { ...value.location, ...patch };
560
+ const empty = !merged.country && !merged.region && isEmptyRecord(merged.locality) && isEmptyRecord(merged.display);
561
+ onChange({ ...value, location: empty ? void 0 : merged });
562
+ };
563
+ const headingId = "admin-section-profile";
564
+ return /* @__PURE__ */ jsxs5("section", { className: styles8.section, "aria-labelledby": headingId, children: [
565
+ /* @__PURE__ */ jsx8("h2", { className: styles8.heading, id: headingId, children: getAdminLabel("section.profile") }),
566
+ /* @__PURE__ */ jsx8(
567
+ LocaleTabs,
568
+ {
569
+ label: getAdminLabel("field.displayName"),
570
+ value: value.displayName,
571
+ locales,
572
+ onChange: (next) => {
573
+ onChange({ ...value, displayName: next ?? {} });
574
+ },
575
+ required: true,
576
+ pointer: `${POINTER}/displayName`,
577
+ errors,
578
+ formatLocale
579
+ }
580
+ ),
581
+ /* @__PURE__ */ jsx8(
582
+ LocaleTabs,
583
+ {
584
+ label: getAdminLabel("field.tagline"),
585
+ value: value.tagline,
586
+ locales,
587
+ onChange: (next) => {
588
+ onChange({ ...value, tagline: next });
589
+ },
590
+ pointer: `${POINTER}/tagline`,
591
+ errors,
592
+ formatLocale
593
+ }
594
+ ),
595
+ /* @__PURE__ */ jsx8("h3", { className: styles8.subheading, children: getAdminLabel("section.about") }),
596
+ /* @__PURE__ */ jsx8(
597
+ LocaleTabs,
598
+ {
599
+ label: getAdminLabel("field.bio"),
600
+ value: value.bio,
601
+ locales,
602
+ onChange: (next) => {
603
+ onChange({ ...value, bio: next });
604
+ },
605
+ multiline: true,
606
+ pointer: `${POINTER}/bio`,
607
+ errors,
608
+ formatLocale
609
+ }
610
+ ),
611
+ /* @__PURE__ */ jsx8(
612
+ TextField,
613
+ {
614
+ label: getAdminLabel("field.avatarUrl"),
615
+ type: "url",
616
+ value: value.avatar?.url ?? "",
617
+ onChange: (url) => {
618
+ updateAvatar({ url });
619
+ },
620
+ hint: getAdminLabel("hint.avatarNoUpload"),
621
+ errors: errorsAt(errors, `${POINTER}/avatar/url`)
622
+ }
623
+ ),
624
+ /* @__PURE__ */ jsx8(
625
+ LocaleTabs,
626
+ {
627
+ label: getAdminLabel("field.avatarAlt"),
628
+ value: value.avatar?.alt,
629
+ locales,
630
+ onChange: (next) => {
631
+ updateAvatar({ alt: next });
632
+ },
633
+ pointer: `${POINTER}/avatar/alt`,
634
+ errors,
635
+ formatLocale
636
+ }
637
+ ),
638
+ /* @__PURE__ */ jsx8(
639
+ TextField,
640
+ {
641
+ label: getAdminLabel("field.location.country"),
642
+ value: value.location?.country ?? "",
643
+ onChange: (country) => {
644
+ updateLocation({ country: country || void 0 });
645
+ },
646
+ hint: getAdminLabel("hint.country"),
647
+ errors: errorsAt(errors, `${POINTER}/location/country`)
648
+ }
649
+ ),
650
+ /* @__PURE__ */ jsx8(
651
+ TextField,
652
+ {
653
+ label: getAdminLabel("field.location.region"),
654
+ value: value.location?.region ?? "",
655
+ onChange: (region) => {
656
+ updateLocation({ region: region || void 0 });
657
+ },
658
+ errors: errorsAt(errors, `${POINTER}/location/region`)
659
+ }
660
+ ),
661
+ /* @__PURE__ */ jsx8(
662
+ LocaleTabs,
663
+ {
664
+ label: getAdminLabel("field.location.locality"),
665
+ value: value.location?.locality,
666
+ locales,
667
+ onChange: (next) => {
668
+ updateLocation({ locality: next });
669
+ },
670
+ pointer: `${POINTER}/location/locality`,
671
+ errors,
672
+ formatLocale
673
+ }
674
+ )
675
+ ] });
676
+ }
677
+
678
+ // src/admin/ids.ts
679
+ function makeId(prefix, taken) {
680
+ const safe = prefix.toLowerCase().replace(/[^a-z0-9-]/g, "") || "item";
681
+ const used = new Set(taken);
682
+ let n = 1;
683
+ while (used.has(`${safe}-${String(n)}`)) n += 1;
684
+ return `${safe}-${String(n)}`;
685
+ }
686
+
687
+ // src/admin/sections/LinksForm.tsx
688
+ import { Fragment as Fragment2, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
689
+ var LINK_TYPES = [
690
+ "website",
691
+ "blog",
692
+ "github",
693
+ "gitlab",
694
+ "linkedin",
695
+ "x",
696
+ "mastodon",
697
+ "bluesky",
698
+ "instagram",
699
+ "youtube",
700
+ "threads",
701
+ "facebook",
702
+ "email",
703
+ "rss",
704
+ "custom"
705
+ ];
706
+ function retypeLink(link, type) {
707
+ if (type === "custom") {
708
+ return {
709
+ id: link.id,
710
+ url: link.url,
711
+ label: link.label,
712
+ featured: link.featured,
713
+ order: link.order,
714
+ type: "custom",
715
+ iconUrl: link.iconUrl ?? ""
716
+ };
717
+ }
718
+ const builtin = {
719
+ id: link.id,
720
+ url: link.url,
721
+ label: link.label,
722
+ featured: link.featured,
723
+ order: link.order,
724
+ type
725
+ };
726
+ if (link.iconUrl) builtin.iconUrl = link.iconUrl;
727
+ return builtin;
728
+ }
729
+ function setIconUrl(link, iconUrl) {
730
+ if (link.type === "custom") return { ...link, iconUrl };
731
+ return { ...link, iconUrl: iconUrl || void 0 };
732
+ }
733
+ function LinksForm({
734
+ value,
735
+ onChange,
736
+ locales,
737
+ errors = NO_FIELD_ERRORS,
738
+ formatLocale
739
+ }) {
740
+ return /* @__PURE__ */ jsx9(
741
+ Repeater,
742
+ {
743
+ legend: getAdminLabel("section.links"),
744
+ items: value,
745
+ onChange,
746
+ keyOf: (link) => link.id,
747
+ itemLabel: (link, index) => link.url || `${getAdminLabel("item.link")} ${String(index + 1)}`,
748
+ createItem: () => ({
749
+ id: makeId(
750
+ "link",
751
+ value.map((l) => l.id)
752
+ ),
753
+ type: "website",
754
+ url: ""
755
+ }),
756
+ addLabel: getAdminLabel("action.add"),
757
+ removeLabel: getAdminLabel("action.remove"),
758
+ moveUpLabel: getAdminLabel("action.moveUp"),
759
+ moveDownLabel: getAdminLabel("action.moveDown"),
760
+ emptyHint: getAdminLabel("empty.links"),
761
+ renderItem: (link, update, index) => {
762
+ const at = `/links/${String(index)}`;
763
+ return /* @__PURE__ */ jsxs6(Fragment2, { children: [
764
+ /* @__PURE__ */ jsx9(
765
+ SelectField,
766
+ {
767
+ label: getAdminLabel("field.link.type"),
768
+ value: link.type,
769
+ options: LINK_TYPES.map((t) => ({ value: t, label: t })),
770
+ onChange: (t) => {
771
+ update(retypeLink(link, t));
772
+ },
773
+ errors: errorsAt(errors, `${at}/type`)
774
+ }
775
+ ),
776
+ /* @__PURE__ */ jsx9(
777
+ TextField,
778
+ {
779
+ label: getAdminLabel("field.link.url"),
780
+ type: "url",
781
+ value: link.url,
782
+ onChange: (url) => {
783
+ update({ ...link, url });
784
+ },
785
+ required: true,
786
+ errors: errorsAt(errors, `${at}/url`)
787
+ }
788
+ ),
789
+ /* @__PURE__ */ jsx9(
790
+ LocaleTabs,
791
+ {
792
+ label: getAdminLabel("field.link.label"),
793
+ value: link.label,
794
+ locales,
795
+ onChange: (next) => {
796
+ update({ ...link, label: next });
797
+ },
798
+ pointer: `${at}/label`,
799
+ errors,
800
+ formatLocale
801
+ }
802
+ ),
803
+ /* @__PURE__ */ jsx9(
804
+ TextField,
805
+ {
806
+ label: getAdminLabel("field.link.iconUrl"),
807
+ type: "url",
808
+ value: link.iconUrl ?? "",
809
+ onChange: (iconUrl) => {
810
+ update(setIconUrl(link, iconUrl));
811
+ },
812
+ required: link.type === "custom",
813
+ errors: errorsAt(errors, `${at}/iconUrl`)
814
+ }
815
+ ),
816
+ /* @__PURE__ */ jsx9(
817
+ CheckboxField,
818
+ {
819
+ label: getAdminLabel("field.link.featured"),
820
+ checked: link.featured ?? false,
821
+ onChange: (featured) => {
822
+ update({ ...link, featured: featured || void 0 });
823
+ }
824
+ }
825
+ )
826
+ ] });
827
+ }
828
+ }
829
+ );
830
+ }
831
+
832
+ // src/admin/localized.ts
833
+ function firstLocalized(record, locales) {
834
+ if (!record) return "";
835
+ for (const locale of locales) {
836
+ const value = record[locale];
837
+ if (value) return value;
838
+ }
839
+ return Object.values(record).find((value) => value !== "") ?? "";
840
+ }
841
+
842
+ // src/admin/sections/CareersForm.tsx
843
+ import { Fragment as Fragment3, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
844
+ function CareersForm({
845
+ value,
846
+ onChange,
847
+ locales,
848
+ errors = NO_FIELD_ERRORS,
849
+ formatLocale
850
+ }) {
851
+ return /* @__PURE__ */ jsx10(
852
+ Repeater,
853
+ {
854
+ legend: getAdminLabel("section.careers"),
855
+ items: value,
856
+ onChange,
857
+ keyOf: (career) => career.id,
858
+ itemLabel: (career, index) => firstLocalized(career.organization, locales) || `${getAdminLabel("item.career")} ${String(index + 1)}`,
859
+ createItem: () => ({
860
+ id: makeId(
861
+ "career",
862
+ value.map((c) => c.id)
863
+ ),
864
+ organization: {},
865
+ role: {},
866
+ startDate: ""
867
+ }),
868
+ addLabel: getAdminLabel("action.add"),
869
+ removeLabel: getAdminLabel("action.remove"),
870
+ moveUpLabel: getAdminLabel("action.moveUp"),
871
+ moveDownLabel: getAdminLabel("action.moveDown"),
872
+ emptyHint: getAdminLabel("empty.careers"),
873
+ renderItem: (career, update, index) => {
874
+ const at = `/careers/${String(index)}`;
875
+ return /* @__PURE__ */ jsxs7(Fragment3, { children: [
876
+ /* @__PURE__ */ jsx10(
877
+ LocaleTabs,
878
+ {
879
+ label: getAdminLabel("field.career.organization"),
880
+ value: career.organization,
881
+ locales,
882
+ onChange: (next) => {
883
+ update({ ...career, organization: next ?? {} });
884
+ },
885
+ required: true,
886
+ pointer: `${at}/organization`,
887
+ errors,
888
+ formatLocale
889
+ }
890
+ ),
891
+ /* @__PURE__ */ jsx10(
892
+ LocaleTabs,
893
+ {
894
+ label: getAdminLabel("field.career.role"),
895
+ value: career.role,
896
+ locales,
897
+ onChange: (next) => {
898
+ update({ ...career, role: next ?? {} });
899
+ },
900
+ required: true,
901
+ pointer: `${at}/role`,
902
+ errors,
903
+ formatLocale
904
+ }
905
+ ),
906
+ /* @__PURE__ */ jsx10(
907
+ TextField,
908
+ {
909
+ label: getAdminLabel("field.career.startDate"),
910
+ type: "month",
911
+ value: career.startDate,
912
+ onChange: (startDate) => {
913
+ update({ ...career, startDate });
914
+ },
915
+ required: true,
916
+ hint: getAdminLabel("hint.month"),
917
+ errors: errorsAt(errors, `${at}/startDate`)
918
+ }
919
+ ),
920
+ /* @__PURE__ */ jsx10(
921
+ TextField,
922
+ {
923
+ label: getAdminLabel("field.career.endDate"),
924
+ type: "month",
925
+ value: career.endDate ?? "",
926
+ onChange: (endDate) => {
927
+ update({ ...career, endDate: endDate || void 0 });
928
+ },
929
+ hint: getAdminLabel("hint.month"),
930
+ errors: errorsAt(errors, `${at}/endDate`)
931
+ }
932
+ ),
933
+ /* @__PURE__ */ jsx10(
934
+ CheckboxField,
935
+ {
936
+ label: getAdminLabel("field.career.isCurrent"),
937
+ checked: career.isCurrent ?? false,
938
+ onChange: (isCurrent) => {
939
+ update({ ...career, isCurrent: isCurrent || void 0 });
940
+ }
941
+ }
942
+ ),
943
+ /* @__PURE__ */ jsx10(
944
+ LocaleTabs,
945
+ {
946
+ label: getAdminLabel("field.career.description"),
947
+ value: career.description,
948
+ locales,
949
+ onChange: (next) => {
950
+ update({ ...career, description: next });
951
+ },
952
+ multiline: true,
953
+ pointer: `${at}/description`,
954
+ errors,
955
+ formatLocale
956
+ }
957
+ ),
958
+ /* @__PURE__ */ jsx10(
959
+ TextField,
960
+ {
961
+ label: getAdminLabel("field.career.url"),
962
+ type: "url",
963
+ value: career.url ?? "",
964
+ onChange: (url) => {
965
+ update({ ...career, url: url || void 0 });
966
+ },
967
+ errors: errorsAt(errors, `${at}/url`)
968
+ }
969
+ )
970
+ ] });
971
+ }
972
+ }
973
+ );
974
+ }
975
+
976
+ // src/admin/sections/ProjectsForm.tsx
977
+ import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
978
+ function parseTags(input) {
979
+ const tags = input.split(",").map((tag) => tag.trim()).filter((tag) => tag !== "");
980
+ return tags.length > 0 ? tags : void 0;
981
+ }
982
+ function ProjectsForm({
983
+ value,
984
+ onChange,
985
+ locales,
986
+ errors = NO_FIELD_ERRORS,
987
+ formatLocale
988
+ }) {
989
+ return /* @__PURE__ */ jsx11(
990
+ Repeater,
991
+ {
992
+ legend: getAdminLabel("section.projects"),
993
+ items: value,
994
+ onChange,
995
+ keyOf: (project) => project.id,
996
+ itemLabel: (project, index) => firstLocalized(project.title, locales) || `${getAdminLabel("item.project")} ${String(index + 1)}`,
997
+ createItem: () => ({
998
+ id: makeId(
999
+ "project",
1000
+ value.map((p) => p.id)
1001
+ ),
1002
+ title: {}
1003
+ }),
1004
+ addLabel: getAdminLabel("action.add"),
1005
+ removeLabel: getAdminLabel("action.remove"),
1006
+ moveUpLabel: getAdminLabel("action.moveUp"),
1007
+ moveDownLabel: getAdminLabel("action.moveDown"),
1008
+ emptyHint: getAdminLabel("empty.projects"),
1009
+ renderItem: (project, update, index) => {
1010
+ const at = `/projects/${String(index)}`;
1011
+ return /* @__PURE__ */ jsxs8(Fragment4, { children: [
1012
+ /* @__PURE__ */ jsx11(
1013
+ LocaleTabs,
1014
+ {
1015
+ label: getAdminLabel("field.project.title"),
1016
+ value: project.title,
1017
+ locales,
1018
+ onChange: (next) => {
1019
+ update({ ...project, title: next ?? {} });
1020
+ },
1021
+ required: true,
1022
+ pointer: `${at}/title`,
1023
+ errors,
1024
+ formatLocale
1025
+ }
1026
+ ),
1027
+ /* @__PURE__ */ jsx11(
1028
+ LocaleTabs,
1029
+ {
1030
+ label: getAdminLabel("field.project.description"),
1031
+ value: project.description,
1032
+ locales,
1033
+ onChange: (next) => {
1034
+ update({ ...project, description: next });
1035
+ },
1036
+ multiline: true,
1037
+ pointer: `${at}/description`,
1038
+ errors,
1039
+ formatLocale
1040
+ }
1041
+ ),
1042
+ /* @__PURE__ */ jsx11(
1043
+ TextField,
1044
+ {
1045
+ label: getAdminLabel("field.project.url"),
1046
+ type: "url",
1047
+ value: project.url ?? "",
1048
+ onChange: (url) => {
1049
+ update({ ...project, url: url || void 0 });
1050
+ },
1051
+ errors: errorsAt(errors, `${at}/url`)
1052
+ }
1053
+ ),
1054
+ /* @__PURE__ */ jsx11(
1055
+ TextField,
1056
+ {
1057
+ label: getAdminLabel("field.project.tags"),
1058
+ value: (project.tags ?? []).join(", "),
1059
+ onChange: (input) => {
1060
+ update({ ...project, tags: parseTags(input) });
1061
+ },
1062
+ hint: getAdminLabel("hint.tags"),
1063
+ errors: collectErrorsUnder(errors, `${at}/tags`)
1064
+ }
1065
+ ),
1066
+ /* @__PURE__ */ jsx11(
1067
+ TextField,
1068
+ {
1069
+ label: getAdminLabel("field.project.startDate"),
1070
+ type: "month",
1071
+ value: project.startDate ?? "",
1072
+ onChange: (startDate) => {
1073
+ update({ ...project, startDate: startDate || void 0 });
1074
+ },
1075
+ hint: getAdminLabel("hint.month"),
1076
+ errors: errorsAt(errors, `${at}/startDate`)
1077
+ }
1078
+ ),
1079
+ /* @__PURE__ */ jsx11(
1080
+ TextField,
1081
+ {
1082
+ label: getAdminLabel("field.project.endDate"),
1083
+ type: "month",
1084
+ value: project.endDate ?? "",
1085
+ onChange: (endDate) => {
1086
+ update({ ...project, endDate: endDate || void 0 });
1087
+ },
1088
+ hint: getAdminLabel("hint.month"),
1089
+ errors: errorsAt(errors, `${at}/endDate`)
1090
+ }
1091
+ ),
1092
+ /* @__PURE__ */ jsx11(
1093
+ CheckboxField,
1094
+ {
1095
+ label: getAdminLabel("field.project.highlighted"),
1096
+ checked: project.highlighted ?? false,
1097
+ onChange: (highlighted) => {
1098
+ update({ ...project, highlighted: highlighted || void 0 });
1099
+ }
1100
+ }
1101
+ )
1102
+ ] });
1103
+ }
1104
+ }
1105
+ );
1106
+ }
1107
+
1108
+ // src/admin/sections/SkillsForm.tsx
1109
+ import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1110
+ function SkillsForm({
1111
+ value,
1112
+ onChange,
1113
+ errors = NO_FIELD_ERRORS
1114
+ }) {
1115
+ return /* @__PURE__ */ jsx12(
1116
+ Repeater,
1117
+ {
1118
+ legend: getAdminLabel("section.skills"),
1119
+ items: value,
1120
+ onChange,
1121
+ keyOf: (skill) => skill.id,
1122
+ itemLabel: (skill, index) => skill.label || `${getAdminLabel("item.skill")} ${String(index + 1)}`,
1123
+ createItem: () => ({
1124
+ id: makeId(
1125
+ "skill",
1126
+ value.map((s) => s.id)
1127
+ ),
1128
+ label: ""
1129
+ }),
1130
+ addLabel: getAdminLabel("action.add"),
1131
+ removeLabel: getAdminLabel("action.remove"),
1132
+ moveUpLabel: getAdminLabel("action.moveUp"),
1133
+ moveDownLabel: getAdminLabel("action.moveDown"),
1134
+ emptyHint: getAdminLabel("empty.skills"),
1135
+ renderItem: (skill, update, index) => {
1136
+ const at = `/skills/${String(index)}`;
1137
+ return /* @__PURE__ */ jsxs9(Fragment5, { children: [
1138
+ /* @__PURE__ */ jsx12(
1139
+ TextField,
1140
+ {
1141
+ label: getAdminLabel("field.skill.label"),
1142
+ value: skill.label,
1143
+ onChange: (label) => {
1144
+ update({ ...skill, label });
1145
+ },
1146
+ required: true,
1147
+ errors: errorsAt(errors, `${at}/label`)
1148
+ }
1149
+ ),
1150
+ /* @__PURE__ */ jsx12(
1151
+ TextField,
1152
+ {
1153
+ label: getAdminLabel("field.skill.category"),
1154
+ value: skill.category ?? "",
1155
+ onChange: (category) => {
1156
+ update({ ...skill, category: category || void 0 });
1157
+ },
1158
+ errors: errorsAt(errors, `${at}/category`)
1159
+ }
1160
+ )
1161
+ ] });
1162
+ }
1163
+ }
1164
+ );
1165
+ }
1166
+
1167
+ // src/admin/sections/SettingsForm.tsx
1168
+ import styles9 from "../sections.module-ZVBKZHDE.module.css";
1169
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1170
+ function parseLocales(input) {
1171
+ return input.split(",").map((tag) => tag.trim()).filter((tag) => tag !== "");
1172
+ }
1173
+ function SettingsForm({
1174
+ value,
1175
+ onChange,
1176
+ errors = NO_FIELD_ERRORS,
1177
+ formatLocale
1178
+ }) {
1179
+ const format = formatLocale ?? ((locale) => locale);
1180
+ const localeOptions = value.availableLocales.map((locale) => ({
1181
+ value: locale,
1182
+ label: format(locale)
1183
+ }));
1184
+ const headingId = "admin-section-settings";
1185
+ return /* @__PURE__ */ jsxs10("section", { className: styles9.section, "aria-labelledby": headingId, children: [
1186
+ /* @__PURE__ */ jsx13("h2", { className: styles9.heading, id: headingId, children: getAdminLabel("section.settings") }),
1187
+ /* @__PURE__ */ jsx13(
1188
+ TextField,
1189
+ {
1190
+ label: getAdminLabel("field.settings.availableLocales"),
1191
+ value: value.availableLocales.join(", "),
1192
+ onChange: (input) => {
1193
+ onChange({ ...value, availableLocales: parseLocales(input) });
1194
+ },
1195
+ required: true,
1196
+ hint: getAdminLabel("hint.locales"),
1197
+ errors: collectErrorsUnder(errors, "/settings/availableLocales")
1198
+ }
1199
+ ),
1200
+ /* @__PURE__ */ jsx13(
1201
+ SelectField,
1202
+ {
1203
+ label: getAdminLabel("field.settings.defaultLocale"),
1204
+ value: value.defaultLocale,
1205
+ options: localeOptions,
1206
+ onChange: (defaultLocale) => {
1207
+ onChange({ ...value, defaultLocale });
1208
+ },
1209
+ required: true,
1210
+ errors: errorsAt(errors, "/settings/defaultLocale")
1211
+ }
1212
+ ),
1213
+ /* @__PURE__ */ jsx13(
1214
+ SelectField,
1215
+ {
1216
+ label: getAdminLabel("field.settings.fallbackLocale"),
1217
+ value: value.fallbackLocale ?? "",
1218
+ options: [{ value: "", label: getAdminLabel("option.none") }, ...localeOptions],
1219
+ onChange: (fallbackLocale) => {
1220
+ onChange({ ...value, fallbackLocale: fallbackLocale || void 0 });
1221
+ },
1222
+ errors: errorsAt(errors, "/settings/fallbackLocale")
1223
+ }
1224
+ ),
1225
+ /* @__PURE__ */ jsx13(
1226
+ TextField,
1227
+ {
1228
+ label: getAdminLabel("field.settings.theme"),
1229
+ value: value.theme ?? "",
1230
+ onChange: (theme) => {
1231
+ onChange({ ...value, theme: theme || void 0 });
1232
+ },
1233
+ errors: errorsAt(errors, "/settings/theme")
1234
+ }
1235
+ ),
1236
+ /* @__PURE__ */ jsx13(
1237
+ CheckboxField,
1238
+ {
1239
+ label: getAdminLabel("field.settings.showPoweredBy"),
1240
+ checked: value.showPoweredBy ?? true,
1241
+ onChange: (showPoweredBy) => {
1242
+ onChange({ ...value, showPoweredBy });
1243
+ }
1244
+ }
1245
+ ),
1246
+ /* @__PURE__ */ jsx13(
1247
+ CheckboxField,
1248
+ {
1249
+ label: getAdminLabel("field.settings.enableJsonLd"),
1250
+ checked: value.enableJsonLd ?? true,
1251
+ onChange: (enableJsonLd) => {
1252
+ onChange({ ...value, enableJsonLd });
1253
+ }
1254
+ }
1255
+ ),
1256
+ /* @__PURE__ */ jsx13(
1257
+ CheckboxField,
1258
+ {
1259
+ label: getAdminLabel("field.settings.enableApi"),
1260
+ checked: value.enableApi ?? true,
1261
+ onChange: (enableApi) => {
1262
+ onChange({ ...value, enableApi });
1263
+ }
1264
+ }
1265
+ ),
1266
+ /* @__PURE__ */ jsx13(
1267
+ CheckboxField,
1268
+ {
1269
+ label: getAdminLabel("field.settings.enableAnalytics"),
1270
+ checked: value.enableAnalytics ?? false,
1271
+ onChange: (enableAnalytics) => {
1272
+ onChange({ ...value, enableAnalytics });
1273
+ }
1274
+ }
1275
+ )
1276
+ ] });
1277
+ }
1278
+
1279
+ // src/admin/RawJsonEditor.tsx
1280
+ import { validate } from "@takuhon/core";
1281
+ import { useId as useId4, useState as useState2 } from "react";
1282
+ import styles10 from "../RawJsonEditor.module-NGGM3IBY.module.css";
1283
+ import { jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
1284
+ var MAX_SHOWN_PROBLEMS = 50;
1285
+ function RawJsonEditor({ value, onChange }) {
1286
+ const [text, setText] = useState2(() => JSON.stringify(value, null, 2));
1287
+ const [problems, setProblems] = useState2([]);
1288
+ const labelId = useId4();
1289
+ const errorId = useId4();
1290
+ const apply = (next) => {
1291
+ setText(next);
1292
+ let parsed;
1293
+ try {
1294
+ parsed = JSON.parse(next);
1295
+ } catch (error) {
1296
+ setProblems([error instanceof Error ? error.message : "Invalid JSON."]);
1297
+ return;
1298
+ }
1299
+ const result = validate(parsed);
1300
+ if (!result.ok) {
1301
+ setProblems(
1302
+ result.errors.slice(0, MAX_SHOWN_PROBLEMS).map((error) => `${error.pointer || "/"}: ${error.message}`)
1303
+ );
1304
+ return;
1305
+ }
1306
+ setProblems([]);
1307
+ onChange(result.data);
1308
+ };
1309
+ const hasProblems = problems.length > 0;
1310
+ return /* @__PURE__ */ jsxs11("section", { className: styles10.wrapper, "aria-labelledby": labelId, children: [
1311
+ /* @__PURE__ */ jsx14("h2", { className: styles10.heading, id: labelId, children: getAdminLabel("mode.advanced") }),
1312
+ /* @__PURE__ */ jsx14("p", { className: styles10.hint, children: getAdminLabel("advanced.hint") }),
1313
+ /* @__PURE__ */ jsx14(
1314
+ "textarea",
1315
+ {
1316
+ className: styles10.textarea,
1317
+ value: text,
1318
+ spellCheck: false,
1319
+ "aria-label": getAdminLabel("mode.advanced"),
1320
+ "aria-invalid": hasProblems || void 0,
1321
+ "aria-describedby": hasProblems ? errorId : void 0,
1322
+ onChange: (event) => {
1323
+ apply(event.target.value);
1324
+ }
1325
+ }
1326
+ ),
1327
+ hasProblems ? /* @__PURE__ */ jsxs11("div", { className: styles10.problems, id: errorId, role: "alert", children: [
1328
+ /* @__PURE__ */ jsx14("p", { className: styles10.problemsTitle, children: getAdminLabel("advanced.invalid") }),
1329
+ /* @__PURE__ */ jsx14("ul", { children: problems.map((problem, i) => /* @__PURE__ */ jsx14("li", { children: problem }, i)) })
1330
+ ] }) : null
1331
+ ] });
1332
+ }
1333
+
1334
+ // src/admin/AdminEditor.tsx
1335
+ import { validate as validate2 } from "@takuhon/core";
1336
+ import { useRef as useRef2, useState as useState3 } from "react";
1337
+ import styles11 from "../AdminEditor.module-ABYQXFN4.module.css";
1338
+ import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
1339
+ function AdminEditor({
1340
+ initialDocument,
1341
+ onSave,
1342
+ onReload,
1343
+ onExport,
1344
+ onImport,
1345
+ formatLocale
1346
+ }) {
1347
+ const [draft, setDraft] = useState3(initialDocument);
1348
+ const [mode, setMode] = useState3("form");
1349
+ const [errors, setErrors] = useState3(NO_FIELD_ERRORS);
1350
+ const [status, setStatus] = useState3(null);
1351
+ const [busy, setBusy] = useState3(false);
1352
+ const [loadGen, setLoadGen] = useState3(0);
1353
+ const intentRef = useRef2(0);
1354
+ const locales = draft.settings.availableLocales;
1355
+ const updateDraft = (next) => {
1356
+ intentRef.current += 1;
1357
+ setDraft(next);
1358
+ setErrors(NO_FIELD_ERRORS);
1359
+ setStatus(null);
1360
+ };
1361
+ const loadDocument = (next, message) => {
1362
+ intentRef.current += 1;
1363
+ setDraft(next);
1364
+ setErrors(NO_FIELD_ERRORS);
1365
+ setStatus(message);
1366
+ setLoadGen((generation) => generation + 1);
1367
+ };
1368
+ const handleSave = async () => {
1369
+ const result = validate2(draft);
1370
+ if (!result.ok) {
1371
+ setErrors(indexValidationErrors(result.errors));
1372
+ setStatus({ tone: "error", message: getAdminLabel("status.invalid") });
1373
+ return;
1374
+ }
1375
+ setErrors(NO_FIELD_ERRORS);
1376
+ intentRef.current += 1;
1377
+ const intent = intentRef.current;
1378
+ setBusy(true);
1379
+ setStatus({ tone: "info", message: getAdminLabel("status.saving") });
1380
+ try {
1381
+ const outcome = await onSave(result.data);
1382
+ if (intent !== intentRef.current) return;
1383
+ switch (outcome.status) {
1384
+ case "saved":
1385
+ setStatus({ tone: "success", message: getAdminLabel("status.saved") });
1386
+ break;
1387
+ case "conflict":
1388
+ setStatus({ tone: "error", message: getAdminLabel("status.conflict") });
1389
+ break;
1390
+ case "invalid":
1391
+ setErrors(indexErrors(outcome.errors));
1392
+ setStatus({ tone: "error", message: getAdminLabel("status.invalid") });
1393
+ break;
1394
+ case "error":
1395
+ setStatus({ tone: "error", message: outcome.message ?? getAdminLabel("status.error") });
1396
+ break;
1397
+ }
1398
+ } catch {
1399
+ if (intent === intentRef.current) {
1400
+ setStatus({ tone: "error", message: getAdminLabel("status.error") });
1401
+ }
1402
+ } finally {
1403
+ setBusy(false);
1404
+ }
1405
+ };
1406
+ const handleReload = async () => {
1407
+ if (!onReload) return;
1408
+ setBusy(true);
1409
+ setStatus({ tone: "info", message: getAdminLabel("status.loading") });
1410
+ try {
1411
+ const next = await onReload();
1412
+ loadDocument(next, null);
1413
+ } catch {
1414
+ setStatus({ tone: "error", message: getAdminLabel("status.error") });
1415
+ } finally {
1416
+ setBusy(false);
1417
+ }
1418
+ };
1419
+ const handleImport = async () => {
1420
+ if (!onImport) return;
1421
+ try {
1422
+ const raw = await onImport();
1423
+ if (raw === void 0) return;
1424
+ const result = validate2(raw);
1425
+ if (!result.ok) {
1426
+ setStatus({ tone: "error", message: getAdminLabel("status.importInvalid") });
1427
+ return;
1428
+ }
1429
+ loadDocument(result.data, {
1430
+ tone: "info",
1431
+ message: getAdminLabel("status.imported")
1432
+ });
1433
+ } catch {
1434
+ setStatus({ tone: "error", message: getAdminLabel("status.error") });
1435
+ }
1436
+ };
1437
+ const errorEntries = [...errors].flatMap(
1438
+ ([pointer, messages]) => messages.map((message) => ({ pointer, message }))
1439
+ );
1440
+ return /* @__PURE__ */ jsxs12("div", { className: styles11.editor, children: [
1441
+ /* @__PURE__ */ jsxs12("div", { className: styles11.toolbar, role: "toolbar", "aria-label": getAdminLabel("toolbar.label"), children: [
1442
+ /* @__PURE__ */ jsxs12("div", { className: styles11.modes, role: "group", "aria-label": getAdminLabel("mode.label"), children: [
1443
+ /* @__PURE__ */ jsx15(
1444
+ "button",
1445
+ {
1446
+ type: "button",
1447
+ className: `${styles11.modeButton} ${mode === "form" ? styles11.modeActive : ""}`,
1448
+ "aria-pressed": mode === "form",
1449
+ onClick: () => {
1450
+ setMode("form");
1451
+ },
1452
+ children: getAdminLabel("mode.form")
1453
+ }
1454
+ ),
1455
+ /* @__PURE__ */ jsx15(
1456
+ "button",
1457
+ {
1458
+ type: "button",
1459
+ className: `${styles11.modeButton} ${mode === "advanced" ? styles11.modeActive : ""}`,
1460
+ "aria-pressed": mode === "advanced",
1461
+ onClick: () => {
1462
+ setMode("advanced");
1463
+ },
1464
+ children: getAdminLabel("mode.advanced")
1465
+ }
1466
+ )
1467
+ ] }),
1468
+ /* @__PURE__ */ jsxs12("div", { className: styles11.actions, children: [
1469
+ /* @__PURE__ */ jsx15(
1470
+ "button",
1471
+ {
1472
+ type: "button",
1473
+ className: styles11.primary,
1474
+ disabled: busy,
1475
+ onClick: () => {
1476
+ void handleSave();
1477
+ },
1478
+ children: getAdminLabel("action.save")
1479
+ }
1480
+ ),
1481
+ onReload ? /* @__PURE__ */ jsx15(
1482
+ "button",
1483
+ {
1484
+ type: "button",
1485
+ className: styles11.secondary,
1486
+ disabled: busy,
1487
+ onClick: () => {
1488
+ void handleReload();
1489
+ },
1490
+ children: getAdminLabel("action.reload")
1491
+ }
1492
+ ) : null,
1493
+ onExport ? /* @__PURE__ */ jsx15(
1494
+ "button",
1495
+ {
1496
+ type: "button",
1497
+ className: styles11.secondary,
1498
+ onClick: () => {
1499
+ onExport(draft);
1500
+ },
1501
+ children: getAdminLabel("action.export")
1502
+ }
1503
+ ) : null,
1504
+ onImport ? /* @__PURE__ */ jsx15(
1505
+ "button",
1506
+ {
1507
+ type: "button",
1508
+ className: styles11.secondary,
1509
+ disabled: busy,
1510
+ onClick: () => {
1511
+ void handleImport();
1512
+ },
1513
+ children: getAdminLabel("action.import")
1514
+ }
1515
+ ) : null
1516
+ ] })
1517
+ ] }),
1518
+ /* @__PURE__ */ jsx15("p", { className: styles11.status, role: "status", "aria-live": "polite", "data-tone": status?.tone, children: status?.message ?? "" }),
1519
+ errorEntries.length > 0 ? /* @__PURE__ */ jsxs12("section", { className: styles11.summary, "aria-labelledby": "admin-error-summary", children: [
1520
+ /* @__PURE__ */ jsx15("h2", { className: styles11.summaryHeading, id: "admin-error-summary", children: getAdminLabel("status.fixSummary") }),
1521
+ /* @__PURE__ */ jsx15("ul", { children: errorEntries.map((entry, i) => /* @__PURE__ */ jsx15("li", { children: entry.pointer === "" ? entry.message : `${entry.pointer.replace(/^\//, "")}: ${entry.message}` }, i)) })
1522
+ ] }) : null,
1523
+ mode === "form" ? /* @__PURE__ */ jsxs12("div", { className: styles11.sections, children: [
1524
+ /* @__PURE__ */ jsx15(
1525
+ ProfileForm,
1526
+ {
1527
+ value: draft.profile,
1528
+ onChange: (profile) => {
1529
+ updateDraft({ ...draft, profile });
1530
+ },
1531
+ locales,
1532
+ errors,
1533
+ formatLocale
1534
+ }
1535
+ ),
1536
+ /* @__PURE__ */ jsx15(
1537
+ LinksForm,
1538
+ {
1539
+ value: draft.links,
1540
+ onChange: (links) => {
1541
+ updateDraft({ ...draft, links });
1542
+ },
1543
+ locales,
1544
+ errors,
1545
+ formatLocale
1546
+ }
1547
+ ),
1548
+ /* @__PURE__ */ jsx15(
1549
+ CareersForm,
1550
+ {
1551
+ value: draft.careers,
1552
+ onChange: (careers) => {
1553
+ updateDraft({ ...draft, careers });
1554
+ },
1555
+ locales,
1556
+ errors,
1557
+ formatLocale
1558
+ }
1559
+ ),
1560
+ /* @__PURE__ */ jsx15(
1561
+ ProjectsForm,
1562
+ {
1563
+ value: draft.projects,
1564
+ onChange: (projects) => {
1565
+ updateDraft({ ...draft, projects });
1566
+ },
1567
+ locales,
1568
+ errors,
1569
+ formatLocale
1570
+ }
1571
+ ),
1572
+ /* @__PURE__ */ jsx15(
1573
+ SkillsForm,
1574
+ {
1575
+ value: draft.skills,
1576
+ onChange: (skills) => {
1577
+ updateDraft({ ...draft, skills });
1578
+ },
1579
+ errors
1580
+ }
1581
+ ),
1582
+ /* @__PURE__ */ jsx15(
1583
+ SettingsForm,
1584
+ {
1585
+ value: draft.settings,
1586
+ onChange: (settings) => {
1587
+ updateDraft({ ...draft, settings });
1588
+ },
1589
+ errors,
1590
+ formatLocale
1591
+ }
1592
+ )
1593
+ ] }) : /* @__PURE__ */ jsx15(RawJsonEditor, { value: draft, onChange: updateDraft }, loadGen)
1594
+ ] });
1595
+ }
1596
+ export {
1597
+ AdminEditor,
1598
+ CareersForm,
1599
+ CheckboxField,
1600
+ Field,
1601
+ LinksForm,
1602
+ LocaleTabs,
1603
+ NO_FIELD_ERRORS,
1604
+ ProfileForm,
1605
+ ProjectsForm,
1606
+ RawJsonEditor,
1607
+ Repeater,
1608
+ SelectField,
1609
+ SettingsForm,
1610
+ SkillsForm,
1611
+ TextAreaField,
1612
+ TextField,
1613
+ canonicalPointer,
1614
+ collectErrorsUnder,
1615
+ errorsAt,
1616
+ getAdminLabel,
1617
+ hasErrorsUnder,
1618
+ indexErrors,
1619
+ indexValidationErrors
1620
+ };
1621
+ //# sourceMappingURL=index.js.map