@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.
@@ -1,4 +1,4 @@
1
- import { ValidationError, LocaleTag, Profile, Link, Career, Project, Skill, Settings, Takuhon } from '@takuhon/core';
1
+ import { ValidationError, LocaleTag, Takuhon } from '@takuhon/core';
2
2
 
3
3
  /**
4
4
  * Field-error plumbing shared by the admin form components.
@@ -295,6 +295,19 @@ declare const EN: {
295
295
  readonly 'section.projects': "Projects";
296
296
  readonly 'section.skills': "Skills";
297
297
  readonly 'section.settings': "Settings";
298
+ readonly 'section.education': "Education";
299
+ readonly 'section.certifications': "Certifications";
300
+ readonly 'section.publications': "Publications";
301
+ readonly 'section.honors': "Honors & awards";
302
+ readonly 'section.volunteering': "Volunteering";
303
+ readonly 'section.memberships': "Memberships";
304
+ readonly 'section.languages': "Languages";
305
+ readonly 'section.courses': "Courses";
306
+ readonly 'section.patents': "Patents";
307
+ readonly 'section.testScores': "Test scores";
308
+ readonly 'section.recommendations': "Recommendations";
309
+ readonly 'section.contact': "Contact";
310
+ readonly 'section.meta': "Metadata";
298
311
  readonly 'field.displayName': "Display name";
299
312
  readonly 'field.tagline': "Tagline";
300
313
  readonly 'field.bio': "Bio";
@@ -382,71 +395,241 @@ type AdminLabelKey = keyof typeof EN;
382
395
  */
383
396
  declare function getAdminLabel(key: AdminLabelKey, locale?: LocaleTag): string;
384
397
 
385
- interface ProfileFormProps {
386
- value: Profile;
387
- onChange: (next: Profile) => void;
388
- locales: readonly LocaleTag[];
389
- errors?: FieldErrorIndex;
390
- formatLocale?: (locale: LocaleTag) => string;
391
- /**
392
- * Upload an avatar image file. When provided, the avatar field offers a file
393
- * picker; when omitted, it stays URL-only.
394
- */
395
- uploadAsset?: UploadAsset;
398
+ /**
399
+ * Schema-driven field classification for the admin editor.
400
+ *
401
+ * Maps a node of the canonical takuhon JSON Schema (2020-12) to a
402
+ * {@link FieldKind} — the widget the admin form should render for it. This is
403
+ * the pure-logic foundation of the schema-driven form engine (design:
404
+ * `admin-form-schema-driven-design.md`). It resolves local `$ref`s against
405
+ * `$defs` and unwraps nullable `anyOf` unions, so the renderer never hand-wires
406
+ * fields per section. No React here; the classifier is fully unit-testable.
407
+ *
408
+ * Presentation concerns the schema cannot express (widget overrides, ordering,
409
+ * help text, cross-section reference selectors, image/Gravatar widgets) live in
410
+ * a separate field-spec registry, not in the data schema (design decision A1).
411
+ */
412
+ /**
413
+ * Minimal structural view of a JSON Schema 2020-12 node — only the keywords the
414
+ * classifier reads. The canonical schema (`@takuhon/core`'s `schema`) is cast to
415
+ * this for traversal.
416
+ */
417
+ interface SchemaNode {
418
+ $ref?: string;
419
+ $defs?: Record<string, SchemaNode>;
420
+ type?: string | readonly string[];
421
+ format?: string;
422
+ enum?: readonly unknown[];
423
+ properties?: Record<string, SchemaNode>;
424
+ required?: readonly string[];
425
+ items?: SchemaNode;
426
+ anyOf?: readonly SchemaNode[];
427
+ maxLength?: number;
428
+ minimum?: number;
429
+ description?: string;
430
+ default?: unknown;
396
431
  }
432
+ /** The widget the admin form should render for a classified schema node. */
433
+ type FieldKind = {
434
+ widget: 'text';
435
+ maxLength?: number;
436
+ } | {
437
+ widget: 'url';
438
+ } | {
439
+ widget: 'email';
440
+ } | {
441
+ widget: 'month';
442
+ } | {
443
+ widget: 'date';
444
+ } | {
445
+ widget: 'datetime';
446
+ } | {
447
+ widget: 'integer';
448
+ minimum?: number;
449
+ } | {
450
+ widget: 'checkbox';
451
+ default?: boolean;
452
+ } | {
453
+ widget: 'select';
454
+ options: readonly string[];
455
+ } | {
456
+ widget: 'localeTag';
457
+ } | {
458
+ widget: 'localizedTitle';
459
+ } | {
460
+ widget: 'localizedBody';
461
+ } | {
462
+ widget: 'slug';
463
+ } | {
464
+ widget: 'array';
465
+ item: FieldKind;
466
+ } | {
467
+ widget: 'object';
468
+ fields: readonly FieldEntry[];
469
+ } | {
470
+ widget: 'unsupported';
471
+ };
472
+ /** A named property of an object schema: its optionality and classified kind. */
473
+ interface FieldEntry {
474
+ name: string;
475
+ /** Listed in the object's `required` array. */
476
+ required: boolean;
477
+ /** The schema permits an explicit `null` (e.g. `anyOf: [X, { type: 'null' }]`). */
478
+ nullable: boolean;
479
+ kind: FieldKind;
480
+ }
481
+ /** Classify a (possibly `$ref`/nullable) schema node into the widget to render. */
482
+ declare function classifyNode(root: SchemaNode, node: SchemaNode): FieldKind;
483
+ /** Classify a top-level section (e.g. `'education'`, `'contact'`) by name. */
484
+ declare function sectionFieldKind(root: SchemaNode, section: string): FieldKind;
485
+
397
486
  /**
398
- * Basic profile + About: display name, tagline, bio, avatar, and a structured
399
- * location (spec §6.5 / §14.2). The avatar accepts a URL and, when
400
- * `uploadAsset` is supplied by the host, an uploaded image file.
487
+ * Field-spec registry the UI-hint layer the data schema deliberately does not
488
+ * carry (design decision A1). Keyed by a section-relative dot path *without*
489
+ * array indices (e.g. `education.institution`, `meta.createdAt`), it overrides
490
+ * or augments the schema-derived classification with presentation concerns:
491
+ * labels, help text, hiding auto-managed fields, forcing a widget.
492
+ *
493
+ * Keeping these out of `takuhon.schema.json` leaves the data contract pure
494
+ * (consumers / validators see no UI noise) and means this track changes no
495
+ * `schemaVersion`.
401
496
  */
402
- declare function ProfileForm({ value, onChange, locales, errors, formatLocale, uploadAsset, }: ProfileFormProps): React.JSX.Element;
403
497
 
404
- interface LinksFormProps {
405
- value: readonly Link[];
406
- onChange: (next: Link[]) => void;
498
+ /**
499
+ * Everything a custom field renderer needs. The engine builds this as it walks,
500
+ * so a bespoke widget (avatar, comma-separated list, visibility matrix) plugs in
501
+ * without re-deriving its pointer/path or re-threading locales and errors. This
502
+ * is the escape hatch for the handful of fields a generic widget cannot capture
503
+ * (design §8 "generic default + targeted craft").
504
+ */
505
+ interface CustomFieldContext {
506
+ /** Current value at this path. */
507
+ value: unknown;
508
+ /** Write the next value for this field back into the document. */
509
+ onChange: (next: unknown) => void;
510
+ /** RFC 6901 pointer (with array indices) for error lookup. */
511
+ pointer: string;
512
+ /** Registry path (no array indices). */
513
+ path: string;
514
+ /** Resolved label (registry override or humanized field name). */
515
+ label: string;
516
+ /** Listed in the parent object's `required`. */
517
+ required: boolean;
407
518
  locales: readonly LocaleTag[];
408
- errors?: FieldErrorIndex;
519
+ errors: FieldErrorIndex;
409
520
  formatLocale?: (locale: LocaleTag) => string;
521
+ /** Avatar upload, threaded from the host; absent means URL-only. */
522
+ uploadAsset?: UploadAsset;
523
+ /**
524
+ * The whole document (root value), so a field can read sibling sections —
525
+ * e.g. a reference selector listing `careers[]` / `education[]` ids. Typed
526
+ * `unknown` to keep the engine schema-generic; renderers narrow it.
527
+ */
528
+ document?: unknown;
410
529
  }
411
- /** SNS / profile links with builtin-vs-custom typing (spec §6.6 / §14.2). */
412
- declare function LinksForm({ value, onChange, locales, errors, formatLocale, }: LinksFormProps): React.JSX.Element;
413
-
414
- interface CareersFormProps {
415
- value: readonly Career[];
416
- onChange: (next: Career[]) => void;
417
- locales: readonly LocaleTag[];
418
- errors?: FieldErrorIndex;
419
- formatLocale?: (locale: LocaleTag) => string;
530
+ /** Presentation overrides for a single field, looked up by its schema path. */
531
+ interface FieldHint {
532
+ /** Replace the humanized field name. */
533
+ label?: string;
534
+ /** Help text shown under the control. */
535
+ hint?: string;
536
+ /** Omit from the form (the value is still preserved in the document). */
537
+ hidden?: boolean;
538
+ /** Render this widget instead of the schema-derived one. */
539
+ widget?: FieldKind['widget'];
540
+ /** Force multiline for a localized/text field. */
541
+ multiline?: boolean;
542
+ /**
543
+ * Render a bespoke control for this field instead of the schema-derived
544
+ * widget. Used for the few fields no generic widget captures (avatar,
545
+ * comma-separated lists, the public-visibility matrix).
546
+ */
547
+ render?: (ctx: CustomFieldContext) => React.ReactNode;
420
548
  }
421
- /** Work experience (spec §6.7 / §14.2 "職歴"). */
422
- declare function CareersForm({ value, onChange, locales, errors, formatLocale, }: CareersFormProps): React.JSX.Element;
549
+ /** Schema-path hint. Paths use dots and never include array indices. */
550
+ type FieldRegistry = Readonly<Record<string, FieldHint>>;
551
+ declare const EMPTY_REGISTRY: FieldRegistry;
552
+ /**
553
+ * Humanize a property name for a default label: `fieldOfStudy` → `Field of
554
+ * study`, `credentialId` → `Credential ID`, `url` → `URL`. Known acronyms stay
555
+ * uppercased. The registry overrides this where a better label is needed.
556
+ */
557
+ declare function humanize(name: string): string;
558
+ /** The hint at `path`, or an empty hint when none is registered. */
559
+ declare function hintAt(registry: FieldRegistry, path: string): FieldHint;
560
+
561
+ /**
562
+ * Schema-driven form engine.
563
+ *
564
+ * Renders an editable form for any takuhon section straight from its
565
+ * {@link FieldKind} (see `field-classification.ts`), using the existing admin
566
+ * primitives. The RFC 6901 `pointer` (with array indices, for error lookup) and
567
+ * the registry `path` (without indices, for UI hints) are derived automatically
568
+ * as the engine walks — so no section hand-wires field plumbing or pointer
569
+ * strings. Presentation overrides come from the {@link FieldRegistry}
570
+ * (decision A1); the data schema stays UI-free.
571
+ *
572
+ * Values are treated as `unknown` and narrowed per kind — the document has been
573
+ * (or will be) validated against the schema, so each branch's cast is sound.
574
+ */
423
575
 
424
- interface ProjectsFormProps {
425
- value: readonly Project[];
426
- onChange: (next: Project[]) => void;
576
+ interface SchemaFormProps {
577
+ /** Classified shape of the section (from `sectionFieldKind`). */
578
+ kind: FieldKind;
579
+ /** Current value for the section. */
580
+ value: unknown;
581
+ /** Receives the next value on any edit. */
582
+ onChange: (next: unknown) => void;
583
+ /** RFC 6901 pointer to this value, e.g. `/education`. */
584
+ pointer: string;
585
+ /** Registry path (no array indices). Defaults to the pointer sans leading slash. */
586
+ path?: string;
587
+ /** Heading for the section / object. */
588
+ label: string;
427
589
  locales: readonly LocaleTag[];
428
590
  errors?: FieldErrorIndex;
591
+ registry?: FieldRegistry;
429
592
  formatLocale?: (locale: LocaleTag) => string;
593
+ /** Avatar upload, threaded to the avatar field's custom renderer. */
594
+ uploadAsset?: UploadAsset;
595
+ /** Whole document, so reference fields can list sibling-section ids. */
596
+ document?: unknown;
430
597
  }
431
- /** Projects (spec §6.8 / §14.2). */
432
- declare function ProjectsForm({ value, onChange, locales, errors, formatLocale, }: ProjectsFormProps): React.JSX.Element;
598
+ /**
599
+ * Render a section's form from its schema-derived {@link FieldKind}. Array
600
+ * sections become a {@link Repeater}; object sections a labelled fieldset;
601
+ * scalars map to the matching primitive.
602
+ */
603
+ declare function SchemaForm(props: SchemaFormProps): React.JSX.Element;
433
604
 
434
- interface SkillsFormProps {
435
- value: readonly Skill[];
436
- onChange: (next: Skill[]) => void;
437
- errors?: FieldErrorIndex;
438
- }
439
- /** Skills (spec §6.9 / §14.2). `label` is a plain string, not localized. */
440
- declare function SkillsForm({ value, onChange, errors, }: SkillsFormProps): React.JSX.Element;
605
+ /**
606
+ * The admin editor's single source of truth for which sections to render and how.
607
+ *
608
+ * {@link ADMIN_SECTIONS} lists every top-level section in display order; the
609
+ * {@link SchemaForm} engine renders each one from the schema. {@link SECTION_REGISTRY}
610
+ * is the UI-hint layer (decision A1): bespoke renderers for the few fields a
611
+ * generic widget cannot capture (avatar, comma-separated lists, the visibility
612
+ * matrix), plus curated labels and help text. The data schema stays UI-free.
613
+ */
441
614
 
442
- interface SettingsFormProps {
443
- value: Settings;
444
- onChange: (next: Settings) => void;
445
- errors?: FieldErrorIndex;
446
- formatLocale?: (locale: LocaleTag) => string;
615
+ interface AdminSection {
616
+ key: keyof Takuhon;
617
+ label: AdminLabelKey;
447
618
  }
448
- /** Site settings: locales, theme, and feature toggles (spec §6.20 / §14.2). */
449
- declare function SettingsForm({ value, onChange, errors, formatLocale, }: SettingsFormProps): React.JSX.Element;
619
+ /**
620
+ * All editable sections, in display order. The first six were once bespoke
621
+ * forms; they now share the schema-driven engine with the rest, so every section
622
+ * is a form (spec §14.2 Phase 5, no more raw-JSON-only sections).
623
+ */
624
+ declare const ADMIN_SECTIONS: readonly AdminSection[];
625
+ /**
626
+ * UI hints keyed by section-relative dot path (no array indices). Covers the
627
+ * bespoke widgets (avatar, comma-separated lists, visibility matrix, and
628
+ * cross-section reference selectors), curated labels for fields whose humanized
629
+ * name reads poorly, help text, and the auto-managed meta fields that stay
630
+ * hidden.
631
+ */
632
+ declare const SECTION_REGISTRY: FieldRegistry;
450
633
 
451
634
  interface RawJsonEditorProps {
452
635
  value: Takuhon;
@@ -506,4 +689,4 @@ interface AdminEditorProps {
506
689
  */
507
690
  declare function AdminEditor({ initialDocument, onSave, onReload, onExport, onImport, formatLocale, uploadAsset, }: AdminEditorProps): React.JSX.Element;
508
691
 
509
- export { AdminEditor, type AdminEditorProps, type AdminLabelKey, type AdminSaveOutcome, type AssetUploadResult, CareersForm, type CareersFormProps, CheckboxField, type CheckboxFieldProps, Field, type FieldControlProps, type FieldErrorIndex, type FieldErrorLike, type FieldProps, GravatarField, type GravatarFieldProps, ImageField, type ImageFieldProps, LinksForm, type LinksFormProps, LocaleTabs, type LocaleTabsProps, NO_FIELD_ERRORS, ProfileForm, type ProfileFormProps, ProjectsForm, type ProjectsFormProps, RawJsonEditor, type RawJsonEditorProps, Repeater, type RepeaterProps, SelectField, type SelectFieldProps, type SelectOption, SettingsForm, type SettingsFormProps, SkillsForm, type SkillsFormProps, TextAreaField, type TextAreaFieldProps, TextField, type TextFieldProps, type UploadAsset, canonicalPointer, collectErrorsUnder, errorsAt, getAdminLabel, hasErrorsUnder, indexErrors, indexValidationErrors };
692
+ export { ADMIN_SECTIONS, AdminEditor, type AdminEditorProps, type AdminLabelKey, type AdminSaveOutcome, type AdminSection, type AssetUploadResult, CheckboxField, type CheckboxFieldProps, type CustomFieldContext, EMPTY_REGISTRY, Field, type FieldControlProps, type FieldErrorIndex, type FieldErrorLike, type FieldHint, type FieldKind, type FieldProps, type FieldRegistry, GravatarField, type GravatarFieldProps, ImageField, type ImageFieldProps, LocaleTabs, type LocaleTabsProps, NO_FIELD_ERRORS, RawJsonEditor, type RawJsonEditorProps, Repeater, type RepeaterProps, SECTION_REGISTRY, SchemaForm, type SchemaFormProps, type SchemaNode, SelectField, type SelectFieldProps, type SelectOption, TextAreaField, type TextAreaFieldProps, TextField, type TextFieldProps, type UploadAsset, canonicalPointer, classifyNode, collectErrorsUnder, errorsAt, getAdminLabel, hasErrorsUnder, hintAt, humanize, indexErrors, indexValidationErrors, sectionFieldKind };