@pipelex/mthds-form 0.12.1 → 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/README.md CHANGED
@@ -58,6 +58,14 @@ Submitting goes through the gate: `buildRunInputsSchema` → `prepareRunInputs`
58
58
 
59
59
  A file field (`document` or `image`) carries `formats`, the formats its slot accepts, and the control's hint, file-picker filter and check all read that one list. A host whose upload path takes fewer types narrows the whole tree once with `narrowFileFormats(fields, mimeTypes)` and has its server check uploads against the same list. [docs/upload-seam.md](docs/upload-seam.md) is everything a file control and its host owe each other.
60
60
 
61
+ The controls' own copy comes through `FieldStringsProvider`: `locale` picks a language pack (`fr`, `fr-FR`, `en`…, English when unknown) and `strings` overrides single keys on top. The field labels, helper lines and enum choices come from the method's own names and descriptions, never from the host:
62
+
63
+ ```tsx
64
+ <FieldStringsProvider locale="fr">{/* the form over buildRunFields(form, contract) */}</FieldStringsProvider>
65
+ ```
66
+
67
+ [docs/i18n.md](docs/i18n.md) has the language packs and the locale switch.
68
+
61
69
  ## Theming
62
70
 
63
71
  The controls carry Tailwind classes over the standard shadcn/ui semantic tokens (`--background`, `--input`, `--border`, `--primary`, …). A host that already defines those tokens gets controls that match its product with no configuration. A host with no Tailwind build loads the prebuilt `@pipelex/mthds-form/styles.css`. Both paths, and the exact token list, are in [docs/theming.md](docs/theming.md).
@@ -71,6 +79,7 @@ The controls carry Tailwind classes over the standard shadcn/ui semantic tokens
71
79
  | [docs/theming.md](docs/theming.md) | the token contract and host setup |
72
80
  | [docs/wire-correspondence.md](docs/wire-correspondence.md) | the name-for-name wire ↔ `RunField` mapping |
73
81
  | [docs/upload-seam.md](docs/upload-seam.md) | what a file control and its host owe each other: accepted formats and narrowing them, write-back paths, previews, the file card |
82
+ | [docs/i18n.md](docs/i18n.md) | wording a form for its reader: the controls' strings, the French set and the locale switch |
74
83
  | [docs/derivation-swap.md](docs/derivation-swap.md) | the record of the swap to the wire descriptor — what changed, what survived |
75
84
 
76
85
  ## Development
@@ -3,7 +3,7 @@ import { readStuffFile } from './chunk-RRTI7AIR.js';
3
3
  import { viewableUrl, isNativeDateNode, isNativeHtmlNode, readHtmlContent, isNativeCompositeNode, readCompositeContent, isNativeValueNode, readDateContent, formatDateContent, readImageContent, readDataUrl, readDocumentContent } from './chunk-CM4KVG53.js';
4
4
  import { conceptCategory } from './chunk-FABY3DEY.js';
5
5
  import { acceptLabel, acceptMap, isAcceptedFile, formatOfFilename, DOCUMENT_FORMATS, IMAGE_FORMATS } from './chunk-XCO6GTEC.js';
6
- import { isFilled } from './chunk-PM55AHRO.js';
6
+ import { shownAsOptional, isFilled } from './chunk-MJQ7CXUB.js';
7
7
  import { toStoredDateValue, toDateInputValue } from './chunk-GKV77RAS.js';
8
8
  import { ownProp } from './chunk-V3OV2XQT.js';
9
9
  import { clsx } from 'clsx';
@@ -181,12 +181,14 @@ function BooleanField({ field, value, onChange, id, disabled }) {
181
181
  }
182
182
  );
183
183
  }
184
+
185
+ // src/react/locales/en.ts
184
186
  function formatBytes(bytes) {
185
187
  if (bytes < 1024) return bytes === 1 ? "1 byte" : `${bytes} bytes`;
186
188
  if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;
187
189
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
188
190
  }
189
- var DEFAULT_FIELD_STRINGS = {
191
+ var EN_FIELD_STRINGS = {
190
192
  optionalBadge: "optional",
191
193
  selectPlaceholder: "Select\u2026",
192
194
  typeValuePlaceholder: "Type a value\u2026",
@@ -237,12 +239,100 @@ var DEFAULT_FIELD_STRINGS = {
237
239
  optionalFieldsCount: (count) => count === 1 ? "1 optional field" : `${count} optional fields`,
238
240
  optionalInputsCount: (count) => count === 1 ? "1 optional input" : `${count} optional inputs`
239
241
  };
242
+
243
+ // src/react/locales/fr.ts
244
+ var PLURALS = new Intl.PluralRules("fr");
245
+ var BYTES = new Intl.NumberFormat("fr-FR", { style: "unit", unit: "byte", unitDisplay: "long" });
246
+ var KILOBYTES = new Intl.NumberFormat("fr-FR", {
247
+ style: "unit",
248
+ unit: "kilobyte",
249
+ unitDisplay: "short",
250
+ maximumFractionDigits: 0
251
+ });
252
+ var MEGABYTES = new Intl.NumberFormat("fr-FR", {
253
+ style: "unit",
254
+ unit: "megabyte",
255
+ unitDisplay: "short",
256
+ maximumFractionDigits: 1
257
+ });
258
+ function plural(count, one, other) {
259
+ return PLURALS.select(count) === "one" ? one : other;
260
+ }
261
+ function formatBytes2(bytes) {
262
+ if (bytes < 1024) return BYTES.format(bytes);
263
+ if (bytes < 1024 * 1024) return KILOBYTES.format(bytes / 1024);
264
+ return MEGABYTES.format(bytes / (1024 * 1024));
265
+ }
266
+ var FR_FIELD_STRINGS = {
267
+ optionalBadge: "facultatif",
268
+ selectPlaceholder: "Choisir\u2026",
269
+ typeValuePlaceholder: "Saisissez une valeur\u2026",
270
+ writeHerePlaceholder: "\xC9crivez ici\u2026",
271
+ jsonHint: "Saisissez cette valeur en JSON brut.",
272
+ decrease: "Diminuer",
273
+ increase: "Augmenter",
274
+ noItemsYet: "Aucun \xE9l\xE9ment pour le moment",
275
+ addItem: "Ajouter un \xE9l\xE9ment",
276
+ removeItemAria: (index) => `Supprimer l\u2019\xE9l\xE9ment ${index}`,
277
+ itemsCount: (count) => `${count} ${plural(count, "\xE9l\xE9ment", "\xE9l\xE9ments")}`,
278
+ itemsCountOf: (count, total) => `${count} sur ${total} ${plural(total, "\xE9l\xE9ment", "\xE9l\xE9ments")}`,
279
+ fieldsCount: (count) => `${count} ${plural(count, "champ", "champs")}`,
280
+ uploading: "Envoi en cours\u2026",
281
+ dropToUpload: "D\xE9posez pour envoyer",
282
+ dropOrBrowse: "D\xE9posez un fichier ou cliquez pour parcourir",
283
+ pasteUrlInstead: "coller un lien \xE0 la place",
284
+ uploadUnavailable: "Impossible d\u2019envoyer un fichier ici. Collez plut\xF4t un lien vers le fichier.",
285
+ urlPlaceholder: "https://\u2026",
286
+ fileUrlAria: (label) => label ? `Lien vers le fichier pour ${label}` : "Lien vers le fichier",
287
+ uploadedFile: "Fichier joint",
288
+ encodedFileSummary: (format, bytes) => `${format} \xB7 ${formatBytes2(bytes)}`,
289
+ preview: "Aper\xE7u",
290
+ removeFileAria: "Retirer le fichier",
291
+ previewUnavailablePdf: "Aper\xE7u indisponible - ouvrez le fichier pour le consulter.",
292
+ // U+002D HYPHEN-MINUS, as in the English defaults.
293
+ resultAbsent: "-",
294
+ resultAbsentDescription: "Non renseign\xE9",
295
+ resultUndescribed: "Aucun descripteur de sortie pour ce pipe \u2014 affichage de la valeur brute.",
296
+ toggleRowDetails: (index) => `Afficher ou masquer le d\xE9tail de la ligne ${index}`,
297
+ rowDetailsColumn: "D\xE9tail",
298
+ copyUrl: "Copier le lien",
299
+ copyText: "Copier le texte",
300
+ download: "T\xE9l\xE9charger",
301
+ downloading: "T\xE9l\xE9chargement\u2026",
302
+ downloadIncomplete: (names) => `${names.length <= 1 ? "Un fichier n\u2019a pas pu \xEAtre enregistr\xE9" : `${names.length} fichiers n\u2019ont pas pu \xEAtre enregistr\xE9s`}\xA0: ${names.join(", ")}`,
303
+ downloadFile: (name) => `T\xE9l\xE9charger ${name}`,
304
+ downloadFileFailed: "Le fichier n\u2019a pas pu \xEAtre enregistr\xE9",
305
+ viewRendered: "R\xE9sultat",
306
+ viewJson: "JSON",
307
+ copyJson: "Copier le JSON",
308
+ resultViewGroup: "Affichage du r\xE9sultat",
309
+ yes: "Oui",
310
+ no: "Non",
311
+ unsupportedFileType: (formats) => `Ce type de fichier n\u2019est pas pris en charge. Formats accept\xE9s\xA0: ${formats}.`,
312
+ hideOptionalFields: "Masquer les champs facultatifs",
313
+ hideOptionalInputs: "Masquer les entr\xE9es facultatives",
314
+ optionalFieldsCount: (count) => `${count} ${plural(count, "champ facultatif", "champs facultatifs")}`,
315
+ optionalInputsCount: (count) => `${count} ${plural(count, "entr\xE9e facultative", "entr\xE9es facultatives")}`
316
+ };
317
+
318
+ // src/react/locales/index.ts
319
+ var FIELD_STRINGS_BY_LOCALE = {
320
+ en: EN_FIELD_STRINGS,
321
+ fr: FR_FIELD_STRINGS
322
+ };
323
+ var SUPPORTED_LOCALES = Object.keys(FIELD_STRINGS_BY_LOCALE);
324
+ function fieldStringsFor(locale) {
325
+ const language = (locale ?? "").trim().toLowerCase().split(/[-_]/)[0] ?? "";
326
+ return FIELD_STRINGS_BY_LOCALE[language] ?? EN_FIELD_STRINGS;
327
+ }
328
+ var DEFAULT_FIELD_STRINGS = EN_FIELD_STRINGS;
240
329
  var FieldStringsContext = createContext(DEFAULT_FIELD_STRINGS);
241
330
  function FieldStringsProvider({
331
+ locale,
242
332
  strings,
243
333
  children
244
334
  }) {
245
- const merged = useMemo(() => ({ ...DEFAULT_FIELD_STRINGS, ...strings }), [strings]);
335
+ const merged = useMemo(() => ({ ...fieldStringsFor(locale), ...strings }), [locale, strings]);
246
336
  return /* @__PURE__ */ jsx(FieldStringsContext.Provider, { value: merged, children });
247
337
  }
248
338
  function useFieldStrings() {
@@ -315,7 +405,7 @@ function DateField({ field, value, onChange, id, error, disabled }) {
315
405
  conceptRef: field.conceptRef,
316
406
  category: "date",
317
407
  description: field.description,
318
- required: field.required,
408
+ required: !shownAsOptional(field),
319
409
  error,
320
410
  htmlFor: domId,
321
411
  children: /* @__PURE__ */ jsx(
@@ -511,7 +601,7 @@ function FileField({
511
601
  conceptRef: field.conceptRef,
512
602
  category,
513
603
  description: field.description,
514
- required: field.required,
604
+ required: !shownAsOptional(field),
515
605
  error,
516
606
  htmlFor: domId,
517
607
  children: [
@@ -903,7 +993,7 @@ function EnumField({ field, value, onChange, id, error, disabled }) {
903
993
  conceptRef: field.conceptRef,
904
994
  category: "choice",
905
995
  description: field.description,
906
- required: field.required,
996
+ required: !shownAsOptional(field),
907
997
  error,
908
998
  htmlFor: isSegmented ? void 0 : domId,
909
999
  children: isSegmented ? (
@@ -972,7 +1062,7 @@ function NumberField({ field, value, onChange, id, error, disabled }) {
972
1062
  conceptRef: field.conceptRef,
973
1063
  category: "number",
974
1064
  description: field.description ?? rangeHint,
975
- required: field.required,
1065
+ required: !shownAsOptional(field),
976
1066
  error,
977
1067
  htmlFor: domId,
978
1068
  children: /* @__PURE__ */ jsxs("div", { className: "flex items-stretch gap-1.5", children: [
@@ -1034,7 +1124,7 @@ function TextField({ field, value, onChange, id, error, disabled }) {
1034
1124
  conceptRef: field.conceptRef,
1035
1125
  category: "text",
1036
1126
  description: field.description,
1037
- required: field.required,
1127
+ required: !shownAsOptional(field),
1038
1128
  error,
1039
1129
  htmlFor: domId,
1040
1130
  children: /* @__PURE__ */ jsx(
@@ -1064,7 +1154,7 @@ function ProseField({ field, value, onChange, id, error, disabled }) {
1064
1154
  conceptRef: field.conceptRef,
1065
1155
  category: "text",
1066
1156
  description: field.description,
1067
- required: field.required,
1157
+ required: !shownAsOptional(field),
1068
1158
  error,
1069
1159
  htmlFor: domId,
1070
1160
  children: /* @__PURE__ */ jsx(
@@ -1108,8 +1198,10 @@ function ObjectField({ field, value, onChange, id, error, env }) {
1108
1198
  const isApp = presentation === "app";
1109
1199
  const data = value ?? {};
1110
1200
  const setChild = (name, childValue) => onChange({ ...data, [name]: childValue });
1111
- const optionalEmpty = field.fields.filter((f) => !f.required && !isFilled(ownProp(data, f.name)));
1112
- const visible = showOptional ? field.fields : field.fields.filter((f) => f.required || isFilled(ownProp(data, f.name)));
1201
+ const optionalEmpty = field.fields.filter(
1202
+ (f) => shownAsOptional(f) && !isFilled(ownProp(data, f.name))
1203
+ );
1204
+ const visible = showOptional ? field.fields : field.fields.filter((f) => !shownAsOptional(f) || isFilled(ownProp(data, f.name)));
1113
1205
  return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1114
1206
  /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-baseline gap-x-2 gap-y-1", children: [
1115
1207
  /* @__PURE__ */ jsx(
@@ -1123,7 +1215,7 @@ function ObjectField({ field, value, onChange, id, error, env }) {
1123
1215
  }
1124
1216
  ),
1125
1217
  !isApp && /* @__PURE__ */ jsx(ConceptPill, { conceptRef: field.conceptRef, category: "structured" }),
1126
- !field.required && /* @__PURE__ */ jsx("span", { className: "ml-auto font-mono text-[10px] uppercase tracking-wider text-muted-foreground", children: s.optionalBadge })
1218
+ shownAsOptional(field) && /* @__PURE__ */ jsx("span", { className: "ml-auto font-mono text-[10px] uppercase tracking-wider text-muted-foreground", children: s.optionalBadge })
1127
1219
  ] }),
1128
1220
  field.description && /* @__PURE__ */ jsx("p", { className: "text-[12px] leading-relaxed text-muted-foreground", children: field.description }),
1129
1221
  /* @__PURE__ */ jsxs(
@@ -1223,7 +1315,7 @@ function ListField({ field, value, onChange, id, error, env }) {
1223
1315
  ),
1224
1316
  !isApp && /* @__PURE__ */ jsx(ConceptPill, { conceptRef: field.conceptRef, category: "list" }),
1225
1317
  /* @__PURE__ */ jsx("span", { className: "font-mono text-[10.5px] text-muted-foreground", children: field.itemCount !== void 0 && field.maxItemCount === field.itemCount ? s.itemsCountOf(items.length, field.itemCount) : s.itemsCount(items.length) }),
1226
- !field.required && /* @__PURE__ */ jsx("span", { className: "ml-auto font-mono text-[10px] uppercase tracking-wider text-muted-foreground", children: s.optionalBadge })
1318
+ shownAsOptional(field) && /* @__PURE__ */ jsx("span", { className: "ml-auto font-mono text-[10px] uppercase tracking-wider text-muted-foreground", children: s.optionalBadge })
1227
1319
  ] }),
1228
1320
  field.description && /* @__PURE__ */ jsx("p", { className: "text-[12px] leading-relaxed text-muted-foreground", children: field.description }),
1229
1321
  /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
@@ -1341,7 +1433,7 @@ function UnknownField({ field, value, onChange, id, error, disabled }) {
1341
1433
  conceptRef: field.conceptRef,
1342
1434
  category: "structured",
1343
1435
  description: field.description ?? s.jsonHint,
1344
- required: field.required,
1436
+ required: !shownAsOptional(field),
1345
1437
  error,
1346
1438
  htmlFor: domId,
1347
1439
  children: /* @__PURE__ */ jsx(
@@ -3127,6 +3219,6 @@ function StuffPanel({
3127
3219
  ] });
3128
3220
  }
3129
3221
 
3130
- export { BooleanField, ConceptPill, DEFAULT_FIELD_STRINGS, DateField, DocumentField, EnumField, FieldDomIdProvider, FieldPresentationProvider, FieldRenderer, FieldShell, FieldStringsProvider, HtmlPreview, ImageField, JsonView, ListField, Markdown, NumberField, ObjectField, OptionalToggle, ProseField, ResultEnvProvider, ResultField, ResultRoot, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StuffViewer, Switch, TextField, ToggleGroup, ToggleGroupItem, UnknownField, cn, fieldControlClass, humanizeFieldName, saveInBrowser, useFieldDomId, useFieldPresentation, useFieldStrings, useProseImages, useResolveShareUrl, useResolveUrl, useResolvedUrl };
3131
- //# sourceMappingURL=chunk-2D4NEFUH.js.map
3132
- //# sourceMappingURL=chunk-2D4NEFUH.js.map
3222
+ export { BooleanField, ConceptPill, DEFAULT_FIELD_STRINGS, DateField, DocumentField, EN_FIELD_STRINGS, EnumField, FIELD_STRINGS_BY_LOCALE, FR_FIELD_STRINGS, FieldDomIdProvider, FieldPresentationProvider, FieldRenderer, FieldShell, FieldStringsProvider, HtmlPreview, ImageField, JsonView, ListField, Markdown, NumberField, ObjectField, OptionalToggle, ProseField, ResultEnvProvider, ResultField, ResultRoot, SUPPORTED_LOCALES, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, StuffViewer, Switch, TextField, ToggleGroup, ToggleGroupItem, UnknownField, cn, fieldControlClass, fieldStringsFor, humanizeFieldName, saveInBrowser, useFieldDomId, useFieldPresentation, useFieldStrings, useProseImages, useResolveShareUrl, useResolveUrl, useResolvedUrl };
3223
+ //# sourceMappingURL=chunk-D2EQVWGN.js.map
3224
+ //# sourceMappingURL=chunk-D2EQVWGN.js.map