@takuhon/ui 0.9.0 → 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.
- package/dist/AdminEditor.module-ABYQXFN4.module.css +116 -0
- package/dist/Field.module-CJPK45H7.module.css +34 -0
- package/dist/LocaleTabs.module-IEEC6Q27.module.css +116 -0
- package/dist/RawJsonEditor.module-NGGM3IBY.module.css +64 -0
- package/dist/Repeater.module-MWSEKS4G.module.css +102 -0
- package/dist/admin/index.d.ts +416 -0
- package/dist/admin/index.js +1621 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/controls.module-CMB7V22N.module.css +48 -0
- package/dist/sections.module-ZVBKZHDE.module.css +22 -0
- package/package.json +6 -2
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { ValidationError, LocaleTag, Profile, Link, Career, Project, Skill, Settings, Takuhon } from '@takuhon/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Field-error plumbing shared by the admin form components.
|
|
5
|
+
*
|
|
6
|
+
* Validation failures reach the editor in two shapes that mean the same thing:
|
|
7
|
+
*
|
|
8
|
+
* - `@takuhon/core`'s {@link ValidationError}, produced by a client-side
|
|
9
|
+
* `validate()` call, carries an RFC 6901 `pointer` such as
|
|
10
|
+
* `"/profile/displayName/en"`.
|
|
11
|
+
* - The server's RFC 7807 response (`api.md §5`) carries the same location as a
|
|
12
|
+
* `path` with a leading `#` fragment marker, e.g. `"#/profile/displayName/en"`.
|
|
13
|
+
*
|
|
14
|
+
* Both are normalized to a single canonical pointer so a field can look up its
|
|
15
|
+
* own errors regardless of where they came from.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/** A field error in either the core (`pointer`) or RFC 7807 wire (`path`) shape. */
|
|
19
|
+
interface FieldErrorLike {
|
|
20
|
+
pointer?: string;
|
|
21
|
+
path?: string;
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Canonical RFC 6901 pointer: no leading `#`, a leading `/` for any non-root
|
|
26
|
+
* location, and `""` for the document root. Idempotent.
|
|
27
|
+
*/
|
|
28
|
+
declare function canonicalPointer(raw: string): string;
|
|
29
|
+
/** Maps a canonical pointer to the messages reported at that exact location. */
|
|
30
|
+
type FieldErrorIndex = ReadonlyMap<string, readonly string[]>;
|
|
31
|
+
/** Group a flat error list by canonical pointer, preserving message order. */
|
|
32
|
+
declare function indexErrors(errors: readonly FieldErrorLike[]): FieldErrorIndex;
|
|
33
|
+
/** Convenience over {@link indexErrors} for a core `validate()` failure list. */
|
|
34
|
+
declare function indexValidationErrors(errors: readonly ValidationError[]): FieldErrorIndex;
|
|
35
|
+
/** Messages reported at exactly `pointer` (empty array when none). */
|
|
36
|
+
declare function errorsAt(index: FieldErrorIndex, pointer: string): readonly string[];
|
|
37
|
+
/**
|
|
38
|
+
* Whether any error sits at or below `prefix` — used to badge a section or a
|
|
39
|
+
* repeater item that contains an invalid field deeper in the tree.
|
|
40
|
+
*/
|
|
41
|
+
declare function hasErrorsUnder(index: FieldErrorIndex, prefix: string): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* All messages at or below `prefix`, in pointer order. Used by aggregate
|
|
44
|
+
* controls (a comma-separated list edited as one field) so item-level failures
|
|
45
|
+
* reported at `${prefix}/${itemIndex}` still surface on the single input.
|
|
46
|
+
*/
|
|
47
|
+
declare function collectErrorsUnder(index: FieldErrorIndex, prefix: string): string[];
|
|
48
|
+
/** An empty index, handy as a default prop. */
|
|
49
|
+
declare const NO_FIELD_ERRORS: FieldErrorIndex;
|
|
50
|
+
|
|
51
|
+
/** Identifiers a {@link Field} hands to the control it wraps. */
|
|
52
|
+
interface FieldControlProps {
|
|
53
|
+
/** `id` for the control, paired with the rendered `<label htmlFor>`. */
|
|
54
|
+
controlId: string;
|
|
55
|
+
/** `aria-describedby` value linking the control to its hint/error text. */
|
|
56
|
+
describedBy: string | undefined;
|
|
57
|
+
/** Whether the control should advertise `aria-invalid`. */
|
|
58
|
+
invalid: boolean;
|
|
59
|
+
}
|
|
60
|
+
interface FieldProps {
|
|
61
|
+
/** Visible label text. */
|
|
62
|
+
label: string;
|
|
63
|
+
/** Validation messages to render beneath the control. */
|
|
64
|
+
errors?: readonly string[];
|
|
65
|
+
/** Optional helper text shown between the label and the control. */
|
|
66
|
+
hint?: string;
|
|
67
|
+
/** Marks the field visually and via `aria-required` on the control. */
|
|
68
|
+
required?: boolean;
|
|
69
|
+
/** Render the control, wired with the supplied accessibility ids. */
|
|
70
|
+
children: (props: FieldControlProps) => React.ReactNode;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Layout + accessibility wrapper for a single form control. Owns the `id`
|
|
74
|
+
* wiring so the label, hint, error list, and control stay associated for
|
|
75
|
+
* screen readers (WCAG 2.1 AA, spec §8.5). Presentational only — it holds no
|
|
76
|
+
* value state; the wrapped control is controlled by its parent.
|
|
77
|
+
*/
|
|
78
|
+
declare function Field({ label, errors, hint, required, children }: FieldProps): React.JSX.Element;
|
|
79
|
+
|
|
80
|
+
interface TextFieldProps {
|
|
81
|
+
label: string;
|
|
82
|
+
value: string;
|
|
83
|
+
onChange: (value: string) => void;
|
|
84
|
+
errors?: readonly string[];
|
|
85
|
+
hint?: string;
|
|
86
|
+
required?: boolean;
|
|
87
|
+
placeholder?: string;
|
|
88
|
+
/**
|
|
89
|
+
* HTML input type. Defaults to `'text'`; `'url'` for link/avatar URLs,
|
|
90
|
+
* `'month'` for `YearMonth` values (a native month picker emits `YYYY-MM`).
|
|
91
|
+
*/
|
|
92
|
+
type?: 'text' | 'url' | 'email' | 'month';
|
|
93
|
+
inputMode?: React.HTMLAttributes<HTMLInputElement>['inputMode'];
|
|
94
|
+
}
|
|
95
|
+
/** A labelled single-line text input wired for accessibility via {@link Field}. */
|
|
96
|
+
declare function TextField({ label, value, onChange, errors, hint, required, placeholder, type, inputMode, }: TextFieldProps): React.JSX.Element;
|
|
97
|
+
|
|
98
|
+
interface TextAreaFieldProps {
|
|
99
|
+
label: string;
|
|
100
|
+
value: string;
|
|
101
|
+
onChange: (value: string) => void;
|
|
102
|
+
errors?: readonly string[];
|
|
103
|
+
hint?: string;
|
|
104
|
+
required?: boolean;
|
|
105
|
+
placeholder?: string;
|
|
106
|
+
rows?: number;
|
|
107
|
+
}
|
|
108
|
+
/** A labelled multi-line text input wired for accessibility via {@link Field}. */
|
|
109
|
+
declare function TextAreaField({ label, value, onChange, errors, hint, required, placeholder, rows, }: TextAreaFieldProps): React.JSX.Element;
|
|
110
|
+
|
|
111
|
+
interface SelectOption {
|
|
112
|
+
value: string;
|
|
113
|
+
label: string;
|
|
114
|
+
}
|
|
115
|
+
interface SelectFieldProps {
|
|
116
|
+
label: string;
|
|
117
|
+
value: string;
|
|
118
|
+
options: readonly SelectOption[];
|
|
119
|
+
onChange: (value: string) => void;
|
|
120
|
+
errors?: readonly string[];
|
|
121
|
+
hint?: string;
|
|
122
|
+
required?: boolean;
|
|
123
|
+
}
|
|
124
|
+
/** A labelled `<select>` wired for accessibility via {@link Field}. */
|
|
125
|
+
declare function SelectField({ label, value, options, onChange, errors, hint, required, }: SelectFieldProps): React.JSX.Element;
|
|
126
|
+
|
|
127
|
+
interface CheckboxFieldProps {
|
|
128
|
+
label: string;
|
|
129
|
+
checked: boolean;
|
|
130
|
+
onChange: (checked: boolean) => void;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* A labelled checkbox. Laid out checkbox-first (distinct from the stacked
|
|
134
|
+
* {@link Field} controls), with the label associated via `htmlFor`.
|
|
135
|
+
*/
|
|
136
|
+
declare function CheckboxField({ label, checked, onChange }: CheckboxFieldProps): React.JSX.Element;
|
|
137
|
+
|
|
138
|
+
interface LocaleTabsProps {
|
|
139
|
+
/** Group label, e.g. `"Display name"`. */
|
|
140
|
+
label: string;
|
|
141
|
+
/** Current localized value (BCP-47 tag → string), or undefined when empty. */
|
|
142
|
+
value: Record<LocaleTag, string> | undefined;
|
|
143
|
+
/** Locales offered as tabs (typically `settings.availableLocales`). */
|
|
144
|
+
locales: readonly LocaleTag[];
|
|
145
|
+
/**
|
|
146
|
+
* Receives the next localized record. Empty strings are pruned, and a record
|
|
147
|
+
* that becomes empty is reported as `undefined` so the field is omitted
|
|
148
|
+
* rather than emitted as `{}` (which fails the schema's `minProperties`).
|
|
149
|
+
*/
|
|
150
|
+
onChange: (next: Record<LocaleTag, string> | undefined) => void;
|
|
151
|
+
/** Render a `<textarea>` instead of an `<input>` for body-length text. */
|
|
152
|
+
multiline?: boolean;
|
|
153
|
+
required?: boolean;
|
|
154
|
+
hint?: string;
|
|
155
|
+
/** Full error index; per-locale errors are looked up at `${pointer}/${locale}`. */
|
|
156
|
+
errors?: FieldErrorIndex;
|
|
157
|
+
/** Base RFC 6901 pointer to this localized field, e.g. `/profile/displayName`. */
|
|
158
|
+
pointer?: string;
|
|
159
|
+
/** Render a friendlier tab label than the raw locale tag. */
|
|
160
|
+
formatLocale?: (locale: LocaleTag) => string;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Edits a localized string map (`LocalizedTitle` / `LocalizedBody`) behind one
|
|
164
|
+
* tab per locale (spec §14.2 "多言語タブ"). Implements the WAI-ARIA Tabs
|
|
165
|
+
* pattern — roving tabindex, arrow/Home/End navigation, a single shared
|
|
166
|
+
* `tabpanel` — so it stays keyboard-operable and screen-reader-correct
|
|
167
|
+
* (WCAG 2.1 AA, spec §8.5).
|
|
168
|
+
*/
|
|
169
|
+
declare function LocaleTabs({ label, value, locales, onChange, multiline, required, hint, errors, pointer, formatLocale, }: LocaleTabsProps): React.JSX.Element;
|
|
170
|
+
|
|
171
|
+
interface RepeaterProps<T> {
|
|
172
|
+
/** Group caption rendered as the fieldset legend, e.g. `"Links"`. */
|
|
173
|
+
legend: string;
|
|
174
|
+
items: readonly T[];
|
|
175
|
+
/** Receives the next array after an add / remove / reorder / item edit. */
|
|
176
|
+
onChange: (next: T[]) => void;
|
|
177
|
+
/** Render one item's fields; `update` replaces that item in place. */
|
|
178
|
+
renderItem: (item: T, update: (next: T) => void, index: number) => React.ReactNode;
|
|
179
|
+
/** Factory for the entry appended by the Add button. */
|
|
180
|
+
createItem: () => T;
|
|
181
|
+
/** Stable React key per item. Defaults to the index. */
|
|
182
|
+
keyOf?: (item: T, index: number) => string;
|
|
183
|
+
/** Human label for an item, used in the item caption and button names. */
|
|
184
|
+
itemLabel: (item: T, index: number) => string;
|
|
185
|
+
addLabel?: string;
|
|
186
|
+
removeLabel?: string;
|
|
187
|
+
moveUpLabel?: string;
|
|
188
|
+
moveDownLabel?: string;
|
|
189
|
+
/** Shown when there are no items yet. */
|
|
190
|
+
emptyHint?: string;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Add / remove / reorder editor for a repeating array field (spec §14.2
|
|
194
|
+
* "リピーター"). Reordering uses keyboard-operable Move up / Move down buttons
|
|
195
|
+
* rather than drag-and-drop so the control meets WCAG 2.1 AA (spec §8.5)
|
|
196
|
+
* without a pointer.
|
|
197
|
+
*/
|
|
198
|
+
declare function Repeater<T>({ legend, items, onChange, renderItem, createItem, keyOf, itemLabel, addLabel, removeLabel, moveUpLabel, moveDownLabel, emptyHint, }: RepeaterProps<T>): React.JSX.Element;
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* UI strings for the admin editor.
|
|
202
|
+
*
|
|
203
|
+
* The admin surface is operator-only, so a single English dictionary ships
|
|
204
|
+
* today; the lookup mirrors `getUILabel` (exact locale → base subtag →
|
|
205
|
+
* English) so locales can be added later without touching call sites. Keys are
|
|
206
|
+
* derived from the English dictionary, so every key is guaranteed to resolve.
|
|
207
|
+
*/
|
|
208
|
+
|
|
209
|
+
declare const EN: {
|
|
210
|
+
readonly 'app.title': "takuhon admin";
|
|
211
|
+
readonly 'toolbar.label': "Editor actions";
|
|
212
|
+
readonly 'mode.label': "Editing mode";
|
|
213
|
+
readonly 'mode.form': "Form";
|
|
214
|
+
readonly 'mode.advanced': "Advanced (JSON)";
|
|
215
|
+
readonly 'action.save': "Save";
|
|
216
|
+
readonly 'action.reload': "Reload";
|
|
217
|
+
readonly 'action.export': "Export";
|
|
218
|
+
readonly 'action.import': "Import";
|
|
219
|
+
readonly 'action.add': "Add";
|
|
220
|
+
readonly 'action.remove': "Remove";
|
|
221
|
+
readonly 'action.moveUp': "Move up";
|
|
222
|
+
readonly 'action.moveDown': "Move down";
|
|
223
|
+
readonly 'status.saving': "Saving…";
|
|
224
|
+
readonly 'status.saved': "Saved.";
|
|
225
|
+
readonly 'status.loading': "Loading…";
|
|
226
|
+
readonly 'status.conflict': "The profile changed on the server since it was loaded. Reload, then reapply the edits.";
|
|
227
|
+
readonly 'status.invalid': "Some fields need attention before saving.";
|
|
228
|
+
readonly 'status.fixSummary': "Please fix the following:";
|
|
229
|
+
readonly 'status.error': "Something went wrong. Please try again.";
|
|
230
|
+
readonly 'status.imported': "Imported. Review the fields, then save to apply.";
|
|
231
|
+
readonly 'status.importInvalid': "The imported file is not a valid takuhon document.";
|
|
232
|
+
readonly 'section.profile': "Profile";
|
|
233
|
+
readonly 'section.about': "About";
|
|
234
|
+
readonly 'section.links': "Links";
|
|
235
|
+
readonly 'section.careers': "Experience";
|
|
236
|
+
readonly 'section.projects': "Projects";
|
|
237
|
+
readonly 'section.skills': "Skills";
|
|
238
|
+
readonly 'section.settings': "Settings";
|
|
239
|
+
readonly 'field.displayName': "Display name";
|
|
240
|
+
readonly 'field.tagline': "Tagline";
|
|
241
|
+
readonly 'field.bio': "Bio";
|
|
242
|
+
readonly 'field.avatarUrl': "Avatar URL";
|
|
243
|
+
readonly 'field.avatarAlt': "Avatar alternative text";
|
|
244
|
+
readonly 'field.location.country': "Country";
|
|
245
|
+
readonly 'field.location.region': "Region";
|
|
246
|
+
readonly 'field.location.locality': "Locality";
|
|
247
|
+
readonly 'field.location.display': "Location (display)";
|
|
248
|
+
readonly 'field.link.type': "Type";
|
|
249
|
+
readonly 'field.link.url': "URL";
|
|
250
|
+
readonly 'field.link.label': "Label";
|
|
251
|
+
readonly 'field.link.iconUrl': "Icon URL";
|
|
252
|
+
readonly 'field.link.featured': "Featured";
|
|
253
|
+
readonly 'field.career.organization': "Organization";
|
|
254
|
+
readonly 'field.career.role': "Role";
|
|
255
|
+
readonly 'field.career.description': "Description";
|
|
256
|
+
readonly 'field.career.startDate': "Start";
|
|
257
|
+
readonly 'field.career.endDate': "End";
|
|
258
|
+
readonly 'field.career.isCurrent': "Current position";
|
|
259
|
+
readonly 'field.career.url': "URL";
|
|
260
|
+
readonly 'field.project.title': "Title";
|
|
261
|
+
readonly 'field.project.description': "Description";
|
|
262
|
+
readonly 'field.project.url': "URL";
|
|
263
|
+
readonly 'field.project.tags': "Tags";
|
|
264
|
+
readonly 'field.project.highlighted': "Highlighted";
|
|
265
|
+
readonly 'field.project.startDate': "Start";
|
|
266
|
+
readonly 'field.project.endDate': "End";
|
|
267
|
+
readonly 'field.skill.label': "Label";
|
|
268
|
+
readonly 'field.skill.category': "Category";
|
|
269
|
+
readonly 'field.settings.defaultLocale': "Default locale";
|
|
270
|
+
readonly 'field.settings.fallbackLocale': "Fallback locale";
|
|
271
|
+
readonly 'field.settings.availableLocales': "Available locales";
|
|
272
|
+
readonly 'field.settings.theme': "Theme";
|
|
273
|
+
readonly 'field.settings.showPoweredBy': "Show the \"Powered by takuhon\" footer";
|
|
274
|
+
readonly 'field.settings.enableJsonLd': "Emit Schema.org JSON-LD";
|
|
275
|
+
readonly 'field.settings.enableApi': "Expose the public read API";
|
|
276
|
+
readonly 'field.settings.enableAnalytics': "Enable first-party analytics";
|
|
277
|
+
readonly 'item.link': "Link";
|
|
278
|
+
readonly 'item.career': "Position";
|
|
279
|
+
readonly 'item.project': "Project";
|
|
280
|
+
readonly 'item.skill': "Skill";
|
|
281
|
+
readonly 'empty.links': "No links yet.";
|
|
282
|
+
readonly 'empty.careers': "No positions yet.";
|
|
283
|
+
readonly 'empty.projects': "No projects yet.";
|
|
284
|
+
readonly 'empty.skills': "No skills yet.";
|
|
285
|
+
readonly 'hint.avatarNoUpload': "Paste an image URL. Uploading image files is not available yet.";
|
|
286
|
+
readonly 'hint.month': "Format: YYYY-MM (e.g. 2024-03).";
|
|
287
|
+
readonly 'hint.country': "ISO 3166-1 alpha-2 code, e.g. US.";
|
|
288
|
+
readonly 'hint.tags': "Comma-separated.";
|
|
289
|
+
readonly 'hint.locales': "Comma-separated BCP-47 tags, e.g. en, ja. Drives the language tabs above.";
|
|
290
|
+
readonly 'advanced.hint': "Edit the entire document as JSON. Edits apply only while the JSON is valid.";
|
|
291
|
+
readonly 'advanced.invalid': "The JSON is not a valid takuhon document:";
|
|
292
|
+
readonly 'option.none': "(none)";
|
|
293
|
+
};
|
|
294
|
+
/** Every label key understood by {@link getAdminLabel}. */
|
|
295
|
+
type AdminLabelKey = keyof typeof EN;
|
|
296
|
+
/**
|
|
297
|
+
* Resolve an admin label for a locale, falling back to the base language
|
|
298
|
+
* subtag and then English. English is always present, so the return is always
|
|
299
|
+
* a string.
|
|
300
|
+
*/
|
|
301
|
+
declare function getAdminLabel(key: AdminLabelKey, locale?: LocaleTag): string;
|
|
302
|
+
|
|
303
|
+
interface ProfileFormProps {
|
|
304
|
+
value: Profile;
|
|
305
|
+
onChange: (next: Profile) => void;
|
|
306
|
+
locales: readonly LocaleTag[];
|
|
307
|
+
errors?: FieldErrorIndex;
|
|
308
|
+
formatLocale?: (locale: LocaleTag) => string;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Basic profile + About: display name, tagline, bio, avatar URL, and a
|
|
312
|
+
* structured location (spec §6.5 / §14.2). Image upload is not wired yet, so
|
|
313
|
+
* the avatar is a URL field only.
|
|
314
|
+
*/
|
|
315
|
+
declare function ProfileForm({ value, onChange, locales, errors, formatLocale, }: ProfileFormProps): React.JSX.Element;
|
|
316
|
+
|
|
317
|
+
interface LinksFormProps {
|
|
318
|
+
value: readonly Link[];
|
|
319
|
+
onChange: (next: Link[]) => void;
|
|
320
|
+
locales: readonly LocaleTag[];
|
|
321
|
+
errors?: FieldErrorIndex;
|
|
322
|
+
formatLocale?: (locale: LocaleTag) => string;
|
|
323
|
+
}
|
|
324
|
+
/** SNS / profile links with builtin-vs-custom typing (spec §6.6 / §14.2). */
|
|
325
|
+
declare function LinksForm({ value, onChange, locales, errors, formatLocale, }: LinksFormProps): React.JSX.Element;
|
|
326
|
+
|
|
327
|
+
interface CareersFormProps {
|
|
328
|
+
value: readonly Career[];
|
|
329
|
+
onChange: (next: Career[]) => void;
|
|
330
|
+
locales: readonly LocaleTag[];
|
|
331
|
+
errors?: FieldErrorIndex;
|
|
332
|
+
formatLocale?: (locale: LocaleTag) => string;
|
|
333
|
+
}
|
|
334
|
+
/** Work experience (spec §6.7 / §14.2 "職歴"). */
|
|
335
|
+
declare function CareersForm({ value, onChange, locales, errors, formatLocale, }: CareersFormProps): React.JSX.Element;
|
|
336
|
+
|
|
337
|
+
interface ProjectsFormProps {
|
|
338
|
+
value: readonly Project[];
|
|
339
|
+
onChange: (next: Project[]) => void;
|
|
340
|
+
locales: readonly LocaleTag[];
|
|
341
|
+
errors?: FieldErrorIndex;
|
|
342
|
+
formatLocale?: (locale: LocaleTag) => string;
|
|
343
|
+
}
|
|
344
|
+
/** Projects (spec §6.8 / §14.2). */
|
|
345
|
+
declare function ProjectsForm({ value, onChange, locales, errors, formatLocale, }: ProjectsFormProps): React.JSX.Element;
|
|
346
|
+
|
|
347
|
+
interface SkillsFormProps {
|
|
348
|
+
value: readonly Skill[];
|
|
349
|
+
onChange: (next: Skill[]) => void;
|
|
350
|
+
errors?: FieldErrorIndex;
|
|
351
|
+
}
|
|
352
|
+
/** Skills (spec §6.9 / §14.2). `label` is a plain string, not localized. */
|
|
353
|
+
declare function SkillsForm({ value, onChange, errors, }: SkillsFormProps): React.JSX.Element;
|
|
354
|
+
|
|
355
|
+
interface SettingsFormProps {
|
|
356
|
+
value: Settings;
|
|
357
|
+
onChange: (next: Settings) => void;
|
|
358
|
+
errors?: FieldErrorIndex;
|
|
359
|
+
formatLocale?: (locale: LocaleTag) => string;
|
|
360
|
+
}
|
|
361
|
+
/** Site settings: locales, theme, and feature toggles (spec §6.20 / §14.2). */
|
|
362
|
+
declare function SettingsForm({ value, onChange, errors, formatLocale, }: SettingsFormProps): React.JSX.Element;
|
|
363
|
+
|
|
364
|
+
interface RawJsonEditorProps {
|
|
365
|
+
value: Takuhon;
|
|
366
|
+
/** Called with the parsed document whenever the text is valid JSON + schema. */
|
|
367
|
+
onChange: (next: Takuhon) => void;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Advanced editing surface: the entire document as JSON text. Edits commit to
|
|
371
|
+
* the draft only when the text parses and passes schema validation, so the
|
|
372
|
+
* structured form view it shares state with never sees a malformed document.
|
|
373
|
+
* This is also the only way to edit sections without a dedicated form (spec
|
|
374
|
+
* §14.1 Phase 5 — JSON editor retained as the advanced mode).
|
|
375
|
+
*/
|
|
376
|
+
declare function RawJsonEditor({ value, onChange }: RawJsonEditorProps): React.JSX.Element;
|
|
377
|
+
|
|
378
|
+
/** Result of a save attempt, reported by the host's transport layer. */
|
|
379
|
+
type AdminSaveOutcome = {
|
|
380
|
+
status: 'saved';
|
|
381
|
+
version?: string;
|
|
382
|
+
} | {
|
|
383
|
+
status: 'conflict';
|
|
384
|
+
} | {
|
|
385
|
+
status: 'invalid';
|
|
386
|
+
errors: readonly FieldErrorLike[];
|
|
387
|
+
} | {
|
|
388
|
+
status: 'error';
|
|
389
|
+
message?: string;
|
|
390
|
+
};
|
|
391
|
+
interface AdminEditorProps {
|
|
392
|
+
/** Document to edit, typically loaded from `GET /api/admin/export`. */
|
|
393
|
+
initialDocument: Takuhon;
|
|
394
|
+
/** Persist the document. The editor validates client-side first. */
|
|
395
|
+
onSave: (document: Takuhon) => Promise<AdminSaveOutcome>;
|
|
396
|
+
/** Re-fetch the stored document, replacing the working draft. */
|
|
397
|
+
onReload?: () => Promise<Takuhon>;
|
|
398
|
+
/** Download / hand off the current draft (Export button). */
|
|
399
|
+
onExport?: (document: Takuhon) => void;
|
|
400
|
+
/**
|
|
401
|
+
* Provide a document to load (Import button). Returns raw parsed JSON, which
|
|
402
|
+
* the editor validates; `undefined` cancels. Validation lives here so an
|
|
403
|
+
* invalid file cannot break the draft invariant.
|
|
404
|
+
*/
|
|
405
|
+
onImport?: () => Promise<unknown>;
|
|
406
|
+
formatLocale?: (locale: LocaleTag) => string;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Top-level admin editor: holds the working draft, switches between the field
|
|
410
|
+
* forms and the raw-JSON advanced mode, and runs client-side validation before
|
|
411
|
+
* delegating persistence to the host via `onSave` (transport-agnostic). Server
|
|
412
|
+
* (RFC 7807) and client (`validate`) errors map to the same fields.
|
|
413
|
+
*/
|
|
414
|
+
declare function AdminEditor({ initialDocument, onSave, onReload, onExport, onImport, formatLocale, }: AdminEditorProps): React.JSX.Element;
|
|
415
|
+
|
|
416
|
+
export { AdminEditor, type AdminEditorProps, type AdminLabelKey, type AdminSaveOutcome, CareersForm, type CareersFormProps, CheckboxField, type CheckboxFieldProps, Field, type FieldControlProps, type FieldErrorIndex, type FieldErrorLike, type FieldProps, 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, canonicalPointer, collectErrorsUnder, errorsAt, getAdminLabel, hasErrorsUnder, indexErrors, indexValidationErrors };
|