@takuhon/ui 0.24.0 → 0.25.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.
@@ -214,6 +214,19 @@ var EN = {
214
214
  "section.projects": "Projects",
215
215
  "section.skills": "Skills",
216
216
  "section.settings": "Settings",
217
+ "section.education": "Education",
218
+ "section.certifications": "Certifications",
219
+ "section.publications": "Publications",
220
+ "section.honors": "Honors & awards",
221
+ "section.volunteering": "Volunteering",
222
+ "section.memberships": "Memberships",
223
+ "section.languages": "Languages",
224
+ "section.courses": "Courses",
225
+ "section.patents": "Patents",
226
+ "section.testScores": "Test scores",
227
+ "section.recommendations": "Recommendations",
228
+ "section.contact": "Contact",
229
+ "section.meta": "Metadata",
217
230
  "field.displayName": "Display name",
218
231
  "field.tagline": "Tagline",
219
232
  "field.bio": "Bio",
@@ -683,154 +696,6 @@ function Repeater({
683
696
  ] });
684
697
  }
685
698
 
686
- // src/admin/sections/ProfileForm.tsx
687
- import styles10 from "../sections.module-7MUK6I3V.module.css";
688
- import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
689
- var POINTER = "/profile";
690
- function isEmptyRecord(record) {
691
- return !record || Object.keys(record).length === 0;
692
- }
693
- function ProfileForm({
694
- value,
695
- onChange,
696
- locales,
697
- errors = NO_FIELD_ERRORS,
698
- formatLocale,
699
- uploadAsset
700
- }) {
701
- const updateAvatar = (patch) => {
702
- const merged = { url: "", ...value.avatar, ...patch };
703
- const keep = merged.url !== "" || !isEmptyRecord(merged.alt);
704
- onChange({ ...value, avatar: keep ? merged : void 0 });
705
- };
706
- const updateLocation = (patch) => {
707
- const merged = { ...value.location, ...patch };
708
- const empty = !merged.country && !merged.region && isEmptyRecord(merged.locality) && isEmptyRecord(merged.display);
709
- onChange({ ...value, location: empty ? void 0 : merged });
710
- };
711
- const headingId = "admin-section-profile";
712
- return /* @__PURE__ */ jsxs7("section", { className: styles10.section, "aria-labelledby": headingId, children: [
713
- /* @__PURE__ */ jsx10("h2", { className: styles10.heading, id: headingId, children: getAdminLabel("section.profile") }),
714
- /* @__PURE__ */ jsx10(
715
- LocaleTabs,
716
- {
717
- label: getAdminLabel("field.displayName"),
718
- value: value.displayName,
719
- locales,
720
- onChange: (next) => {
721
- onChange({ ...value, displayName: next ?? {} });
722
- },
723
- required: true,
724
- pointer: `${POINTER}/displayName`,
725
- errors,
726
- formatLocale
727
- }
728
- ),
729
- /* @__PURE__ */ jsx10(
730
- LocaleTabs,
731
- {
732
- label: getAdminLabel("field.tagline"),
733
- value: value.tagline,
734
- locales,
735
- onChange: (next) => {
736
- onChange({ ...value, tagline: next });
737
- },
738
- pointer: `${POINTER}/tagline`,
739
- errors,
740
- formatLocale
741
- }
742
- ),
743
- /* @__PURE__ */ jsx10("h3", { className: styles10.subheading, children: getAdminLabel("section.about") }),
744
- /* @__PURE__ */ jsx10(
745
- LocaleTabs,
746
- {
747
- label: getAdminLabel("field.bio"),
748
- value: value.bio,
749
- locales,
750
- onChange: (next) => {
751
- onChange({ ...value, bio: next });
752
- },
753
- multiline: true,
754
- pointer: `${POINTER}/bio`,
755
- errors,
756
- formatLocale
757
- }
758
- ),
759
- /* @__PURE__ */ jsx10(
760
- ImageField,
761
- {
762
- label: getAdminLabel("field.avatarUrl"),
763
- value: value.avatar?.url ?? "",
764
- onChange: (url) => {
765
- updateAvatar({ url });
766
- },
767
- hint: getAdminLabel(uploadAsset ? "hint.avatarUpload" : "hint.avatarNoUpload"),
768
- errors: errorsAt(errors, `${POINTER}/avatar/url`),
769
- uploadAsset
770
- }
771
- ),
772
- /* @__PURE__ */ jsx10(
773
- GravatarField,
774
- {
775
- onApply: (url) => {
776
- updateAvatar({ url });
777
- }
778
- }
779
- ),
780
- /* @__PURE__ */ jsx10(
781
- LocaleTabs,
782
- {
783
- label: getAdminLabel("field.avatarAlt"),
784
- value: value.avatar?.alt,
785
- locales,
786
- onChange: (next) => {
787
- updateAvatar({ alt: next });
788
- },
789
- pointer: `${POINTER}/avatar/alt`,
790
- errors,
791
- formatLocale
792
- }
793
- ),
794
- /* @__PURE__ */ jsx10(
795
- TextField,
796
- {
797
- label: getAdminLabel("field.location.country"),
798
- value: value.location?.country ?? "",
799
- onChange: (country) => {
800
- updateLocation({ country: country || void 0 });
801
- },
802
- hint: getAdminLabel("hint.country"),
803
- errors: errorsAt(errors, `${POINTER}/location/country`)
804
- }
805
- ),
806
- /* @__PURE__ */ jsx10(
807
- TextField,
808
- {
809
- label: getAdminLabel("field.location.region"),
810
- value: value.location?.region ?? "",
811
- onChange: (region) => {
812
- updateLocation({ region: region || void 0 });
813
- },
814
- errors: errorsAt(errors, `${POINTER}/location/region`)
815
- }
816
- ),
817
- /* @__PURE__ */ jsx10(
818
- LocaleTabs,
819
- {
820
- label: getAdminLabel("field.location.locality"),
821
- value: value.location?.locality,
822
- locales,
823
- onChange: (next) => {
824
- updateLocation({ locality: next });
825
- },
826
- pointer: `${POINTER}/location/locality`,
827
- errors,
828
- formatLocale
829
- }
830
- )
831
- ] });
832
- }
833
-
834
699
  // src/admin/ids.ts
835
700
  function makeId(prefix, taken) {
836
701
  const safe = prefix.toLowerCase().replace(/[^a-z0-9-]/g, "") || "item";
@@ -840,151 +705,6 @@ function makeId(prefix, taken) {
840
705
  return `${safe}-${String(n)}`;
841
706
  }
842
707
 
843
- // src/admin/sections/LinksForm.tsx
844
- import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
845
- var LINK_TYPES = [
846
- "website",
847
- "blog",
848
- "github",
849
- "gitlab",
850
- "linkedin",
851
- "x",
852
- "mastodon",
853
- "bluesky",
854
- "instagram",
855
- "youtube",
856
- "threads",
857
- "facebook",
858
- "email",
859
- "rss",
860
- "custom"
861
- ];
862
- function retypeLink(link, type) {
863
- if (type === "custom") {
864
- return {
865
- id: link.id,
866
- url: link.url,
867
- label: link.label,
868
- featured: link.featured,
869
- order: link.order,
870
- type: "custom",
871
- iconUrl: link.iconUrl ?? ""
872
- };
873
- }
874
- const builtin = {
875
- id: link.id,
876
- url: link.url,
877
- label: link.label,
878
- featured: link.featured,
879
- order: link.order,
880
- type
881
- };
882
- if (link.iconUrl) builtin.iconUrl = link.iconUrl;
883
- return builtin;
884
- }
885
- function setIconUrl(link, iconUrl) {
886
- if (link.type === "custom") return { ...link, iconUrl };
887
- return { ...link, iconUrl: iconUrl || void 0 };
888
- }
889
- function LinksForm({
890
- value,
891
- onChange,
892
- locales,
893
- errors = NO_FIELD_ERRORS,
894
- formatLocale
895
- }) {
896
- return /* @__PURE__ */ jsx11(
897
- Repeater,
898
- {
899
- legend: getAdminLabel("section.links"),
900
- items: value,
901
- onChange,
902
- keyOf: (link) => link.id,
903
- itemLabel: (link, index) => link.url || `${getAdminLabel("item.link")} ${String(index + 1)}`,
904
- createItem: () => ({
905
- id: makeId(
906
- "link",
907
- value.map((l) => l.id)
908
- ),
909
- type: "website",
910
- url: ""
911
- }),
912
- addLabel: getAdminLabel("action.add"),
913
- removeLabel: getAdminLabel("action.remove"),
914
- moveUpLabel: getAdminLabel("action.moveUp"),
915
- moveDownLabel: getAdminLabel("action.moveDown"),
916
- emptyHint: getAdminLabel("empty.links"),
917
- renderItem: (link, update, index) => {
918
- const at = `/links/${String(index)}`;
919
- return /* @__PURE__ */ jsxs8(Fragment4, { children: [
920
- /* @__PURE__ */ jsx11(
921
- SelectField,
922
- {
923
- label: getAdminLabel("field.link.type"),
924
- value: link.type,
925
- options: LINK_TYPES.map((t) => ({ value: t, label: t })),
926
- onChange: (t) => {
927
- update(retypeLink(link, t));
928
- },
929
- errors: errorsAt(errors, `${at}/type`)
930
- }
931
- ),
932
- /* @__PURE__ */ jsx11(
933
- TextField,
934
- {
935
- label: getAdminLabel("field.link.url"),
936
- type: "url",
937
- value: link.url,
938
- onChange: (url) => {
939
- update({ ...link, url });
940
- },
941
- required: true,
942
- errors: errorsAt(errors, `${at}/url`)
943
- }
944
- ),
945
- /* @__PURE__ */ jsx11(
946
- LocaleTabs,
947
- {
948
- label: getAdminLabel("field.link.label"),
949
- value: link.label,
950
- locales,
951
- onChange: (next) => {
952
- update({ ...link, label: next });
953
- },
954
- pointer: `${at}/label`,
955
- errors,
956
- formatLocale
957
- }
958
- ),
959
- /* @__PURE__ */ jsx11(
960
- TextField,
961
- {
962
- label: getAdminLabel("field.link.iconUrl"),
963
- type: "url",
964
- value: link.iconUrl ?? "",
965
- onChange: (iconUrl) => {
966
- update(setIconUrl(link, iconUrl));
967
- },
968
- required: link.type === "custom",
969
- errors: errorsAt(errors, `${at}/iconUrl`)
970
- }
971
- ),
972
- /* @__PURE__ */ jsx11(
973
- CheckboxField,
974
- {
975
- label: getAdminLabel("field.link.featured"),
976
- checked: link.featured ?? false,
977
- onChange: (featured) => {
978
- update({ ...link, featured: featured || void 0 });
979
- }
980
- }
981
- )
982
- ] });
983
- }
984
- }
985
- );
986
- }
987
-
988
708
  // src/admin/localized.ts
989
709
  function firstLocalized(record, locales) {
990
710
  if (!record) return "";
@@ -995,334 +715,453 @@ function firstLocalized(record, locales) {
995
715
  return Object.values(record).find((value) => value !== "") ?? "";
996
716
  }
997
717
 
998
- // src/admin/sections/CareersForm.tsx
999
- import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1000
- function CareersForm({
1001
- value,
1002
- onChange,
1003
- locales,
1004
- errors = NO_FIELD_ERRORS,
1005
- formatLocale
1006
- }) {
1007
- return /* @__PURE__ */ jsx12(
1008
- Repeater,
1009
- {
1010
- legend: getAdminLabel("section.careers"),
1011
- items: value,
1012
- onChange,
1013
- keyOf: (career) => career.id,
1014
- itemLabel: (career, index) => firstLocalized(career.organization, locales) || `${getAdminLabel("item.career")} ${String(index + 1)}`,
1015
- createItem: () => ({
1016
- id: makeId(
1017
- "career",
1018
- value.map((c) => c.id)
1019
- ),
1020
- organization: {},
1021
- role: {},
1022
- startDate: ""
1023
- }),
1024
- addLabel: getAdminLabel("action.add"),
1025
- removeLabel: getAdminLabel("action.remove"),
1026
- moveUpLabel: getAdminLabel("action.moveUp"),
1027
- moveDownLabel: getAdminLabel("action.moveDown"),
1028
- emptyHint: getAdminLabel("empty.careers"),
1029
- renderItem: (career, update, index) => {
1030
- const at = `/careers/${String(index)}`;
1031
- return /* @__PURE__ */ jsxs9(Fragment5, { children: [
1032
- /* @__PURE__ */ jsx12(
1033
- LocaleTabs,
1034
- {
1035
- label: getAdminLabel("field.career.organization"),
1036
- value: career.organization,
1037
- locales,
1038
- onChange: (next) => {
1039
- update({ ...career, organization: next ?? {} });
1040
- },
1041
- required: true,
1042
- pointer: `${at}/organization`,
1043
- errors,
1044
- formatLocale
1045
- }
1046
- ),
1047
- /* @__PURE__ */ jsx12(
1048
- LocaleTabs,
1049
- {
1050
- label: getAdminLabel("field.career.role"),
1051
- value: career.role,
1052
- locales,
1053
- onChange: (next) => {
1054
- update({ ...career, role: next ?? {} });
1055
- },
1056
- required: true,
1057
- pointer: `${at}/role`,
1058
- errors,
1059
- formatLocale
1060
- }
1061
- ),
1062
- /* @__PURE__ */ jsx12(
1063
- TextField,
1064
- {
1065
- label: getAdminLabel("field.career.startDate"),
1066
- type: "month",
1067
- value: career.startDate,
1068
- onChange: (startDate) => {
1069
- update({ ...career, startDate });
1070
- },
1071
- required: true,
1072
- hint: getAdminLabel("hint.month"),
1073
- errors: errorsAt(errors, `${at}/startDate`)
1074
- }
1075
- ),
1076
- /* @__PURE__ */ jsx12(
1077
- TextField,
1078
- {
1079
- label: getAdminLabel("field.career.endDate"),
1080
- type: "month",
1081
- value: career.endDate ?? "",
1082
- onChange: (endDate) => {
1083
- update({ ...career, endDate: endDate || void 0 });
1084
- },
1085
- hint: getAdminLabel("hint.month"),
1086
- errors: errorsAt(errors, `${at}/endDate`)
1087
- }
1088
- ),
1089
- /* @__PURE__ */ jsx12(
1090
- CheckboxField,
1091
- {
1092
- label: getAdminLabel("field.career.isCurrent"),
1093
- checked: career.isCurrent ?? false,
1094
- onChange: (isCurrent) => {
1095
- update({ ...career, isCurrent: isCurrent || void 0 });
1096
- }
1097
- }
1098
- ),
1099
- /* @__PURE__ */ jsx12(
1100
- LocaleTabs,
1101
- {
1102
- label: getAdminLabel("field.career.description"),
1103
- value: career.description,
1104
- locales,
1105
- onChange: (next) => {
1106
- update({ ...career, description: next });
1107
- },
1108
- multiline: true,
1109
- pointer: `${at}/description`,
1110
- errors,
1111
- formatLocale
1112
- }
1113
- ),
1114
- /* @__PURE__ */ jsx12(
1115
- TextField,
1116
- {
1117
- label: getAdminLabel("field.career.url"),
1118
- type: "url",
1119
- value: career.url ?? "",
1120
- onChange: (url) => {
1121
- update({ ...career, url: url || void 0 });
1122
- },
1123
- errors: errorsAt(errors, `${at}/url`)
1124
- }
1125
- )
1126
- ] });
1127
- }
718
+ // src/admin/schema-form/field-classification.ts
719
+ var REF_PREFIX = "#/$defs/";
720
+ function refName(node) {
721
+ const ref = node.$ref;
722
+ if (typeof ref !== "string" || !ref.startsWith(REF_PREFIX)) return void 0;
723
+ return ref.slice(REF_PREFIX.length);
724
+ }
725
+ function deref(root, node) {
726
+ let current = node;
727
+ let nullable = false;
728
+ const variants = current.anyOf;
729
+ if (variants) {
730
+ const nonNull = variants.filter((variant) => variant.type !== "null");
731
+ const [only] = nonNull;
732
+ if (only && nonNull.length === 1 && nonNull.length < variants.length) {
733
+ nullable = true;
734
+ current = only;
1128
735
  }
1129
- );
736
+ }
737
+ const name = refName(current);
738
+ if (name === void 0) return { node: current, nullable };
739
+ const target = root.$defs?.[name];
740
+ if (!target) return { node: current, nullable };
741
+ return { node: target, ref: name, nullable };
742
+ }
743
+ var REF_WIDGET = {
744
+ LocalizedTitle: { widget: "localizedTitle" },
745
+ LocalizedBody: { widget: "localizedBody" },
746
+ YearMonth: { widget: "month" },
747
+ Slug: { widget: "slug" },
748
+ LocaleTag: { widget: "localeTag" }
749
+ };
750
+ function classifyNode(root, node) {
751
+ const { node: resolved, ref } = deref(root, node);
752
+ if (ref !== void 0) {
753
+ const known = REF_WIDGET[ref];
754
+ if (known) return known;
755
+ }
756
+ if (Array.isArray(resolved.enum)) {
757
+ return { widget: "select", options: resolved.enum.map((value) => String(value)) };
758
+ }
759
+ const type = typeof resolved.type === "string" ? resolved.type : void 0;
760
+ if (type === "string") {
761
+ switch (resolved.format) {
762
+ case "email":
763
+ return { widget: "email" };
764
+ case "uri":
765
+ return { widget: "url" };
766
+ case "date":
767
+ return { widget: "date" };
768
+ case "date-time":
769
+ return { widget: "datetime" };
770
+ default:
771
+ return resolved.maxLength === void 0 ? { widget: "text" } : { widget: "text", maxLength: resolved.maxLength };
772
+ }
773
+ }
774
+ if (type === "boolean") {
775
+ return typeof resolved.default === "boolean" ? { widget: "checkbox", default: resolved.default } : { widget: "checkbox" };
776
+ }
777
+ if (type === "integer" || type === "number") {
778
+ return resolved.minimum === void 0 ? { widget: "integer" } : { widget: "integer", minimum: resolved.minimum };
779
+ }
780
+ if (type === "array") {
781
+ const item = resolved.items ? classifyNode(root, resolved.items) : { widget: "unsupported" };
782
+ return { widget: "array", item };
783
+ }
784
+ if (type === "object") {
785
+ return { widget: "object", fields: objectEntries(root, resolved) };
786
+ }
787
+ return { widget: "unsupported" };
788
+ }
789
+ function objectEntries(root, objectSchema) {
790
+ const properties = objectSchema.properties ?? {};
791
+ const required = new Set(objectSchema.required ?? []);
792
+ return Object.entries(properties).map(([name, propNode]) => ({
793
+ name,
794
+ required: required.has(name),
795
+ nullable: deref(root, propNode).nullable,
796
+ kind: classifyNode(root, propNode)
797
+ }));
798
+ }
799
+ function sectionFieldKind(root, section) {
800
+ const node = root.properties?.[section];
801
+ if (!node) return { widget: "unsupported" };
802
+ return classifyNode(root, node);
1130
803
  }
1131
804
 
1132
- // src/admin/sections/ProjectsForm.tsx
1133
- import { Fragment as Fragment6, jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1134
- function parseTags(input) {
1135
- const tags = input.split(",").map((tag) => tag.trim()).filter((tag) => tag !== "");
1136
- return tags.length > 0 ? tags : void 0;
805
+ // src/admin/schema-form/field-registry.ts
806
+ var EMPTY_REGISTRY = {};
807
+ var ACRONYMS = /* @__PURE__ */ new Set(["url", "id", "doi"]);
808
+ function humanize(name) {
809
+ const words = name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(" ");
810
+ return words.map(
811
+ (word, index) => ACRONYMS.has(word) ? word.toUpperCase() : index === 0 ? word.charAt(0).toUpperCase() + word.slice(1) : word
812
+ ).join(" ");
1137
813
  }
1138
- function ProjectsForm({
1139
- value,
1140
- onChange,
1141
- locales,
1142
- errors = NO_FIELD_ERRORS,
1143
- formatLocale
1144
- }) {
1145
- return /* @__PURE__ */ jsx13(
1146
- Repeater,
1147
- {
1148
- legend: getAdminLabel("section.projects"),
1149
- items: value,
1150
- onChange,
1151
- keyOf: (project) => project.id,
1152
- itemLabel: (project, index) => firstLocalized(project.title, locales) || `${getAdminLabel("item.project")} ${String(index + 1)}`,
1153
- createItem: () => ({
1154
- id: makeId(
1155
- "project",
1156
- value.map((p) => p.id)
1157
- ),
1158
- title: {}
1159
- }),
1160
- addLabel: getAdminLabel("action.add"),
1161
- removeLabel: getAdminLabel("action.remove"),
1162
- moveUpLabel: getAdminLabel("action.moveUp"),
1163
- moveDownLabel: getAdminLabel("action.moveDown"),
1164
- emptyHint: getAdminLabel("empty.projects"),
1165
- renderItem: (project, update, index) => {
1166
- const at = `/projects/${String(index)}`;
1167
- return /* @__PURE__ */ jsxs10(Fragment6, { children: [
1168
- /* @__PURE__ */ jsx13(
1169
- LocaleTabs,
1170
- {
1171
- label: getAdminLabel("field.project.title"),
1172
- value: project.title,
1173
- locales,
1174
- onChange: (next) => {
1175
- update({ ...project, title: next ?? {} });
1176
- },
1177
- required: true,
1178
- pointer: `${at}/title`,
1179
- errors,
1180
- formatLocale
1181
- }
1182
- ),
1183
- /* @__PURE__ */ jsx13(
1184
- LocaleTabs,
1185
- {
1186
- label: getAdminLabel("field.project.description"),
1187
- value: project.description,
1188
- locales,
1189
- onChange: (next) => {
1190
- update({ ...project, description: next });
1191
- },
1192
- multiline: true,
1193
- pointer: `${at}/description`,
1194
- errors,
1195
- formatLocale
1196
- }
1197
- ),
1198
- /* @__PURE__ */ jsx13(
1199
- TextField,
1200
- {
1201
- label: getAdminLabel("field.project.url"),
1202
- type: "url",
1203
- value: project.url ?? "",
1204
- onChange: (url) => {
1205
- update({ ...project, url: url || void 0 });
1206
- },
1207
- errors: errorsAt(errors, `${at}/url`)
1208
- }
1209
- ),
1210
- /* @__PURE__ */ jsx13(
1211
- TextField,
1212
- {
1213
- label: getAdminLabel("field.project.tags"),
1214
- value: (project.tags ?? []).join(", "),
1215
- onChange: (input) => {
1216
- update({ ...project, tags: parseTags(input) });
1217
- },
1218
- hint: getAdminLabel("hint.tags"),
1219
- errors: collectErrorsUnder(errors, `${at}/tags`)
1220
- }
1221
- ),
1222
- /* @__PURE__ */ jsx13(
1223
- TextField,
1224
- {
1225
- label: getAdminLabel("field.project.startDate"),
1226
- type: "month",
1227
- value: project.startDate ?? "",
1228
- onChange: (startDate) => {
1229
- update({ ...project, startDate: startDate || void 0 });
1230
- },
1231
- hint: getAdminLabel("hint.month"),
1232
- errors: errorsAt(errors, `${at}/startDate`)
1233
- }
1234
- ),
1235
- /* @__PURE__ */ jsx13(
1236
- TextField,
1237
- {
1238
- label: getAdminLabel("field.project.endDate"),
1239
- type: "month",
1240
- value: project.endDate ?? "",
1241
- onChange: (endDate) => {
1242
- update({ ...project, endDate: endDate || void 0 });
1243
- },
1244
- hint: getAdminLabel("hint.month"),
1245
- errors: errorsAt(errors, `${at}/endDate`)
1246
- }
1247
- ),
1248
- /* @__PURE__ */ jsx13(
1249
- CheckboxField,
1250
- {
1251
- label: getAdminLabel("field.project.highlighted"),
1252
- checked: project.highlighted ?? false,
1253
- onChange: (highlighted) => {
1254
- update({ ...project, highlighted: highlighted || void 0 });
1255
- }
1256
- }
1257
- )
1258
- ] });
814
+ function hintAt(registry, path) {
815
+ return registry[path] ?? {};
816
+ }
817
+
818
+ // src/admin/schema-form/SchemaForm.tsx
819
+ import { Fragment as Fragment4, jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
820
+ function asRecord(value) {
821
+ return value && typeof value === "object" ? value : {};
822
+ }
823
+ function asLocalized(value) {
824
+ return value && typeof value === "object" ? value : void 0;
825
+ }
826
+ function asString(value) {
827
+ return typeof value === "string" ? value : "";
828
+ }
829
+ function withKey(obj, name, next) {
830
+ return { ...obj, [name]: next };
831
+ }
832
+ function idOf(item) {
833
+ const id = asRecord(item).id;
834
+ return typeof id === "string" ? id : void 0;
835
+ }
836
+ function lastSegment(path) {
837
+ const parts = path.split(".");
838
+ return parts[parts.length - 1] ?? path;
839
+ }
840
+ function blankValue(kind, takenIds, path) {
841
+ switch (kind.widget) {
842
+ case "object": {
843
+ const obj = {};
844
+ for (const field of kind.fields) {
845
+ if (!field.required) continue;
846
+ obj[field.name] = field.kind.widget === "slug" ? makeId(lastSegment(path), takenIds) : blankValue(field.kind, [], `${path}.${field.name}`);
1259
847
  }
848
+ return obj;
1260
849
  }
1261
- );
850
+ case "checkbox":
851
+ return false;
852
+ case "integer":
853
+ return 0;
854
+ case "localizedTitle":
855
+ case "localizedBody":
856
+ return {};
857
+ case "select":
858
+ return kind.options[0] ?? "";
859
+ case "array":
860
+ return [];
861
+ default:
862
+ return "";
863
+ }
1262
864
  }
1263
-
1264
- // src/admin/sections/SkillsForm.tsx
1265
- import { Fragment as Fragment7, jsx as jsx14, jsxs as jsxs11 } from "react/jsx-runtime";
1266
- function SkillsForm({
1267
- value,
1268
- onChange,
1269
- errors = NO_FIELD_ERRORS
1270
- }) {
1271
- return /* @__PURE__ */ jsx14(
865
+ function captionOf(item, itemKind, locales) {
866
+ if (itemKind.widget !== "object") return asString(item);
867
+ const obj = asRecord(item);
868
+ for (const field of itemKind.fields) {
869
+ if (field.kind.widget === "localizedTitle") {
870
+ const caption = firstLocalized(asLocalized(obj[field.name]), locales);
871
+ if (caption) return caption;
872
+ }
873
+ }
874
+ for (const field of itemKind.fields) {
875
+ if (field.kind.widget === "text" || field.kind.widget === "select") {
876
+ const value = asString(obj[field.name]);
877
+ if (value) return value;
878
+ }
879
+ }
880
+ return "";
881
+ }
882
+ function renderScalar(kind, value, onChange, ctx) {
883
+ const errors = errorsAt(ctx.errors, ctx.pointer);
884
+ switch (kind.widget) {
885
+ case "localizedTitle":
886
+ case "localizedBody":
887
+ return /* @__PURE__ */ jsx10(
888
+ LocaleTabs,
889
+ {
890
+ label: ctx.label,
891
+ value: asLocalized(value),
892
+ locales: ctx.locales,
893
+ onChange: (next) => {
894
+ onChange(next);
895
+ },
896
+ multiline: kind.widget === "localizedBody" || ctx.multiline,
897
+ required: ctx.required,
898
+ pointer: ctx.pointer,
899
+ errors: ctx.errors,
900
+ formatLocale: ctx.formatLocale
901
+ }
902
+ );
903
+ case "checkbox": {
904
+ const hasDefault = kind.default !== void 0;
905
+ const checked = value === void 0 ? kind.default ?? false : value === true;
906
+ return /* @__PURE__ */ jsx10(
907
+ CheckboxField,
908
+ {
909
+ label: ctx.label,
910
+ checked,
911
+ onChange: (next) => {
912
+ onChange(hasDefault ? next : next || void 0);
913
+ }
914
+ }
915
+ );
916
+ }
917
+ case "select":
918
+ case "localeTag": {
919
+ const localeOptions = ctx.locales.map((locale) => ({
920
+ value: locale,
921
+ label: ctx.formatLocale ? ctx.formatLocale(locale) : locale
922
+ }));
923
+ const options = kind.widget === "select" ? kind.options.map((option) => ({ value: option, label: option })) : ctx.required ? localeOptions : [{ value: "", label: getAdminLabel("option.none") }, ...localeOptions];
924
+ return /* @__PURE__ */ jsx10(
925
+ SelectField,
926
+ {
927
+ label: ctx.label,
928
+ value: asString(value),
929
+ options,
930
+ onChange: (next) => {
931
+ onChange(next || void 0);
932
+ },
933
+ required: ctx.required,
934
+ errors
935
+ }
936
+ );
937
+ }
938
+ case "integer":
939
+ return /* @__PURE__ */ jsx10(
940
+ TextField,
941
+ {
942
+ label: ctx.label,
943
+ type: "text",
944
+ inputMode: "numeric",
945
+ value: typeof value === "number" ? String(value) : "",
946
+ onChange: (next) => {
947
+ onChange(next === "" ? void 0 : Number(next));
948
+ },
949
+ required: ctx.required,
950
+ hint: ctx.hint,
951
+ errors
952
+ }
953
+ );
954
+ default: {
955
+ const type = kind.widget === "url" ? "url" : kind.widget === "email" ? "email" : kind.widget === "month" ? "month" : "text";
956
+ const hint = ctx.hint ?? (kind.widget === "month" ? getAdminLabel("hint.month") : void 0);
957
+ return /* @__PURE__ */ jsx10(
958
+ TextField,
959
+ {
960
+ label: ctx.label,
961
+ type,
962
+ value: asString(value),
963
+ onChange: (next) => {
964
+ onChange(next || void 0);
965
+ },
966
+ required: ctx.required,
967
+ hint,
968
+ errors
969
+ }
970
+ );
971
+ }
972
+ }
973
+ }
974
+ function renderEntry(entry, obj, onChange, ctx) {
975
+ const path = ctx.path ? `${ctx.path}.${entry.name}` : entry.name;
976
+ const hint = hintAt(ctx.registry, path);
977
+ if (hint.hidden) return null;
978
+ const childCtx = {
979
+ ...ctx,
980
+ pointer: `${ctx.pointer}/${entry.name}`,
981
+ path,
982
+ label: hint.label ?? humanize(entry.name),
983
+ hint: hint.hint,
984
+ required: entry.required,
985
+ multiline: hint.multiline ?? false
986
+ };
987
+ const update = (next) => {
988
+ onChange(withKey(obj, entry.name, next));
989
+ };
990
+ if (hint.render) {
991
+ const customCtx = {
992
+ value: obj[entry.name],
993
+ onChange: update,
994
+ pointer: childCtx.pointer,
995
+ path: childCtx.path,
996
+ label: childCtx.label,
997
+ required: childCtx.required,
998
+ locales: childCtx.locales,
999
+ errors: childCtx.errors,
1000
+ formatLocale: childCtx.formatLocale,
1001
+ uploadAsset: childCtx.uploadAsset,
1002
+ document: childCtx.document
1003
+ };
1004
+ return /* @__PURE__ */ jsx10("div", { children: hint.render(customCtx) }, entry.name);
1005
+ }
1006
+ return /* @__PURE__ */ jsx10("div", { children: entry.kind.widget === "object" ? (
1007
+ // Nested objects (location, the activity providers, meta.privacy) become
1008
+ // their own labelled fieldset, so same-named children (github.username
1009
+ // vs wakatime.username) stay distinguishable.
1010
+ /* @__PURE__ */ jsxs7("fieldset", { children: [
1011
+ /* @__PURE__ */ jsx10("legend", { children: childCtx.label }),
1012
+ renderObjectFields(entry.kind, asRecord(obj[entry.name]), update, childCtx)
1013
+ ] })
1014
+ ) : entry.kind.widget === "array" ? renderArray(entry.kind, obj[entry.name], update, childCtx) : renderScalar(entry.kind, obj[entry.name], update, childCtx) }, entry.name);
1015
+ }
1016
+ function renderObjectFields(kind, obj, onChange, ctx) {
1017
+ const update = (next) => {
1018
+ onChange(next);
1019
+ };
1020
+ return kind.fields.map((entry) => renderEntry(entry, obj, update, ctx));
1021
+ }
1022
+ function renderArray(kind, value, onChange, ctx) {
1023
+ const items = Array.isArray(value) ? value : [];
1024
+ const itemKind = kind.item;
1025
+ const takenIds = items.map(idOf).filter((id) => id !== void 0);
1026
+ return /* @__PURE__ */ jsx10(
1272
1027
  Repeater,
1273
1028
  {
1274
- legend: getAdminLabel("section.skills"),
1275
- items: value,
1276
- onChange,
1277
- keyOf: (skill) => skill.id,
1278
- itemLabel: (skill, index) => skill.label || `${getAdminLabel("item.skill")} ${String(index + 1)}`,
1279
- createItem: () => ({
1280
- id: makeId(
1281
- "skill",
1282
- value.map((s) => s.id)
1283
- ),
1284
- label: ""
1285
- }),
1029
+ legend: ctx.label,
1030
+ items,
1031
+ onChange: (next) => {
1032
+ onChange(next);
1033
+ },
1034
+ keyOf: (item, index) => idOf(item) ?? String(index),
1035
+ itemLabel: (item, index) => captionOf(item, itemKind, ctx.locales) || `${ctx.label} ${String(index + 1)}`,
1036
+ createItem: () => blankValue(itemKind, takenIds, ctx.path),
1286
1037
  addLabel: getAdminLabel("action.add"),
1287
1038
  removeLabel: getAdminLabel("action.remove"),
1288
1039
  moveUpLabel: getAdminLabel("action.moveUp"),
1289
1040
  moveDownLabel: getAdminLabel("action.moveDown"),
1290
- emptyHint: getAdminLabel("empty.skills"),
1291
- renderItem: (skill, update, index) => {
1292
- const at = `/skills/${String(index)}`;
1293
- return /* @__PURE__ */ jsxs11(Fragment7, { children: [
1294
- /* @__PURE__ */ jsx14(
1295
- TextField,
1296
- {
1297
- label: getAdminLabel("field.skill.label"),
1298
- value: skill.label,
1299
- onChange: (label) => {
1300
- update({ ...skill, label });
1301
- },
1302
- required: true,
1303
- errors: errorsAt(errors, `${at}/label`)
1304
- }
1305
- ),
1306
- /* @__PURE__ */ jsx14(
1307
- TextField,
1308
- {
1309
- label: getAdminLabel("field.skill.category"),
1310
- value: skill.category ?? "",
1311
- onChange: (category) => {
1312
- update({ ...skill, category: category || void 0 });
1313
- },
1314
- errors: errorsAt(errors, `${at}/category`)
1315
- }
1316
- )
1317
- ] });
1041
+ renderItem: (item, update, index) => {
1042
+ const itemCtx = { ...ctx, pointer: `${ctx.pointer}/${String(index)}` };
1043
+ return itemKind.widget === "object" ? renderObjectFields(itemKind, asRecord(item), update, itemCtx) : renderScalar(itemKind, item, update, itemCtx);
1318
1044
  }
1319
1045
  }
1320
1046
  );
1321
1047
  }
1048
+ function SchemaForm(props) {
1049
+ const ctx = {
1050
+ pointer: props.pointer,
1051
+ path: props.path ?? props.pointer.replace(/^\//, "").replace(/\//g, "."),
1052
+ label: props.label,
1053
+ required: true,
1054
+ multiline: false,
1055
+ locales: props.locales,
1056
+ errors: props.errors ?? NO_FIELD_ERRORS,
1057
+ registry: props.registry ?? EMPTY_REGISTRY,
1058
+ formatLocale: props.formatLocale,
1059
+ uploadAsset: props.uploadAsset,
1060
+ document: props.document
1061
+ };
1062
+ if (props.kind.widget === "array") {
1063
+ return /* @__PURE__ */ jsx10(Fragment4, { children: renderArray(props.kind, props.value, props.onChange, ctx) });
1064
+ }
1065
+ if (props.kind.widget === "object") {
1066
+ return /* @__PURE__ */ jsxs7("fieldset", { children: [
1067
+ /* @__PURE__ */ jsx10("legend", { children: props.label }),
1068
+ renderObjectFields(props.kind, asRecord(props.value), props.onChange, ctx)
1069
+ ] });
1070
+ }
1071
+ return /* @__PURE__ */ jsx10(Fragment4, { children: renderScalar(props.kind, props.value, props.onChange, ctx) });
1072
+ }
1073
+
1074
+ // src/admin/schema-form/section-registry.tsx
1075
+ import "@takuhon/core";
1322
1076
 
1323
- // src/admin/sections/SettingsForm.tsx
1324
- import styles11 from "../sections.module-7MUK6I3V.module.css";
1325
- import { jsx as jsx15, jsxs as jsxs12 } from "react/jsx-runtime";
1077
+ // src/admin/schema-form/custom-fields.tsx
1078
+ import styles10 from "../custom-fields.module-3T2M3DO3.module.css";
1079
+ import { Fragment as Fragment5, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
1080
+ function asRecord2(value) {
1081
+ return value && typeof value === "object" ? value : {};
1082
+ }
1083
+ function asLocalized2(value) {
1084
+ return value && typeof value === "object" ? value : void 0;
1085
+ }
1086
+ function isEmptyRecord(record) {
1087
+ return !record || Object.keys(record).length === 0;
1088
+ }
1089
+ function asString2(value) {
1090
+ return typeof value === "string" ? value : "";
1091
+ }
1092
+ function idOf2(item) {
1093
+ const id = asRecord2(item).id;
1094
+ return typeof id === "string" ? id : "";
1095
+ }
1096
+ function renderAvatar(ctx) {
1097
+ const avatar = asRecord2(ctx.value);
1098
+ const url = typeof avatar.url === "string" ? avatar.url : "";
1099
+ const updateAvatar = (patch) => {
1100
+ const merged = { url: "", ...avatar, ...patch };
1101
+ const keep = merged.url !== "" || !isEmptyRecord(asLocalized2(merged.alt));
1102
+ ctx.onChange(keep ? merged : void 0);
1103
+ };
1104
+ return /* @__PURE__ */ jsxs8(Fragment5, { children: [
1105
+ /* @__PURE__ */ jsx11(
1106
+ ImageField,
1107
+ {
1108
+ label: getAdminLabel("field.avatarUrl"),
1109
+ value: url,
1110
+ onChange: (next) => {
1111
+ updateAvatar({ url: next });
1112
+ },
1113
+ hint: getAdminLabel(ctx.uploadAsset ? "hint.avatarUpload" : "hint.avatarNoUpload"),
1114
+ errors: errorsAt(ctx.errors, `${ctx.pointer}/url`),
1115
+ uploadAsset: ctx.uploadAsset
1116
+ }
1117
+ ),
1118
+ /* @__PURE__ */ jsx11(
1119
+ GravatarField,
1120
+ {
1121
+ onApply: (next) => {
1122
+ updateAvatar({ url: next });
1123
+ }
1124
+ }
1125
+ ),
1126
+ /* @__PURE__ */ jsx11(
1127
+ LocaleTabs,
1128
+ {
1129
+ label: getAdminLabel("field.avatarAlt"),
1130
+ value: asLocalized2(avatar.alt),
1131
+ locales: ctx.locales,
1132
+ onChange: (next) => {
1133
+ updateAvatar({ alt: next });
1134
+ },
1135
+ pointer: `${ctx.pointer}/alt`,
1136
+ errors: ctx.errors,
1137
+ formatLocale: ctx.formatLocale
1138
+ }
1139
+ )
1140
+ ] });
1141
+ }
1142
+ function parseCsv(input) {
1143
+ return input.split(",").map((entry) => entry.trim()).filter((entry) => entry !== "");
1144
+ }
1145
+ function csvListRenderer(options) {
1146
+ return function renderCsvList(ctx) {
1147
+ const items = Array.isArray(ctx.value) ? ctx.value : [];
1148
+ const text = items.filter((item) => typeof item === "string").join(", ");
1149
+ return /* @__PURE__ */ jsx11(
1150
+ TextField,
1151
+ {
1152
+ label: ctx.label,
1153
+ value: text,
1154
+ onChange: (input) => {
1155
+ const next = parseCsv(input);
1156
+ ctx.onChange(options.emptyToUndefined && next.length === 0 ? void 0 : next);
1157
+ },
1158
+ required: ctx.required,
1159
+ hint: getAdminLabel(options.hint),
1160
+ errors: collectErrorsUnder(ctx.errors, ctx.pointer)
1161
+ }
1162
+ );
1163
+ };
1164
+ }
1326
1165
  var VISIBILITY_SECTIONS = [
1327
1166
  { key: "links", label: "field.publicVisibility.links" },
1328
1167
  { key: "careers", label: "field.publicVisibility.careers" },
@@ -1341,129 +1180,25 @@ var VISIBILITY_SECTIONS = [
1341
1180
  { key: "recommendations", label: "field.publicVisibility.recommendations" },
1342
1181
  { key: "contact", label: "field.publicVisibility.contact" }
1343
1182
  ];
1344
- function parseLocales(input) {
1345
- return input.split(",").map((tag) => tag.trim()).filter((tag) => tag !== "");
1346
- }
1347
- function SettingsForm({
1348
- value,
1349
- onChange,
1350
- errors = NO_FIELD_ERRORS,
1351
- formatLocale
1352
- }) {
1353
- const format = formatLocale ?? ((locale) => locale);
1354
- const localeOptions = value.availableLocales.map((locale) => ({
1355
- value: locale,
1356
- label: format(locale)
1357
- }));
1358
- const headingId = "admin-section-settings";
1183
+ function renderVisibilityMatrix(ctx) {
1184
+ const visibility = asRecord2(ctx.value);
1359
1185
  const setVisibility = (key, visible) => {
1360
- const next = { ...value.publicVisibility };
1186
+ const next = { ...visibility };
1361
1187
  if (visible) {
1362
1188
  delete next[key];
1363
1189
  } else {
1364
1190
  next[key] = false;
1365
1191
  }
1366
- const publicVisibility = Object.keys(next).length > 0 ? next : void 0;
1367
- onChange({ ...value, publicVisibility });
1192
+ ctx.onChange(Object.keys(next).length > 0 ? next : void 0);
1368
1193
  };
1369
- return /* @__PURE__ */ jsxs12("section", { className: styles11.section, "aria-labelledby": headingId, children: [
1370
- /* @__PURE__ */ jsx15("h2", { className: styles11.heading, id: headingId, children: getAdminLabel("section.settings") }),
1371
- /* @__PURE__ */ jsx15(
1372
- TextField,
1373
- {
1374
- label: getAdminLabel("field.settings.availableLocales"),
1375
- value: value.availableLocales.join(", "),
1376
- onChange: (input) => {
1377
- onChange({ ...value, availableLocales: parseLocales(input) });
1378
- },
1379
- required: true,
1380
- hint: getAdminLabel("hint.locales"),
1381
- errors: collectErrorsUnder(errors, "/settings/availableLocales")
1382
- }
1383
- ),
1384
- /* @__PURE__ */ jsx15(
1385
- SelectField,
1386
- {
1387
- label: getAdminLabel("field.settings.defaultLocale"),
1388
- value: value.defaultLocale,
1389
- options: localeOptions,
1390
- onChange: (defaultLocale) => {
1391
- onChange({ ...value, defaultLocale });
1392
- },
1393
- required: true,
1394
- errors: errorsAt(errors, "/settings/defaultLocale")
1395
- }
1396
- ),
1397
- /* @__PURE__ */ jsx15(
1398
- SelectField,
1399
- {
1400
- label: getAdminLabel("field.settings.fallbackLocale"),
1401
- value: value.fallbackLocale ?? "",
1402
- options: [{ value: "", label: getAdminLabel("option.none") }, ...localeOptions],
1403
- onChange: (fallbackLocale) => {
1404
- onChange({ ...value, fallbackLocale: fallbackLocale || void 0 });
1405
- },
1406
- errors: errorsAt(errors, "/settings/fallbackLocale")
1407
- }
1408
- ),
1409
- /* @__PURE__ */ jsx15(
1410
- TextField,
1411
- {
1412
- label: getAdminLabel("field.settings.theme"),
1413
- value: value.theme ?? "",
1414
- onChange: (theme) => {
1415
- onChange({ ...value, theme: theme || void 0 });
1416
- },
1417
- errors: errorsAt(errors, "/settings/theme")
1418
- }
1419
- ),
1420
- /* @__PURE__ */ jsx15(
1421
- CheckboxField,
1422
- {
1423
- label: getAdminLabel("field.settings.showPoweredBy"),
1424
- checked: value.showPoweredBy ?? true,
1425
- onChange: (showPoweredBy) => {
1426
- onChange({ ...value, showPoweredBy });
1427
- }
1428
- }
1429
- ),
1430
- /* @__PURE__ */ jsx15(
1431
- CheckboxField,
1432
- {
1433
- label: getAdminLabel("field.settings.enableJsonLd"),
1434
- checked: value.enableJsonLd ?? true,
1435
- onChange: (enableJsonLd) => {
1436
- onChange({ ...value, enableJsonLd });
1437
- }
1438
- }
1439
- ),
1440
- /* @__PURE__ */ jsx15(
1441
- CheckboxField,
1442
- {
1443
- label: getAdminLabel("field.settings.enableApi"),
1444
- checked: value.enableApi ?? true,
1445
- onChange: (enableApi) => {
1446
- onChange({ ...value, enableApi });
1447
- }
1448
- }
1449
- ),
1450
- /* @__PURE__ */ jsx15(
1451
- CheckboxField,
1452
- {
1453
- label: getAdminLabel("field.settings.enableAnalytics"),
1454
- checked: value.enableAnalytics ?? false,
1455
- onChange: (enableAnalytics) => {
1456
- onChange({ ...value, enableAnalytics });
1457
- }
1458
- }
1459
- ),
1460
- /* @__PURE__ */ jsx15("h3", { className: styles11.subheading, children: getAdminLabel("field.settings.publicVisibility") }),
1461
- /* @__PURE__ */ jsx15("p", { className: styles11.hint, children: getAdminLabel("hint.publicVisibility") }),
1462
- VISIBILITY_SECTIONS.map(({ key, label }) => /* @__PURE__ */ jsx15(
1194
+ return /* @__PURE__ */ jsxs8(Fragment5, { children: [
1195
+ /* @__PURE__ */ jsx11("h3", { className: styles10.subheading, children: getAdminLabel("field.settings.publicVisibility") }),
1196
+ /* @__PURE__ */ jsx11("p", { className: styles10.hint, children: getAdminLabel("hint.publicVisibility") }),
1197
+ VISIBILITY_SECTIONS.map(({ key, label }) => /* @__PURE__ */ jsx11(
1463
1198
  CheckboxField,
1464
1199
  {
1465
1200
  label: getAdminLabel(label),
1466
- checked: value.publicVisibility?.[key] ?? true,
1201
+ checked: visibility[key] ?? true,
1467
1202
  onChange: (visible) => {
1468
1203
  setVisibility(key, visible);
1469
1204
  }
@@ -1472,12 +1207,129 @@ function SettingsForm({
1472
1207
  ))
1473
1208
  ] });
1474
1209
  }
1210
+ function referenceSelect(spec) {
1211
+ return function renderReferenceSelect(ctx) {
1212
+ const value = asString2(ctx.value);
1213
+ const items = (() => {
1214
+ const list = asRecord2(ctx.document)[spec.section];
1215
+ return Array.isArray(list) ? list : [];
1216
+ })();
1217
+ const candidates = items.map((item) => {
1218
+ const id = idOf2(item);
1219
+ const caption = firstLocalized(asLocalized2(asRecord2(item)[spec.captionField]), ctx.locales);
1220
+ return { value: id, label: caption ? `${caption} (${id})` : id };
1221
+ }).filter((option) => option.value !== "");
1222
+ const danglingId = value !== "" && !candidates.some((option) => option.value === value);
1223
+ const options = [
1224
+ ...ctx.required ? [] : [{ value: "", label: getAdminLabel("option.none") }],
1225
+ ...candidates,
1226
+ // Keep an unrecognized current value selectable so it is never dropped.
1227
+ ...danglingId ? [{ value, label: value }] : []
1228
+ ];
1229
+ return /* @__PURE__ */ jsx11(
1230
+ SelectField,
1231
+ {
1232
+ label: ctx.label,
1233
+ value,
1234
+ options,
1235
+ onChange: (next) => {
1236
+ ctx.onChange(next || void 0);
1237
+ },
1238
+ required: ctx.required,
1239
+ errors: errorsAt(ctx.errors, ctx.pointer)
1240
+ }
1241
+ );
1242
+ };
1243
+ }
1244
+
1245
+ // src/admin/schema-form/section-registry.tsx
1246
+ var ADMIN_SECTIONS = [
1247
+ { key: "profile", label: "section.profile" },
1248
+ { key: "links", label: "section.links" },
1249
+ { key: "careers", label: "section.careers" },
1250
+ { key: "projects", label: "section.projects" },
1251
+ { key: "skills", label: "section.skills" },
1252
+ { key: "settings", label: "section.settings" },
1253
+ { key: "education", label: "section.education" },
1254
+ { key: "certifications", label: "section.certifications" },
1255
+ { key: "publications", label: "section.publications" },
1256
+ { key: "honors", label: "section.honors" },
1257
+ { key: "volunteering", label: "section.volunteering" },
1258
+ { key: "memberships", label: "section.memberships" },
1259
+ { key: "languages", label: "section.languages" },
1260
+ { key: "courses", label: "section.courses" },
1261
+ { key: "patents", label: "section.patents" },
1262
+ { key: "testScores", label: "section.testScores" },
1263
+ { key: "recommendations", label: "section.recommendations" },
1264
+ { key: "contact", label: "section.contact" },
1265
+ { key: "meta", label: "section.meta" }
1266
+ ];
1267
+ var SECTION_REGISTRY = {
1268
+ // Profile — avatar is the URL/upload/Gravatar trio.
1269
+ "profile.avatar": { render: renderAvatar },
1270
+ "profile.location.country": { hint: getAdminLabel("hint.country") },
1271
+ "profile.location.display": { label: getAdminLabel("field.location.display") },
1272
+ // Links.
1273
+ "links.url": { label: getAdminLabel("field.link.url") },
1274
+ "links.iconUrl": { label: getAdminLabel("field.link.iconUrl") },
1275
+ // Careers.
1276
+ "careers.startDate": {
1277
+ label: getAdminLabel("field.career.startDate"),
1278
+ hint: getAdminLabel("hint.month")
1279
+ },
1280
+ "careers.endDate": {
1281
+ label: getAdminLabel("field.career.endDate"),
1282
+ hint: getAdminLabel("hint.month")
1283
+ },
1284
+ "careers.isCurrent": { label: getAdminLabel("field.career.isCurrent") },
1285
+ "careers.url": { label: getAdminLabel("field.career.url") },
1286
+ // Projects — tags are a comma-separated list; relatedCareerId is a reference.
1287
+ "projects.tags": { render: csvListRenderer({ hint: "hint.tags", emptyToUndefined: true }) },
1288
+ "projects.relatedCareerId": {
1289
+ render: referenceSelect({ section: "careers", captionField: "organization" })
1290
+ },
1291
+ "projects.url": { label: getAdminLabel("field.project.url") },
1292
+ "projects.startDate": { hint: getAdminLabel("hint.month") },
1293
+ "projects.endDate": { hint: getAdminLabel("hint.month") },
1294
+ // Settings — locales are comma-separated, visibility is the inverted matrix.
1295
+ "settings.availableLocales": {
1296
+ render: csvListRenderer({ hint: "hint.locales", emptyToUndefined: false })
1297
+ },
1298
+ "settings.theme": { label: getAdminLabel("field.settings.theme") },
1299
+ "settings.showPoweredBy": { label: getAdminLabel("field.settings.showPoweredBy") },
1300
+ "settings.enableJsonLd": { label: getAdminLabel("field.settings.enableJsonLd") },
1301
+ "settings.enableApi": { label: getAdminLabel("field.settings.enableApi") },
1302
+ "settings.enableAnalytics": { label: getAdminLabel("field.settings.enableAnalytics") },
1303
+ "settings.activity.github": { label: "GitHub" },
1304
+ "settings.activity.wakatime": { label: "WakaTime" },
1305
+ "settings.publicVisibility": { render: renderVisibilityMatrix },
1306
+ // Cross-section reference selectors (decision C): dropdowns of sibling-section
1307
+ // ids. courses / testScores / recommendations point at education / careers.
1308
+ "courses.relatedEducationId": {
1309
+ render: referenceSelect({ section: "education", captionField: "institution" })
1310
+ },
1311
+ "testScores.relatedEducationId": {
1312
+ render: referenceSelect({ section: "education", captionField: "institution" })
1313
+ },
1314
+ "recommendations.relatedCareerId": {
1315
+ render: referenceSelect({ section: "careers", captionField: "organization" })
1316
+ },
1317
+ "recommendations.relatedEducationId": {
1318
+ render: referenceSelect({ section: "education", captionField: "institution" })
1319
+ },
1320
+ // Languages — disambiguate from the profile's "Display name".
1321
+ "languages.displayName": { label: "Language name" },
1322
+ // Metadata — auto-managed, never hand-edited.
1323
+ "meta.createdAt": { hidden: true },
1324
+ "meta.updatedAt": { hidden: true },
1325
+ "meta.generator": { hidden: true }
1326
+ };
1475
1327
 
1476
1328
  // src/admin/RawJsonEditor.tsx
1477
1329
  import { validate } from "@takuhon/core";
1478
1330
  import { useId as useId5, useState as useState4 } from "react";
1479
- import styles12 from "../RawJsonEditor.module-NGGM3IBY.module.css";
1480
- import { jsx as jsx16, jsxs as jsxs13 } from "react/jsx-runtime";
1331
+ import styles11 from "../RawJsonEditor.module-NGGM3IBY.module.css";
1332
+ import { jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
1481
1333
  var MAX_SHOWN_PROBLEMS = 50;
1482
1334
  function RawJsonEditor({ value, onChange }) {
1483
1335
  const [text, setText] = useState4(() => JSON.stringify(value, null, 2));
@@ -1504,13 +1356,13 @@ function RawJsonEditor({ value, onChange }) {
1504
1356
  onChange(result.data);
1505
1357
  };
1506
1358
  const hasProblems = problems.length > 0;
1507
- return /* @__PURE__ */ jsxs13("section", { className: styles12.wrapper, "aria-labelledby": labelId, children: [
1508
- /* @__PURE__ */ jsx16("h2", { className: styles12.heading, id: labelId, children: getAdminLabel("mode.advanced") }),
1509
- /* @__PURE__ */ jsx16("p", { className: styles12.hint, children: getAdminLabel("advanced.hint") }),
1510
- /* @__PURE__ */ jsx16(
1359
+ return /* @__PURE__ */ jsxs9("section", { className: styles11.wrapper, "aria-labelledby": labelId, children: [
1360
+ /* @__PURE__ */ jsx12("h2", { className: styles11.heading, id: labelId, children: getAdminLabel("mode.advanced") }),
1361
+ /* @__PURE__ */ jsx12("p", { className: styles11.hint, children: getAdminLabel("advanced.hint") }),
1362
+ /* @__PURE__ */ jsx12(
1511
1363
  "textarea",
1512
1364
  {
1513
- className: styles12.textarea,
1365
+ className: styles11.textarea,
1514
1366
  value: text,
1515
1367
  spellCheck: false,
1516
1368
  "aria-label": getAdminLabel("mode.advanced"),
@@ -1521,18 +1373,22 @@ function RawJsonEditor({ value, onChange }) {
1521
1373
  }
1522
1374
  }
1523
1375
  ),
1524
- hasProblems ? /* @__PURE__ */ jsxs13("div", { className: styles12.problems, id: errorId, role: "alert", children: [
1525
- /* @__PURE__ */ jsx16("p", { className: styles12.problemsTitle, children: getAdminLabel("advanced.invalid") }),
1526
- /* @__PURE__ */ jsx16("ul", { children: problems.map((problem, i) => /* @__PURE__ */ jsx16("li", { children: problem }, i)) })
1376
+ hasProblems ? /* @__PURE__ */ jsxs9("div", { className: styles11.problems, id: errorId, role: "alert", children: [
1377
+ /* @__PURE__ */ jsx12("p", { className: styles11.problemsTitle, children: getAdminLabel("advanced.invalid") }),
1378
+ /* @__PURE__ */ jsx12("ul", { children: problems.map((problem, i) => /* @__PURE__ */ jsx12("li", { children: problem }, i)) })
1527
1379
  ] }) : null
1528
1380
  ] });
1529
1381
  }
1530
1382
 
1531
1383
  // src/admin/AdminEditor.tsx
1532
- import { validate as validate2 } from "@takuhon/core";
1533
- import { useRef as useRef3, useState as useState5 } from "react";
1534
- import styles13 from "../AdminEditor.module-ABYQXFN4.module.css";
1535
- import { jsx as jsx17, jsxs as jsxs14 } from "react/jsx-runtime";
1384
+ import { schema, validate as validate2 } from "@takuhon/core";
1385
+ import { useEffect, useRef as useRef3, useState as useState5 } from "react";
1386
+ import styles12 from "../AdminEditor.module-ABYQXFN4.module.css";
1387
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1388
+ var schemaRoot = schema;
1389
+ function setSection(doc, key, value) {
1390
+ return { ...doc, [key]: value };
1391
+ }
1536
1392
  function AdminEditor({
1537
1393
  initialDocument,
1538
1394
  onSave,
@@ -1549,7 +1405,15 @@ function AdminEditor({
1549
1405
  const [busy, setBusy] = useState5(false);
1550
1406
  const [loadGen, setLoadGen] = useState5(0);
1551
1407
  const intentRef = useRef3(0);
1408
+ const summaryRef = useRef3(null);
1409
+ const focusSummaryRef = useRef3(false);
1552
1410
  const locales = draft.settings.availableLocales;
1411
+ useEffect(() => {
1412
+ if (focusSummaryRef.current && summaryRef.current) {
1413
+ summaryRef.current.focus();
1414
+ }
1415
+ focusSummaryRef.current = false;
1416
+ }, [errors]);
1553
1417
  const updateDraft = (next) => {
1554
1418
  intentRef.current += 1;
1555
1419
  setDraft(next);
@@ -1566,6 +1430,7 @@ function AdminEditor({
1566
1430
  const handleSave = async () => {
1567
1431
  const result = validate2(draft);
1568
1432
  if (!result.ok) {
1433
+ focusSummaryRef.current = true;
1569
1434
  setErrors(indexValidationErrors(result.errors));
1570
1435
  setStatus({ tone: "error", message: getAdminLabel("status.invalid") });
1571
1436
  return;
@@ -1586,6 +1451,7 @@ function AdminEditor({
1586
1451
  setStatus({ tone: "error", message: getAdminLabel("status.conflict") });
1587
1452
  break;
1588
1453
  case "invalid":
1454
+ focusSummaryRef.current = true;
1589
1455
  setErrors(indexErrors(outcome.errors));
1590
1456
  setStatus({ tone: "error", message: getAdminLabel("status.invalid") });
1591
1457
  break;
@@ -1635,14 +1501,14 @@ function AdminEditor({
1635
1501
  const errorEntries = [...errors].flatMap(
1636
1502
  ([pointer, messages]) => messages.map((message) => ({ pointer, message }))
1637
1503
  );
1638
- return /* @__PURE__ */ jsxs14("div", { className: styles13.editor, children: [
1639
- /* @__PURE__ */ jsxs14("div", { className: styles13.toolbar, role: "toolbar", "aria-label": getAdminLabel("toolbar.label"), children: [
1640
- /* @__PURE__ */ jsxs14("div", { className: styles13.modes, role: "group", "aria-label": getAdminLabel("mode.label"), children: [
1641
- /* @__PURE__ */ jsx17(
1504
+ return /* @__PURE__ */ jsxs10("div", { className: styles12.editor, children: [
1505
+ /* @__PURE__ */ jsxs10("div", { className: styles12.toolbar, role: "toolbar", "aria-label": getAdminLabel("toolbar.label"), children: [
1506
+ /* @__PURE__ */ jsxs10("div", { className: styles12.modes, role: "group", "aria-label": getAdminLabel("mode.label"), children: [
1507
+ /* @__PURE__ */ jsx13(
1642
1508
  "button",
1643
1509
  {
1644
1510
  type: "button",
1645
- className: `${styles13.modeButton} ${mode === "form" ? styles13.modeActive : ""}`,
1511
+ className: `${styles12.modeButton} ${mode === "form" ? styles12.modeActive : ""}`,
1646
1512
  "aria-pressed": mode === "form",
1647
1513
  onClick: () => {
1648
1514
  setMode("form");
@@ -1650,11 +1516,11 @@ function AdminEditor({
1650
1516
  children: getAdminLabel("mode.form")
1651
1517
  }
1652
1518
  ),
1653
- /* @__PURE__ */ jsx17(
1519
+ /* @__PURE__ */ jsx13(
1654
1520
  "button",
1655
1521
  {
1656
1522
  type: "button",
1657
- className: `${styles13.modeButton} ${mode === "advanced" ? styles13.modeActive : ""}`,
1523
+ className: `${styles12.modeButton} ${mode === "advanced" ? styles12.modeActive : ""}`,
1658
1524
  "aria-pressed": mode === "advanced",
1659
1525
  onClick: () => {
1660
1526
  setMode("advanced");
@@ -1663,12 +1529,12 @@ function AdminEditor({
1663
1529
  }
1664
1530
  )
1665
1531
  ] }),
1666
- /* @__PURE__ */ jsxs14("div", { className: styles13.actions, children: [
1667
- /* @__PURE__ */ jsx17(
1532
+ /* @__PURE__ */ jsxs10("div", { className: styles12.actions, children: [
1533
+ /* @__PURE__ */ jsx13(
1668
1534
  "button",
1669
1535
  {
1670
1536
  type: "button",
1671
- className: styles13.primary,
1537
+ className: styles12.primary,
1672
1538
  disabled: busy,
1673
1539
  onClick: () => {
1674
1540
  void handleSave();
@@ -1676,11 +1542,11 @@ function AdminEditor({
1676
1542
  children: getAdminLabel("action.save")
1677
1543
  }
1678
1544
  ),
1679
- onReload ? /* @__PURE__ */ jsx17(
1545
+ onReload ? /* @__PURE__ */ jsx13(
1680
1546
  "button",
1681
1547
  {
1682
1548
  type: "button",
1683
- className: styles13.secondary,
1549
+ className: styles12.secondary,
1684
1550
  disabled: busy,
1685
1551
  onClick: () => {
1686
1552
  void handleReload();
@@ -1688,22 +1554,22 @@ function AdminEditor({
1688
1554
  children: getAdminLabel("action.reload")
1689
1555
  }
1690
1556
  ) : null,
1691
- onExport ? /* @__PURE__ */ jsx17(
1557
+ onExport ? /* @__PURE__ */ jsx13(
1692
1558
  "button",
1693
1559
  {
1694
1560
  type: "button",
1695
- className: styles13.secondary,
1561
+ className: styles12.secondary,
1696
1562
  onClick: () => {
1697
1563
  onExport(draft);
1698
1564
  },
1699
1565
  children: getAdminLabel("action.export")
1700
1566
  }
1701
1567
  ) : null,
1702
- onImport ? /* @__PURE__ */ jsx17(
1568
+ onImport ? /* @__PURE__ */ jsx13(
1703
1569
  "button",
1704
1570
  {
1705
1571
  type: "button",
1706
- className: styles13.secondary,
1572
+ className: styles12.secondary,
1707
1573
  disabled: busy,
1708
1574
  onClick: () => {
1709
1575
  void handleImport();
@@ -1713,110 +1579,68 @@ function AdminEditor({
1713
1579
  ) : null
1714
1580
  ] })
1715
1581
  ] }),
1716
- /* @__PURE__ */ jsx17("p", { className: styles13.status, role: "status", "aria-live": "polite", "data-tone": status?.tone, children: status?.message ?? "" }),
1717
- errorEntries.length > 0 ? /* @__PURE__ */ jsxs14("section", { className: styles13.summary, "aria-labelledby": "admin-error-summary", children: [
1718
- /* @__PURE__ */ jsx17("h2", { className: styles13.summaryHeading, id: "admin-error-summary", children: getAdminLabel("status.fixSummary") }),
1719
- /* @__PURE__ */ jsx17("ul", { children: errorEntries.map((entry, i) => /* @__PURE__ */ jsx17("li", { children: entry.pointer === "" ? entry.message : `${entry.pointer.replace(/^\//, "")}: ${entry.message}` }, i)) })
1720
- ] }) : null,
1721
- mode === "form" ? /* @__PURE__ */ jsxs14("div", { className: styles13.sections, children: [
1722
- /* @__PURE__ */ jsx17(
1723
- ProfileForm,
1724
- {
1725
- value: draft.profile,
1726
- onChange: (profile) => {
1727
- updateDraft({ ...draft, profile });
1728
- },
1729
- locales,
1730
- errors,
1731
- formatLocale,
1732
- uploadAsset
1733
- }
1734
- ),
1735
- /* @__PURE__ */ jsx17(
1736
- LinksForm,
1737
- {
1738
- value: draft.links,
1739
- onChange: (links) => {
1740
- updateDraft({ ...draft, links });
1741
- },
1742
- locales,
1743
- errors,
1744
- formatLocale
1745
- }
1746
- ),
1747
- /* @__PURE__ */ jsx17(
1748
- CareersForm,
1749
- {
1750
- value: draft.careers,
1751
- onChange: (careers) => {
1752
- updateDraft({ ...draft, careers });
1753
- },
1754
- locales,
1755
- errors,
1756
- formatLocale
1757
- }
1758
- ),
1759
- /* @__PURE__ */ jsx17(
1760
- ProjectsForm,
1582
+ /* @__PURE__ */ jsx13("p", { className: styles12.status, role: "status", "aria-live": "polite", "data-tone": status?.tone, children: status?.message ?? "" }),
1583
+ errorEntries.length > 0 ? /* @__PURE__ */ jsxs10("section", { className: styles12.summary, "aria-labelledby": "admin-error-summary", children: [
1584
+ /* @__PURE__ */ jsx13(
1585
+ "h2",
1761
1586
  {
1762
- value: draft.projects,
1763
- onChange: (projects) => {
1764
- updateDraft({ ...draft, projects });
1765
- },
1766
- locales,
1767
- errors,
1768
- formatLocale
1769
- }
1770
- ),
1771
- /* @__PURE__ */ jsx17(
1772
- SkillsForm,
1773
- {
1774
- value: draft.skills,
1775
- onChange: (skills) => {
1776
- updateDraft({ ...draft, skills });
1777
- },
1778
- errors
1587
+ className: styles12.summaryHeading,
1588
+ id: "admin-error-summary",
1589
+ tabIndex: -1,
1590
+ ref: summaryRef,
1591
+ children: getAdminLabel("status.fixSummary")
1779
1592
  }
1780
1593
  ),
1781
- /* @__PURE__ */ jsx17(
1782
- SettingsForm,
1783
- {
1784
- value: draft.settings,
1785
- onChange: (settings) => {
1786
- updateDraft({ ...draft, settings });
1787
- },
1788
- errors,
1789
- formatLocale
1790
- }
1791
- )
1792
- ] }) : /* @__PURE__ */ jsx17(RawJsonEditor, { value: draft, onChange: updateDraft }, loadGen)
1594
+ /* @__PURE__ */ jsx13("ul", { children: errorEntries.map((entry, i) => /* @__PURE__ */ jsx13("li", { children: entry.pointer === "" ? entry.message : `${entry.pointer.replace(/^\//, "")}: ${entry.message}` }, i)) })
1595
+ ] }) : null,
1596
+ mode === "form" ? /* @__PURE__ */ jsx13("div", { className: styles12.sections, children: ADMIN_SECTIONS.map(({ key, label }) => /* @__PURE__ */ jsx13(
1597
+ SchemaForm,
1598
+ {
1599
+ kind: sectionFieldKind(schemaRoot, key),
1600
+ value: draft[key],
1601
+ onChange: (next) => {
1602
+ updateDraft(setSection(draft, key, next));
1603
+ },
1604
+ pointer: `/${key}`,
1605
+ label: getAdminLabel(label),
1606
+ locales,
1607
+ errors,
1608
+ registry: SECTION_REGISTRY,
1609
+ formatLocale,
1610
+ uploadAsset,
1611
+ document: draft
1612
+ },
1613
+ key
1614
+ )) }) : /* @__PURE__ */ jsx13(RawJsonEditor, { value: draft, onChange: updateDraft }, loadGen)
1793
1615
  ] });
1794
1616
  }
1795
1617
  export {
1618
+ ADMIN_SECTIONS,
1796
1619
  AdminEditor,
1797
- CareersForm,
1798
1620
  CheckboxField,
1621
+ EMPTY_REGISTRY,
1799
1622
  Field,
1800
1623
  GravatarField,
1801
1624
  ImageField,
1802
- LinksForm,
1803
1625
  LocaleTabs,
1804
1626
  NO_FIELD_ERRORS,
1805
- ProfileForm,
1806
- ProjectsForm,
1807
1627
  RawJsonEditor,
1808
1628
  Repeater,
1629
+ SECTION_REGISTRY,
1630
+ SchemaForm,
1809
1631
  SelectField,
1810
- SettingsForm,
1811
- SkillsForm,
1812
1632
  TextAreaField,
1813
1633
  TextField,
1814
1634
  canonicalPointer,
1635
+ classifyNode,
1815
1636
  collectErrorsUnder,
1816
1637
  errorsAt,
1817
1638
  getAdminLabel,
1818
1639
  hasErrorsUnder,
1640
+ hintAt,
1641
+ humanize,
1819
1642
  indexErrors,
1820
- indexValidationErrors
1643
+ indexValidationErrors,
1644
+ sectionFieldKind
1821
1645
  };
1822
1646
  //# sourceMappingURL=index.js.map