@takuhon/ui 0.11.0 → 0.13.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/ActivitySection.module-SQFQYFCW.module.css +18 -0
- package/dist/CvView.module-4V6YB2ID.module.css +143 -0
- package/dist/ImageField.module-CC3IBMDB.module.css +23 -0
- package/dist/admin/index.d.ts +62 -6
- package/dist/admin/index.js +343 -261
- package/dist/admin/index.js.map +1 -1
- package/dist/index.d.ts +45 -4
- package/dist/index.js +508 -270
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.section {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--takuhon-space-3);
|
|
5
|
+
margin-top: var(--takuhon-space-5);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.heading {
|
|
9
|
+
margin: 0;
|
|
10
|
+
font-size: var(--takuhon-font-size-xl);
|
|
11
|
+
color: var(--takuhon-color-text);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* The card is a fixed-width SVG; scale it down on narrow viewports. */
|
|
15
|
+
.card svg {
|
|
16
|
+
max-width: 100%;
|
|
17
|
+
height: auto;
|
|
18
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/* A4 single-column résumé. On screen the article is a centered "sheet" on a
|
|
2
|
+
neutral backdrop; @media print drops the backdrop and margins so the page
|
|
3
|
+
fills the sheet, and break-inside:avoid keeps entries from splitting across
|
|
4
|
+
pages. Mirrors the CLI's renderCvHtml so the two surfaces match. */
|
|
5
|
+
|
|
6
|
+
.cv {
|
|
7
|
+
display: block;
|
|
8
|
+
width: 210mm;
|
|
9
|
+
min-height: 297mm;
|
|
10
|
+
margin: 1.5rem auto;
|
|
11
|
+
padding: 18mm 16mm;
|
|
12
|
+
background: #fff;
|
|
13
|
+
color: var(--takuhon-color-text, #1a1a1a);
|
|
14
|
+
font-size: 13px;
|
|
15
|
+
line-height: 1.5;
|
|
16
|
+
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.15);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.header {
|
|
20
|
+
border-bottom: 2px solid var(--takuhon-color-text, #1a1a1a);
|
|
21
|
+
padding-bottom: 0.6rem;
|
|
22
|
+
margin-bottom: 0.4rem;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.name {
|
|
26
|
+
font-size: 1.7rem;
|
|
27
|
+
margin: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.tagline {
|
|
31
|
+
font-size: 1.05rem;
|
|
32
|
+
color: var(--takuhon-color-text-muted, #555);
|
|
33
|
+
margin: 0.15rem 0 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.contact {
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-wrap: wrap;
|
|
39
|
+
gap: 0.25rem 1rem;
|
|
40
|
+
color: var(--takuhon-color-text-muted, #555);
|
|
41
|
+
font-size: 0.85rem;
|
|
42
|
+
margin: 0.4rem 0 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.contact a {
|
|
46
|
+
color: var(--takuhon-color-accent, #0b5fff);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.bio {
|
|
50
|
+
margin: 0.75rem 0 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.section {
|
|
54
|
+
margin-top: 1.1rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.heading {
|
|
58
|
+
font-size: 0.95rem;
|
|
59
|
+
text-transform: uppercase;
|
|
60
|
+
letter-spacing: 0.05em;
|
|
61
|
+
color: var(--takuhon-color-accent, #0b5fff);
|
|
62
|
+
border-bottom: 1px solid var(--takuhon-color-border, #d9d9d9);
|
|
63
|
+
margin: 0 0 0.5rem;
|
|
64
|
+
padding-bottom: 0.15rem;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.list {
|
|
68
|
+
list-style: none;
|
|
69
|
+
margin: 0;
|
|
70
|
+
padding: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.entry {
|
|
74
|
+
margin: 0 0 0.7rem;
|
|
75
|
+
break-inside: avoid;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.row {
|
|
79
|
+
display: flex;
|
|
80
|
+
justify-content: space-between;
|
|
81
|
+
gap: 1rem;
|
|
82
|
+
align-items: baseline;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.entryHeading {
|
|
86
|
+
font-size: 0.95rem;
|
|
87
|
+
margin: 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.entryHeading a {
|
|
91
|
+
color: var(--takuhon-color-accent, #0b5fff);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.dates {
|
|
95
|
+
color: var(--takuhon-color-text-muted, #555);
|
|
96
|
+
font-size: 0.8rem;
|
|
97
|
+
white-space: nowrap;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.sub {
|
|
101
|
+
color: var(--takuhon-color-text-muted, #555);
|
|
102
|
+
margin: 0.05rem 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.body {
|
|
106
|
+
margin: 0.2rem 0 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.chips {
|
|
110
|
+
list-style: none;
|
|
111
|
+
margin: 0;
|
|
112
|
+
padding: 0;
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-wrap: wrap;
|
|
115
|
+
gap: 0.3rem;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.chip {
|
|
119
|
+
border: 1px solid var(--takuhon-color-border, #d9d9d9);
|
|
120
|
+
border-radius: 1rem;
|
|
121
|
+
padding: 0.05rem 0.55rem;
|
|
122
|
+
font-size: 0.8rem;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@media print {
|
|
126
|
+
.cv {
|
|
127
|
+
width: auto;
|
|
128
|
+
min-height: 0;
|
|
129
|
+
margin: 0;
|
|
130
|
+
padding: 0;
|
|
131
|
+
box-shadow: none;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.entryHeading a,
|
|
135
|
+
.contact a {
|
|
136
|
+
color: var(--takuhon-color-text, #1a1a1a);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@page {
|
|
141
|
+
size: A4;
|
|
142
|
+
margin: 14mm;
|
|
143
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.uploadRow {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-wrap: wrap;
|
|
4
|
+
align-items: center;
|
|
5
|
+
gap: var(--takuhon-space-2);
|
|
6
|
+
margin-block-start: var(--takuhon-space-2);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.uploadLabel {
|
|
10
|
+
font-family: var(--takuhon-font-family);
|
|
11
|
+
font-size: var(--takuhon-font-size-sm);
|
|
12
|
+
color: var(--takuhon-color-text-muted);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.uploadStatus {
|
|
16
|
+
font-size: var(--takuhon-font-size-sm);
|
|
17
|
+
color: var(--takuhon-color-text-muted);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.uploadError {
|
|
21
|
+
font-size: var(--takuhon-font-size-sm);
|
|
22
|
+
color: #b42318;
|
|
23
|
+
}
|
package/dist/admin/index.d.ts
CHANGED
|
@@ -135,6 +135,47 @@ interface CheckboxFieldProps {
|
|
|
135
135
|
*/
|
|
136
136
|
declare function CheckboxField({ label, checked, onChange }: CheckboxFieldProps): React.JSX.Element;
|
|
137
137
|
|
|
138
|
+
/** Outcome of an asset upload, returned by {@link UploadAsset}. */
|
|
139
|
+
type AssetUploadResult = {
|
|
140
|
+
status: 'uploaded';
|
|
141
|
+
url: string;
|
|
142
|
+
publicUrl: string;
|
|
143
|
+
} | {
|
|
144
|
+
status: 'error';
|
|
145
|
+
message?: string;
|
|
146
|
+
};
|
|
147
|
+
/**
|
|
148
|
+
* Upload one image file and resolve where it now lives. Supplied by the host's
|
|
149
|
+
* transport layer (it carries the admin token / origin); the UI stays
|
|
150
|
+
* transport-agnostic.
|
|
151
|
+
*/
|
|
152
|
+
type UploadAsset = (file: File) => Promise<AssetUploadResult>;
|
|
153
|
+
interface ImageFieldProps {
|
|
154
|
+
label: string;
|
|
155
|
+
/** Current image URL stored in the document. */
|
|
156
|
+
value: string;
|
|
157
|
+
onChange: (url: string) => void;
|
|
158
|
+
errors?: readonly string[];
|
|
159
|
+
hint?: string;
|
|
160
|
+
/**
|
|
161
|
+
* When provided, a file-upload control is rendered alongside the URL input;
|
|
162
|
+
* a successful upload sets the URL to the returned (relative) path. When
|
|
163
|
+
* omitted, the field is URL-only.
|
|
164
|
+
*/
|
|
165
|
+
uploadAsset?: UploadAsset;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Avatar / image field: a URL input plus, when {@link ImageFieldProps.uploadAsset}
|
|
169
|
+
* is supplied, a file picker that uploads the chosen image and writes the
|
|
170
|
+
* returned URL back into the field. The relative `url` (not the absolute
|
|
171
|
+
* `publicUrl`) is stored so the reference stays valid across origins.
|
|
172
|
+
*
|
|
173
|
+
* No `<img>` preview is rendered: the admin CSP is `img-src 'self' blob:`, so a
|
|
174
|
+
* preview of an arbitrary external URL would be blocked — the URL input is the
|
|
175
|
+
* portable, CSP-safe surface, and the public preview shows the real image.
|
|
176
|
+
*/
|
|
177
|
+
declare function ImageField({ label, value, onChange, errors, hint, uploadAsset, }: ImageFieldProps): React.JSX.Element;
|
|
178
|
+
|
|
138
179
|
interface LocaleTabsProps {
|
|
139
180
|
/** Group label, e.g. `"Display name"`. */
|
|
140
181
|
label: string;
|
|
@@ -229,6 +270,8 @@ declare const EN: {
|
|
|
229
270
|
readonly 'status.error': "Something went wrong. Please try again.";
|
|
230
271
|
readonly 'status.imported': "Imported. Review the fields, then save to apply.";
|
|
231
272
|
readonly 'status.importInvalid': "The imported file is not a valid takuhon document.";
|
|
273
|
+
readonly 'status.uploading': "Uploading…";
|
|
274
|
+
readonly 'status.uploadError': "Upload failed. Please try again.";
|
|
232
275
|
readonly 'section.profile': "Profile";
|
|
233
276
|
readonly 'section.about': "About";
|
|
234
277
|
readonly 'section.links': "Links";
|
|
@@ -240,6 +283,7 @@ declare const EN: {
|
|
|
240
283
|
readonly 'field.tagline': "Tagline";
|
|
241
284
|
readonly 'field.bio': "Bio";
|
|
242
285
|
readonly 'field.avatarUrl': "Avatar URL";
|
|
286
|
+
readonly 'field.avatarUpload': "Upload image";
|
|
243
287
|
readonly 'field.avatarAlt': "Avatar alternative text";
|
|
244
288
|
readonly 'field.location.country': "Country";
|
|
245
289
|
readonly 'field.location.region': "Region";
|
|
@@ -283,6 +327,7 @@ declare const EN: {
|
|
|
283
327
|
readonly 'empty.projects': "No projects yet.";
|
|
284
328
|
readonly 'empty.skills': "No skills yet.";
|
|
285
329
|
readonly 'hint.avatarNoUpload': "Paste an image URL. Uploading image files is not available yet.";
|
|
330
|
+
readonly 'hint.avatarUpload': "Paste an image URL, or upload a JPEG, PNG, WebP, or GIF.";
|
|
286
331
|
readonly 'hint.month': "Format: YYYY-MM (e.g. 2024-03).";
|
|
287
332
|
readonly 'hint.country': "ISO 3166-1 alpha-2 code, e.g. US.";
|
|
288
333
|
readonly 'hint.tags': "Comma-separated.";
|
|
@@ -306,13 +351,18 @@ interface ProfileFormProps {
|
|
|
306
351
|
locales: readonly LocaleTag[];
|
|
307
352
|
errors?: FieldErrorIndex;
|
|
308
353
|
formatLocale?: (locale: LocaleTag) => string;
|
|
354
|
+
/**
|
|
355
|
+
* Upload an avatar image file. When provided, the avatar field offers a file
|
|
356
|
+
* picker; when omitted, it stays URL-only.
|
|
357
|
+
*/
|
|
358
|
+
uploadAsset?: UploadAsset;
|
|
309
359
|
}
|
|
310
360
|
/**
|
|
311
|
-
* Basic profile + About: display name, tagline, bio, avatar
|
|
312
|
-
*
|
|
313
|
-
* the
|
|
361
|
+
* Basic profile + About: display name, tagline, bio, avatar, and a structured
|
|
362
|
+
* location (spec §6.5 / §14.2). The avatar accepts a URL and, when
|
|
363
|
+
* `uploadAsset` is supplied by the host, an uploaded image file.
|
|
314
364
|
*/
|
|
315
|
-
declare function ProfileForm({ value, onChange, locales, errors, formatLocale, }: ProfileFormProps): React.JSX.Element;
|
|
365
|
+
declare function ProfileForm({ value, onChange, locales, errors, formatLocale, uploadAsset, }: ProfileFormProps): React.JSX.Element;
|
|
316
366
|
|
|
317
367
|
interface LinksFormProps {
|
|
318
368
|
value: readonly Link[];
|
|
@@ -404,6 +454,12 @@ interface AdminEditorProps {
|
|
|
404
454
|
*/
|
|
405
455
|
onImport?: () => Promise<unknown>;
|
|
406
456
|
formatLocale?: (locale: LocaleTag) => string;
|
|
457
|
+
/**
|
|
458
|
+
* Upload an avatar image file (host-supplied; carries the admin token). When
|
|
459
|
+
* provided, the profile form's avatar field offers a file picker; otherwise
|
|
460
|
+
* the avatar stays URL-only.
|
|
461
|
+
*/
|
|
462
|
+
uploadAsset?: UploadAsset;
|
|
407
463
|
}
|
|
408
464
|
/**
|
|
409
465
|
* Top-level admin editor: holds the working draft, switches between the field
|
|
@@ -411,6 +467,6 @@ interface AdminEditorProps {
|
|
|
411
467
|
* delegating persistence to the host via `onSave` (transport-agnostic). Server
|
|
412
468
|
* (RFC 7807) and client (`validate`) errors map to the same fields.
|
|
413
469
|
*/
|
|
414
|
-
declare function AdminEditor({ initialDocument, onSave, onReload, onExport, onImport, formatLocale, }: AdminEditorProps): React.JSX.Element;
|
|
470
|
+
declare function AdminEditor({ initialDocument, onSave, onReload, onExport, onImport, formatLocale, uploadAsset, }: AdminEditorProps): React.JSX.Element;
|
|
415
471
|
|
|
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 };
|
|
472
|
+
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, 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 };
|