@stapel/attributes-react 0.1.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.
Files changed (74) hide show
  1. package/README.md +123 -0
  2. package/dist/default/FeatureBadges.d.ts +33 -0
  3. package/dist/default/FeatureBadges.d.ts.map +1 -0
  4. package/dist/default/FeatureBadges.js +56 -0
  5. package/dist/default/FeatureBadges.js.map +1 -0
  6. package/dist/default/FeatureFields.d.ts +52 -0
  7. package/dist/default/FeatureFields.d.ts.map +1 -0
  8. package/dist/default/FeatureFields.js +60 -0
  9. package/dist/default/FeatureFields.js.map +1 -0
  10. package/dist/default/editors.d.ts +24 -0
  11. package/dist/default/editors.d.ts.map +1 -0
  12. package/dist/default/editors.js +354 -0
  13. package/dist/default/editors.js.map +1 -0
  14. package/dist/default/index.d.ts +27 -0
  15. package/dist/default/index.d.ts.map +1 -0
  16. package/dist/default/index.js +25 -0
  17. package/dist/default/index.js.map +1 -0
  18. package/dist/dto.d.ts +40 -0
  19. package/dist/dto.d.ts.map +1 -0
  20. package/dist/dto.js +64 -0
  21. package/dist/dto.js.map +1 -0
  22. package/dist/errors.d.ts +57 -0
  23. package/dist/errors.d.ts.map +1 -0
  24. package/dist/errors.js +98 -0
  25. package/dist/errors.js.map +1 -0
  26. package/dist/format.d.ts +46 -0
  27. package/dist/format.d.ts.map +1 -0
  28. package/dist/format.js +159 -0
  29. package/dist/format.js.map +1 -0
  30. package/dist/i18n/es.d.ts +13 -0
  31. package/dist/i18n/es.d.ts.map +1 -0
  32. package/dist/i18n/es.js +36 -0
  33. package/dist/i18n/es.js.map +1 -0
  34. package/dist/i18n/keys.d.ts +56 -0
  35. package/dist/i18n/keys.d.ts.map +1 -0
  36. package/dist/i18n/keys.js +80 -0
  37. package/dist/i18n/keys.js.map +1 -0
  38. package/dist/i18n/ru.d.ts +19 -0
  39. package/dist/i18n/ru.d.ts.map +1 -0
  40. package/dist/i18n/ru.js +42 -0
  41. package/dist/i18n/ru.js.map +1 -0
  42. package/dist/index.d.ts +58 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +54 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/registry.d.ts +122 -0
  47. package/dist/registry.d.ts.map +1 -0
  48. package/dist/registry.js +82 -0
  49. package/dist/registry.js.map +1 -0
  50. package/dist/types.d.ts +134 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +35 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/validate.d.ts +88 -0
  55. package/dist/validate.d.ts.map +1 -0
  56. package/dist/validate.js +410 -0
  57. package/dist/validate.js.map +1 -0
  58. package/manifest.json +96 -0
  59. package/package.json +107 -0
  60. package/src/default/FeatureBadges.tsx +123 -0
  61. package/src/default/FeatureFields.tsx +140 -0
  62. package/src/default/editors.tsx +578 -0
  63. package/src/default/index.ts +34 -0
  64. package/src/dto.ts +78 -0
  65. package/src/errors.ts +127 -0
  66. package/src/format.ts +210 -0
  67. package/src/i18n/es.ts +41 -0
  68. package/src/i18n/keys.ts +89 -0
  69. package/src/i18n/ru.ts +48 -0
  70. package/src/index.ts +98 -0
  71. package/src/registry.ts +167 -0
  72. package/src/types.ts +166 -0
  73. package/src/validate.ts +507 -0
  74. package/tsconfig.json +26 -0
package/README.md ADDED
@@ -0,0 +1,123 @@
1
+ # @stapel/attributes-react
2
+
3
+ The React value layer for `stapel-attributes`' dynamic feature types: draw an
4
+ attribute, check it, submit it, display it.
5
+
6
+ ## The axis — `config.type`, not `FormField.kind`
7
+
8
+ `stapel-attributes` has **two** field vocabularies, and mixing them up is the
9
+ one mistake this package exists to prevent:
10
+
11
+ | | vocabulary | who reads it |
12
+ |---|---|---|
13
+ | `FormField.kind` | the field kinds of the **admin form that configures a type** (`config_form.py`; `GET /forms/api/v1/field-kinds`) | [`@stapel/forms-react`](../forms-react) |
14
+ | `config["type"]` | the **value type** — `string`, `int`, `float`, `bool`, `select`, `date`, `header`, `hex_color`, `hierarchical_select`, `convertible_unit` | **this package** |
15
+
16
+ A person filling in a listing edits the second one. A useful consequence: a
17
+ storefront needs **no catalogue endpoint** for these types — the type arrives
18
+ in the data, on every feature that
19
+ `GET /categories/api/v1/categories/{id}/features/` returns.
20
+
21
+ ## What kind of package this is
22
+
23
+ **L0, not a pair** — modelled on [`@stapel/image`](../image): no client, no
24
+ queries, no generated `schema.ts`. Its backend counterpart is an L1 library
25
+ with no HTTP surface at all (no models, no views, no urls), so there is no
26
+ `/attributes/api/v1/` to pair with and nothing for `pnpm gen:api` to read.
27
+ Feature definitions and validation verdicts reach a browser inside the
28
+ responses of the modules that own them (categories, listings), and those pairs
29
+ depend on this one to draw and check what they carry.
30
+
31
+ The builtin type set and the `ValidationErrorCode` vocabulary are pinned
32
+ against the engine's own generated corpora
33
+ (`stapel-attributes/tests/golden/{declarations,error_codes}.json`) by
34
+ `test/contract.test.ts` — always against the committed fixture, and
35
+ additionally against the live checkout when one is present.
36
+
37
+ ## The ladder
38
+
39
+ ```
40
+ explicit registerValueEditor(type, …) ← a host's, always wins
41
+ → BUILTIN_VALUE_EDITORS ← /default's antd editors, ten types
42
+ → <UnsupportedValueEditor/> ← loud, never a skipped field
43
+ ```
44
+
45
+ …and, while an undrawable feature is on screen, `unsupportedTypeGate` blocks
46
+ the submit **with the reason named**. That last rung is the point: a category
47
+ can legally carry a type this build has no editor for, and drawing nothing
48
+ would silently drop a feature that may be MANDATORY — the person would submit
49
+ a listing they could not complete and be told, by the server, that an
50
+ attribute they never saw is missing.
51
+
52
+ ```tsx
53
+ import {
54
+ featureErrorsBySlug, mirrorValidate, toFeaturesDto, unsupportedTypeGate,
55
+ } from "@stapel/attributes-react";
56
+ import { BUILTIN_VALUE_EDITOR_TYPES, FeatureFields } from "@stapel/attributes-react/default";
57
+
58
+ const gate = unsupportedTypeGate(features, BUILTIN_VALUE_EDITOR_TYPES);
59
+ const errors = featureErrorsBySlug(mirrorValidate(features, toFeaturesDto(features, values)));
60
+
61
+ <FeatureFields features={features} values={values} onChange={setValue} errors={errors} />;
62
+ <Button disabled={!gate.available}>…</Button>
63
+ ```
64
+
65
+ Adding a type your backend registered through `EXTRA_TYPES` takes one line at
66
+ startup:
67
+
68
+ ```tsx
69
+ registerValueEditor("size_grid", SizeGridEditor);
70
+ ```
71
+
72
+ ## The mirror
73
+
74
+ `mirrorValidate(features, dto)` returns the server's own
75
+ `ValidationBatchResult` shape, built from the same rules and reporting the
76
+ same `error.400.feature_*` keys — so a "too long" caught locally and one
77
+ caught by the server render the same sentence. It is feedback, never a
78
+ verdict: `POST /categories/{pk}/validate-dto/` and
79
+ `POST /listings/{pk}/publish/` decide.
80
+
81
+ Two contract details it gets right on purpose, both from
82
+ `stapel-attributes/MODULE.md` §"Pattern contract":
83
+
84
+ - `pattern` matches the **whole** value (`re.fullmatch`), not a prefix.
85
+ - string length is counted in Unicode **code points** on both sides — five
86
+ emoji are five characters, not ten.
87
+
88
+ And two places it deliberately stands down, because a mirror that refuses what
89
+ the server accepts blocks a valid submit:
90
+
91
+ - a value type it does not know (it may be a perfectly good `EXTRA_TYPES`
92
+ registration whose rules live only in Python) — it still checks
93
+ mandatory/empty, which is type-independent;
94
+ - `convertible_unit`'s range, whose `min`/`max` are in the unit family's base
95
+ unit and whose conversion table is server-side. The number and the unit code
96
+ ARE checked.
97
+
98
+ ## Value shapes that are not what you would guess
99
+
100
+ Three of the ten types do not carry a bare scalar, and every one of them is a
101
+ submit that fails silently if you assume otherwise:
102
+
103
+ | type | wire value |
104
+ |---|---|
105
+ | `select` | **always a list**, even when `maxSelected: 1` |
106
+ | `date` | a **Unix timestamp (integer)**, not an ISO string |
107
+ | `hex_color` | an **object** `{simple, hex?, label?}` where `simple` is required and drawn from eighteen colour categories |
108
+ | `convertible_unit` | an **object** `{value, unit}` — the number as typed, tagged with the unit; the server converts |
109
+
110
+ `toFeaturesDto` builds the `{slug: {type, value}}` envelope from a plain
111
+ `{slug: value}` map, tagging each entry from the FEATURE's config (never from
112
+ the editor — the engine overrides a client-sent `type` anyway) and dropping
113
+ headers, untyped rows and blanks.
114
+
115
+ ## Display
116
+
117
+ `formatFeatureValue` is React-free and lives in the main entry, so a result
118
+ card can format a value without pulling antd in. `/default` adds
119
+ `<FeatureBadges/>` (the `show_as_badge` values as tags) and
120
+ `<FeatureValueList/>` (the whole spec table). Both keep the two absences
121
+ apart: "not specified" for a value nobody entered, and a named notice for a
122
+ type this build cannot read — an empty cell would claim the first when it
123
+ means the second.
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The DISPLAY half — `<FeatureBadges/>` for a card, `<FeatureValueList/>` for
3
+ * a detail page. Both are renderers over `formatFeatureValue`; neither
4
+ * re-implements a single type's formatting.
5
+ *
6
+ * Two rules they hold that a naive spec table does not:
7
+ *
8
+ * - **An unreadable value says so.** A type this build cannot format renders
9
+ * a named notice, not an empty cell — the display twin of the unsupported
10
+ * editor. A blank cell where a spec line belongs reads as "this listing
11
+ * has no engine size", which is a different and false statement.
12
+ * - **`show_as_badge` / `show_at_title` are the CATEGORY's decision**, made
13
+ * by whoever configured the feature, and this component honours it instead
14
+ * of picking its own first three values.
15
+ */
16
+ import type { ReactElement } from "react";
17
+ import type { FeatureDef, FeaturesDto } from "../types.js";
18
+ export interface FeatureDisplayProps {
19
+ readonly features: readonly FeatureDef[];
20
+ /** The stored values, in the same `{slug: {type, value}}` envelope the
21
+ * composer submitted. */
22
+ readonly values: FeaturesDto;
23
+ }
24
+ /**
25
+ * The `show_as_badge` values, as antd `Tag`s — what a result card shows under
26
+ * a title. Features with no value are omitted here (a card is a summary, and
27
+ * "not specified" is not a selling point); the detail list below says it
28
+ * explicitly instead.
29
+ */
30
+ export declare function FeatureBadges(props: FeatureDisplayProps): ReactElement;
31
+ /** Every feature and its value — the spec table of a detail page. */
32
+ export declare function FeatureValueList(props: FeatureDisplayProps): ReactElement;
33
+ //# sourceMappingURL=FeatureBadges.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeatureBadges.d.ts","sourceRoot":"","sources":["../../src/default/FeatureBadges.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAG1C,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAK3D,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;IACzC;6BACyB;IACzB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;CAC9B;AAiDD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,mBAAmB,GAAG,YAAY,CAgBtE;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,mBAAmB,GAAG,YAAY,CAoBzE"}
@@ -0,0 +1,56 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { Descriptions, Flex, Tag, Typography } from "antd";
3
+ import { useI18n, useT } from "@stapel/core";
4
+ import { featureName, featureType } from "../types.js";
5
+ import { formatFeatureValue, hexColorSwatch } from "../format.js";
6
+ import { ATTRIBUTES_I18N_KEYS } from "../i18n/keys.js";
7
+ /** Feature + its formatted value, in the order the category declared. */
8
+ function useRows(props, only) {
9
+ const t = useT();
10
+ const { locale } = useI18n();
11
+ return props.features
12
+ .filter((feature) => featureType(feature) !== "header")
13
+ .filter((feature) => (only ? only(feature) : true))
14
+ .map((feature) => {
15
+ const dto = props.values[feature.slug];
16
+ return {
17
+ feature,
18
+ text: formatFeatureValue(feature, dto, { t, locale }),
19
+ swatch: featureType(feature) === "hex_color" ? hexColorSwatch(dto) : undefined,
20
+ };
21
+ });
22
+ }
23
+ /** A row's value, or the named reason there is none. */
24
+ function ValueText(props) {
25
+ const t = useT();
26
+ if (props.text !== undefined)
27
+ return _jsx(_Fragment, { children: props.text });
28
+ const type = featureType(props.feature);
29
+ // Two different absences, said differently: nothing was entered, versus
30
+ // something was entered that this build cannot read.
31
+ if (!props.hasValue || type === undefined) {
32
+ return (_jsx(Typography.Text, { type: "secondary", children: t(ATTRIBUTES_I18N_KEYS.valueNotSet) }));
33
+ }
34
+ return (_jsx(Typography.Text, { type: "warning", "data-testid": "attributes-unreadable-value", children: t(ATTRIBUTES_I18N_KEYS.valueUnreadable, { type }) }));
35
+ }
36
+ /**
37
+ * The `show_as_badge` values, as antd `Tag`s — what a result card shows under
38
+ * a title. Features with no value are omitted here (a card is a summary, and
39
+ * "not specified" is not a selling point); the detail list below says it
40
+ * explicitly instead.
41
+ */
42
+ export function FeatureBadges(props) {
43
+ const rows = useRows(props, (feature) => feature.show_as_badge === true);
44
+ const shown = rows.filter((row) => row.text !== undefined);
45
+ return (_jsx(Flex, { gap: 4, wrap: true, "data-testid": "attributes-badges", children: shown.map((row) => (_jsx(Tag, { ...(row.swatch ? { color: row.swatch } : {}), "data-testid": `attributes-badge-${row.feature.slug}`, children: row.text }, row.feature.slug))) }));
46
+ }
47
+ /** Every feature and its value — the spec table of a detail page. */
48
+ export function FeatureValueList(props) {
49
+ const rows = useRows(props);
50
+ return (_jsx(Descriptions, { column: 1, size: "small", "data-testid": "attributes-value-list", items: rows.map((row) => ({
51
+ key: row.feature.slug,
52
+ label: featureName(row.feature),
53
+ children: (_jsx(ValueText, { feature: row.feature, text: row.text, hasValue: props.values[row.feature.slug] !== undefined })),
54
+ })) }));
55
+ }
56
+ //# sourceMappingURL=FeatureBadges.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeatureBadges.js","sourceRoot":"","sources":["../../src/default/FeatureBadges.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `<FeatureFields/>` — a category's features, drawn as antd form rows, and
3
+ * the place the three-rung ladder is actually executed.
4
+ *
5
+ * It holds no state and owns no submit: values and errors come in, changes go
6
+ * out. That is what lets the composer that DOES own the submit
7
+ * (listings-react) keep one source of truth for a draft, and what lets this
8
+ * component be dropped into any form — a filter panel, an admin preview —
9
+ * without dragging a mutation along.
10
+ */
11
+ import type { ReactElement, ReactNode } from "react";
12
+ import type { FlowError } from "@stapel/core";
13
+ import type { FeatureDef } from "../types.js";
14
+ /** Stable per-feature DOM id, so `<label for>` reaches the control. Slugs are
15
+ * `[a-z0-9_]`-shaped by the engine, so this needs no further escaping. */
16
+ export declare function featureControlId(slug: string): string;
17
+ export interface UnsupportedValueEditorProps {
18
+ readonly feature: FeatureDef;
19
+ }
20
+ /**
21
+ * The loud last rung. NOT a skipped field: a category can legally carry a
22
+ * type this build has no editor for, and drawing nothing would silently drop
23
+ * a feature that may be mandatory — the person would submit a listing they
24
+ * could not complete and be told, by the server, that an attribute they never
25
+ * saw is missing.
26
+ */
27
+ export declare function UnsupportedValueEditor(props: UnsupportedValueEditorProps): ReactElement;
28
+ export interface FeatureFieldsProps {
29
+ readonly features: readonly FeatureDef[];
30
+ /** Current answers keyed by slug. The DTO envelope is built at submit time
31
+ * with `toFeaturesDto`, not held here. */
32
+ readonly values: Readonly<Record<string, unknown>>;
33
+ onChange(slug: string, value: unknown): void;
34
+ /** Refusals keyed by slug — mirrored or from the server, folded through
35
+ * `featureErrorsBySlug` either way. */
36
+ readonly errors?: Readonly<Record<string, FlowError>>;
37
+ /** True while a submit is in flight — every editor goes read-only. */
38
+ readonly disabled?: boolean;
39
+ /** Rendered instead of a default `Form.Item` row, for a host with its own
40
+ * field chrome. */
41
+ readonly renderRow?: (row: FeatureRowProps) => ReactNode;
42
+ }
43
+ export interface FeatureRowProps {
44
+ readonly feature: FeatureDef;
45
+ readonly controlId: string;
46
+ readonly control: ReactNode;
47
+ readonly error: FlowError | undefined;
48
+ readonly required: boolean;
49
+ }
50
+ /** A category's features as form rows, each drawn by the ladder's winner. */
51
+ export declare function FeatureFields(props: FeatureFieldsProps): ReactElement;
52
+ //# sourceMappingURL=FeatureFields.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeatureFields.d.ts","sourceRoot":"","sources":["../../src/default/FeatureFields.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAM9C;0EAC0E;AAC1E,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,MAAM,WAAW,2BAA2B;IAC1C,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,2BAA2B,GACjC,YAAY,CAgBd;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,CAAC;IACzC;8CAC0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACnD,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;IAC7C;2CACuC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IACtD,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAC5B;uBACmB;IACnB,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,KAAK,SAAS,CAAC;CAC1D;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAsBD,6EAA6E;AAC7E,wBAAgB,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,YAAY,CAqCrE"}
@@ -0,0 +1,60 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Alert, Form } from "antd";
3
+ import { useFormatFlowError, useT } from "@stapel/core";
4
+ import { featureName, featureType } from "../types.js";
5
+ import { resolveValueEditor } from "../registry.js";
6
+ import { BUILTIN_VALUE_EDITORS } from "./editors.js";
7
+ import { ATTRIBUTES_I18N_KEYS } from "../i18n/keys.js";
8
+ /** Stable per-feature DOM id, so `<label for>` reaches the control. Slugs are
9
+ * `[a-z0-9_]`-shaped by the engine, so this needs no further escaping. */
10
+ export function featureControlId(slug) {
11
+ return `attributes-field-${slug}`;
12
+ }
13
+ /**
14
+ * The loud last rung. NOT a skipped field: a category can legally carry a
15
+ * type this build has no editor for, and drawing nothing would silently drop
16
+ * a feature that may be mandatory — the person would submit a listing they
17
+ * could not complete and be told, by the server, that an attribute they never
18
+ * saw is missing.
19
+ */
20
+ export function UnsupportedValueEditor(props) {
21
+ const t = useT();
22
+ const type = featureType(props.feature);
23
+ return (_jsx(Alert, { type: "warning", showIcon: true, "data-testid": "attributes-unsupported-type", message: featureName(props.feature), description: type === undefined
24
+ ? t(ATTRIBUTES_I18N_KEYS.untypedFeature)
25
+ : t(ATTRIBUTES_I18N_KEYS.unsupportedType, { type }) }));
26
+ }
27
+ function FeatureRow(props) {
28
+ const format = useFormatFlowError();
29
+ // A header is a caption: no label, no colon, no required marker — rendering
30
+ // one inside a labelled Form.Item would make a section heading look like a
31
+ // question.
32
+ if (featureType(props.feature) === "header") {
33
+ return _jsx(Form.Item, { style: { marginBottom: 8 }, children: props.control });
34
+ }
35
+ return (_jsx(Form.Item, { label: featureName(props.feature), htmlFor: props.controlId, required: props.required, ...(props.error ? { validateStatus: "error", help: format(props.error) } : {}), children: props.control }));
36
+ }
37
+ /** A category's features as form rows, each drawn by the ladder's winner. */
38
+ export function FeatureFields(props) {
39
+ const errors = props.errors ?? {};
40
+ return (_jsx(_Fragment, { children: props.features.map((feature) => {
41
+ const controlId = featureControlId(feature.slug);
42
+ const type = featureType(feature);
43
+ // The ladder: a host's explicit registration outranks the skin's
44
+ // builtin, and nothing at all is a NOTICE, never an omission.
45
+ const Editor = (type === undefined ? null : resolveValueEditor(type)) ??
46
+ (type === undefined ? undefined : BUILTIN_VALUE_EDITORS[type]);
47
+ const control = Editor === undefined || Editor === null ? (_jsx(UnsupportedValueEditor, { feature: feature })) : (_jsx(Editor, { id: controlId, feature: feature, value: props.values[feature.slug], onChange: (value) => props.onChange(feature.slug, value), error: errors[feature.slug], disabled: props.disabled === true }));
48
+ const row = {
49
+ feature,
50
+ controlId,
51
+ control,
52
+ error: errors[feature.slug],
53
+ required: feature.mandatory === true,
54
+ };
55
+ if (props.renderRow)
56
+ return _jsx("div", { children: props.renderRow(row) }, feature.slug);
57
+ return _jsx(FeatureRow, { ...row }, feature.slug);
58
+ }) }));
59
+ }
60
+ //# sourceMappingURL=FeatureFields.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FeatureFields.js","sourceRoot":"","sources":["../../src/default/FeatureFields.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { ValueEditor } from "../registry.js";
2
+ /** Unix seconds → the string a native input of this precision displays,
3
+ * in the VIEWER's time zone (which is what the person typed it in). */
4
+ export declare function timestampToInputValue(seconds: number, precision: string): string;
5
+ /**
6
+ * The inverse. Takes no `precision`: a native input's value already says
7
+ * which shape it is (`2010`, `2010-06`, `2010-06-15`, `2010-06-15T14:30`), so
8
+ * reading the config here would only create a way for the two to disagree.
9
+ *
10
+ * Returns `undefined` for an empty or unparseable input rather than 0 — `0`
11
+ * is 1970, a real timestamp, and the one value that must never be produced by
12
+ * "the person cleared the field".
13
+ */
14
+ export declare function inputValueToTimestamp(text: string): number | undefined;
15
+ /**
16
+ * The skin's builtin editor per value type — the second rung of the ladder.
17
+ * A type absent from this table has no default drawing and reaches
18
+ * `<UnsupportedValueEditor/>`.
19
+ */
20
+ export declare const BUILTIN_VALUE_EDITORS: Readonly<Record<string, ValueEditor>>;
21
+ /** The types this skin can draw — handed to `unsupportedTypes` so the
22
+ * headless half can judge renderability without importing the skin. */
23
+ export declare const BUILTIN_VALUE_EDITOR_TYPES: readonly string[];
24
+ //# sourceMappingURL=editors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editors.d.ts","sourceRoot":"","sources":["../../src/default/editors.tsx"],"names":[],"mappings":"AAiDA,OAAO,KAAK,EAAE,WAAW,EAAoB,MAAM,gBAAgB,CAAC;AAuNpE;uEACuE;AACvE,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAMhF;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CActE;AAmQD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAWvE,CAAC;AAEF;uEACuE;AACvE,eAAO,MAAM,0BAA0B,EAAE,SAAS,MAAM,EACb,CAAC"}