@x12i/fieldkit 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 x12i
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # @x12i/fieldkit
2
+
3
+ Metadata-driven React components for rendering structured records — markdown-derived
4
+ report fields, pipeline output, whatever — as polished UI instead of a plain key/value
5
+ dump. You own a small **rules map** (`field name -> how to render it`); fieldkit owns
6
+ the **toolbox** of renderers those rules point at, plus a **shape-based fallback** for
7
+ any field your map doesn't cover yet.
8
+
9
+ Nothing in the package is domain-specific. Enum colors, which fields count as
10
+ "content" vs. "diagnostics", and what counts as an attention-worthy state are all
11
+ supplied by you — fieldkit just guarantees every field ends up rendered as *something*,
12
+ never silently dropped.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm install @x12i/fieldkit
18
+ ```
19
+
20
+ `react` and `react-dom` (>=18) are peer dependencies.
21
+
22
+ ## Quick start
23
+
24
+ ```tsx
25
+ import { RecordView } from '@x12i/fieldkit';
26
+
27
+ const record = {
28
+ status: 'REQUIRE_INFORMATION',
29
+ summary: 'This endpoint asset has no display name or usable address…',
30
+ highlights: ['Asset record is active…', 'Traffic observation coverage is complete…'],
31
+ riskSignals: ['Identity is unresolved…'],
32
+ openQuestions: ['What host, device, or endpoint corresponds to this source ID?'],
33
+ groundingStatus: 'INSUFFICIENT_EVIDENCE',
34
+ quality: 'EMPTY_MODEL',
35
+ stepId: 'triage-endpoint-asset',
36
+ };
37
+
38
+ const fieldRules = {
39
+ summary: { label: 'Summary', as: 'lead' },
40
+ highlights: { label: 'Highlights', as: 'bullets' },
41
+ riskSignals: { label: 'Risk Signals', as: 'signals' },
42
+ openQuestions: { label: 'Open Questions', as: 'qa' },
43
+ groundingStatus: { label: 'Grounding', as: 'pill' },
44
+ quality: { label: 'Quality', as: 'pill' },
45
+ stepId: { label: 'Step ID', as: 'mono' },
46
+ };
47
+
48
+ <RecordView
49
+ record={record}
50
+ fieldRules={fieldRules}
51
+ contentKeys={['summary', 'highlights', 'riskSignals', 'openQuestions']}
52
+ diagnosticKeys={['groundingStatus', 'quality', 'stepId']}
53
+ toneMap={{ REQUIRE_INFORMATION: 'amber', INSUFFICIENT_EVIDENCE: 'rose', EMPTY_MODEL: 'rose' }}
54
+ />;
55
+ ```
56
+
57
+ See `examples/basic` in this repo for a full working page (header, banner rule, theming) —
58
+ `npm run example` from the repo root.
59
+
60
+ ## The toolbox (`FieldKind`)
61
+
62
+ | kind | for |
63
+ |-----------|----------------------------------------------|
64
+ | `lead` | one prose paragraph, the primary read |
65
+ | `bullets` | array of short affirmative statements |
66
+ | `signals` | array of caution/warning statements (auto-classified warn vs. info) |
67
+ | `qa` | array of open questions |
68
+ | `pill` | a single enum-like value (status, quality…) |
69
+ | `mono` | a short technical string (id, hash, code) |
70
+ | `text` | plain secondary prose |
71
+ | `flag` | a boolean |
72
+ | `stat` | a number |
73
+ | `tags` | array of short tokens (not full sentences) |
74
+ | `empty` | null / undefined / `''` — shown, not hidden |
75
+ | `json` | anything else — pretty-printed as a fallback |
76
+
77
+ Every kind is handled in exactly one place (`FieldRenderer`'s switch), in both a
78
+ `content` (full-weight) and `compact` (diagnostics/leftover) visual variant. The
79
+ switch is exhaustive over the `FieldKind` union, so adding a new kind without wiring
80
+ it up in `FieldRenderer` is a **compile error**, not a silently blank field.
81
+
82
+ ## Your rules map
83
+
84
+ ```ts
85
+ export const fieldRules: FieldRules = {
86
+ summary: { label: 'Summary', as: 'lead' },
87
+ // ...
88
+ };
89
+ ```
90
+
91
+ Promoting a field out of the leftover bucket is a one-line addition here plus adding
92
+ its key to `contentKeys` or `diagnosticKeys`.
93
+
94
+ ## Leftover handling
95
+
96
+ Any key in `record` that isn't in `fieldRules` (or `identityKeys`) is resolved by
97
+ `inferRule(key, value)`, which looks only at the **value's shape**:
98
+
99
+ - `null` / `undefined` / `''` / `[]` → `empty`
100
+ - `boolean` → `flag`
101
+ - `number` → `stat`
102
+ - `'ALL_CAPS_WITH_UNDERSCORES'` → `pill`
103
+ - long prose / multi-sentence string → `lead`
104
+ - short string with no spaces → `mono`
105
+ - other string → `text`
106
+ - array of questions (`?`-terminated) → `qa`
107
+ - array of sentences → `bullets`
108
+ - array of short tokens → `tags`
109
+ - anything else (nested objects, arrays of objects) → `json`
110
+
111
+ These render inside RecordView's diagnostics panel under "Additional Fields", clearly
112
+ marked as unmapped, so a new field your pipeline starts emitting never disappears —
113
+ it just isn't styled by hand yet.
114
+
115
+ ## Theming
116
+
117
+ ```tsx
118
+ import { FieldKitProvider } from '@x12i/fieldkit';
119
+
120
+ <FieldKitProvider
121
+ theme={{
122
+ colors: { accent: '#5B9BD5', background: '#0B0F14' },
123
+ fonts: { heading: 'Poppins, sans-serif', body: 'Inter, sans-serif' },
124
+ }}
125
+ >
126
+ <RecordView {...} />
127
+ </FieldKitProvider>;
128
+ ```
129
+
130
+ Anything you don't override keeps `defaultTheme`. `RecordView` also works with no
131
+ provider at all — it falls back to `defaultTheme` via context.
132
+
133
+ Enum colors are a separate axis from the visual theme: pass `toneMap` (your enum
134
+ value → tone name, e.g. `amber`/`rose`/`green`/`blue`/`violet`/`teal`) as a prop on
135
+ `RecordView`. Anything not in your map still gets a deterministic color via
136
+ `hashTone()` instead of falling back to gray every time.
137
+
138
+ ## Composable primitives
139
+
140
+ `RecordView` is a default assembly of the pieces below — reach for them directly if
141
+ you want a different layout:
142
+
143
+ - `resolveField(key, value, rule, options)` — turn one field into a `ResolvedField`.
144
+ - `inferRule(key, value)` — the leftover shape-inference function.
145
+ - `FieldRenderer` — renders one `ResolvedField`.
146
+ - `labelFromKey`, `defaultClassifySignal`, `hashTone`, `toneHex`, `TONE_PALETTE`.
147
+
148
+ ## Status
149
+
150
+ `1.0.0` — first cut, ported from a design-canvas prototype. Known gaps to add next:
151
+ a `group` kind for nested objects (currently falls into the `json` catch-all), and a
152
+ richer default `classifySignal` (currently a 3-keyword heuristic — pass your own via
153
+ the `classifySignal` prop until this improves).