@philosaether/chipper 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.
- package/LICENSE +21 -0
- package/README.md +778 -0
- package/dist/base.css +1 -0
- package/dist/headless.d.ts +16 -0
- package/dist/headless.d.ts.map +1 -0
- package/dist/headless.js +18 -0
- package/dist/index.js +1445 -0
- package/dist/src/builder/index.d.ts +82 -0
- package/dist/src/builder/index.d.ts.map +1 -0
- package/dist/src/builder/predicates.d.ts +18 -0
- package/dist/src/builder/predicates.d.ts.map +1 -0
- package/dist/src/components/Chip.d.ts +15 -0
- package/dist/src/components/Chip.d.ts.map +1 -0
- package/dist/src/components/ChipInfoPopup.d.ts +12 -0
- package/dist/src/components/ChipInfoPopup.d.ts.map +1 -0
- package/dist/src/components/ChipPopup.d.ts +20 -0
- package/dist/src/components/ChipPopup.d.ts.map +1 -0
- package/dist/src/components/Chipper.d.ts +25 -0
- package/dist/src/components/Chipper.d.ts.map +1 -0
- package/dist/src/components/Clause.d.ts +13 -0
- package/dist/src/components/Clause.d.ts.map +1 -0
- package/dist/src/components/Sentence.d.ts +15 -0
- package/dist/src/components/Sentence.d.ts.map +1 -0
- package/dist/src/components/index.d.ts +12 -0
- package/dist/src/components/index.d.ts.map +1 -0
- package/dist/src/components/popups/AlternativeCoordinatePopup.d.ts +16 -0
- package/dist/src/components/popups/AlternativeCoordinatePopup.d.ts.map +1 -0
- package/dist/src/components/popups/KeywordGroupList.d.ts +29 -0
- package/dist/src/components/popups/KeywordGroupList.d.ts.map +1 -0
- package/dist/src/components/popups/KeywordOrExpressionPopup.d.ts +30 -0
- package/dist/src/components/popups/KeywordOrExpressionPopup.d.ts.map +1 -0
- package/dist/src/components/popups/MultiSelectPopup.d.ts +20 -0
- package/dist/src/components/popups/MultiSelectPopup.d.ts.map +1 -0
- package/dist/src/components/popups/NumericInput.d.ts +16 -0
- package/dist/src/components/popups/NumericInput.d.ts.map +1 -0
- package/dist/src/components/popups/ReferencePopup.d.ts +20 -0
- package/dist/src/components/popups/ReferencePopup.d.ts.map +1 -0
- package/dist/src/core/actions/set-chip-value.d.ts +17 -0
- package/dist/src/core/actions/set-chip-value.d.ts.map +1 -0
- package/dist/src/core/actions/set-context.d.ts +18 -0
- package/dist/src/core/actions/set-context.d.ts.map +1 -0
- package/dist/src/core/actions/set-display-value.d.ts +18 -0
- package/dist/src/core/actions/set-display-value.d.ts.map +1 -0
- package/dist/src/core/actions/toggle-clause.d.ts +15 -0
- package/dist/src/core/actions/toggle-clause.d.ts.map +1 -0
- package/dist/src/core/context-resolution.d.ts +36 -0
- package/dist/src/core/context-resolution.d.ts.map +1 -0
- package/dist/src/core/initialize.d.ts +43 -0
- package/dist/src/core/initialize.d.ts.map +1 -0
- package/dist/src/core/mode-switching.d.ts +10 -0
- package/dist/src/core/mode-switching.d.ts.map +1 -0
- package/dist/src/core/reducer.d.ts +16 -0
- package/dist/src/core/reducer.d.ts.map +1 -0
- package/dist/src/core/resolve-keyword-label.d.ts +7 -0
- package/dist/src/core/resolve-keyword-label.d.ts.map +1 -0
- package/dist/src/core/serialize.d.ts +34 -0
- package/dist/src/core/serialize.d.ts.map +1 -0
- package/dist/src/core/state.d.ts +59 -0
- package/dist/src/core/state.d.ts.map +1 -0
- package/dist/src/core/store.d.ts +23 -0
- package/dist/src/core/store.d.ts.map +1 -0
- package/dist/src/core/types.d.ts +242 -0
- package/dist/src/core/types.d.ts.map +1 -0
- package/dist/src/domains/alternative-coordinate.d.ts +110 -0
- package/dist/src/domains/alternative-coordinate.d.ts.map +1 -0
- package/dist/src/domains/create-domain.d.ts +30 -0
- package/dist/src/domains/create-domain.d.ts.map +1 -0
- package/dist/src/domains/facades.d.ts +134 -0
- package/dist/src/domains/facades.d.ts.map +1 -0
- package/dist/src/domains/index.d.ts +14 -0
- package/dist/src/domains/index.d.ts.map +1 -0
- package/dist/src/domains/keyword-or-expression.d.ts +148 -0
- package/dist/src/domains/keyword-or-expression.d.ts.map +1 -0
- package/dist/src/domains/multi-select.d.ts +68 -0
- package/dist/src/domains/multi-select.d.ts.map +1 -0
- package/dist/src/domains/normalize-keywords.d.ts +83 -0
- package/dist/src/domains/normalize-keywords.d.ts.map +1 -0
- package/dist/src/domains/reference.d.ts +89 -0
- package/dist/src/domains/reference.d.ts.map +1 -0
- package/dist/src/hooks/SentenceProvider.d.ts +17 -0
- package/dist/src/hooks/SentenceProvider.d.ts.map +1 -0
- package/dist/src/hooks/context.d.ts +31 -0
- package/dist/src/hooks/context.d.ts.map +1 -0
- package/dist/src/hooks/index.d.ts +13 -0
- package/dist/src/hooks/index.d.ts.map +1 -0
- package/dist/src/hooks/useChip.d.ts +21 -0
- package/dist/src/hooks/useChip.d.ts.map +1 -0
- package/dist/src/hooks/useDisplaySource.d.ts +16 -0
- package/dist/src/hooks/useDisplaySource.d.ts.map +1 -0
- package/dist/src/hooks/useKeyboardNavigation.d.ts +44 -0
- package/dist/src/hooks/useKeyboardNavigation.d.ts.map +1 -0
- package/dist/src/hooks/usePopup.d.ts +13 -0
- package/dist/src/hooks/usePopup.d.ts.map +1 -0
- package/dist/src/hooks/useReferenceDisplay.d.ts +22 -0
- package/dist/src/hooks/useReferenceDisplay.d.ts.map +1 -0
- package/dist/src/hooks/useSentence.d.ts +14 -0
- package/dist/src/hooks/useSentence.d.ts.map +1 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/palette/index.d.ts +29 -0
- package/dist/src/palette/index.d.ts.map +1 -0
- package/dist/src/themes/apply-theme.d.ts +34 -0
- package/dist/src/themes/apply-theme.d.ts.map +1 -0
- package/dist/src/themes/create-hue.d.ts +23 -0
- package/dist/src/themes/create-hue.d.ts.map +1 -0
- package/dist/src/themes/index.d.ts +13 -0
- package/dist/src/themes/index.d.ts.map +1 -0
- package/dist/src/themes/midnight.d.ts +11 -0
- package/dist/src/themes/midnight.d.ts.map +1 -0
- package/dist/src/themes/praxis.d.ts +11 -0
- package/dist/src/themes/praxis.d.ts.map +1 -0
- package/dist/src/themes/terminal.d.ts +9 -0
- package/dist/src/themes/terminal.d.ts.map +1 -0
- package/dist/src/themes/types.d.ts +72 -0
- package/dist/src/themes/types.d.ts.map +1 -0
- package/dist/styles.css +1 -0
- package/dist/themes/index.js +256 -0
- package/dist/themes/midnight.css +1 -0
- package/dist/themes/praxis.css +1 -0
- package/dist/themes/terminal.css +1 -0
- package/dist/usePopup-Of6OHa1_.js +653 -0
- package/package.json +75 -0
package/README.md
ADDED
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
# Chipper
|
|
2
|
+
|
|
3
|
+
Plain-English editing interfaces for complex configuration.
|
|
4
|
+
|
|
5
|
+
Chipper is a React library that lets users build structured data by clicking
|
|
6
|
+
semantic chips arranged in readable sentences. Each chip is an interactive
|
|
7
|
+
input — keywords, text fields, number steppers, date pickers, multi-selects —
|
|
8
|
+
but the sentence reads like natural language.
|
|
9
|
+
|
|
10
|
+
> Every **[2]** **[weeks]** on **[tuesday]**, create a task named **[review accounts]**.
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
npm install chipper
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { Chipper, sentence, builder, extendPalette, keywordDomain } from 'chipper';
|
|
20
|
+
import 'chipper/styles.css';
|
|
21
|
+
|
|
22
|
+
const palette = extendPalette({
|
|
23
|
+
chips: {
|
|
24
|
+
priority: keywordDomain({
|
|
25
|
+
color: 'rose',
|
|
26
|
+
keywords: [{ value: 'low' }, { value: 'medium' }, { value: 'high' }],
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const mySentence = sentence(palette)
|
|
32
|
+
.clause('main', builder()
|
|
33
|
+
.text('Set priority to')
|
|
34
|
+
.chip('priority')
|
|
35
|
+
.text('.')
|
|
36
|
+
)
|
|
37
|
+
.build();
|
|
38
|
+
|
|
39
|
+
function App() {
|
|
40
|
+
return (
|
|
41
|
+
<Chipper
|
|
42
|
+
sentence={mySentence}
|
|
43
|
+
onChange={(state) => console.log(state)}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That's the minimum: a palette with one domain, a sentence with one clause,
|
|
50
|
+
and a `<Chipper>` component to render it.
|
|
51
|
+
|
|
52
|
+
## Core Concepts
|
|
53
|
+
|
|
54
|
+
### Sentence
|
|
55
|
+
|
|
56
|
+
One complete unit of input. Built with the `sentence()` builder, which
|
|
57
|
+
accepts a palette and chains `.clause()` calls:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const mySentence = sentence(palette)
|
|
61
|
+
.clause('first', builder().text('Do').chip('action'))
|
|
62
|
+
.clause('second', builder().text('at').chip('time'))
|
|
63
|
+
.build();
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Clause
|
|
67
|
+
|
|
68
|
+
A fragment of a sentence containing text and chips. Built with `builder()`.
|
|
69
|
+
Clauses can be required (default), optional (user-toggled), or contingent
|
|
70
|
+
(shown/hidden by the engine based on other chip values).
|
|
71
|
+
|
|
72
|
+
### Chip
|
|
73
|
+
|
|
74
|
+
An interactive input within a clause. Added with `.chip('id')`. The chip
|
|
75
|
+
ID maps to a domain name in the palette — when they match, you only need
|
|
76
|
+
the ID:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
builder().text('Pick a').chip('color') // looks up 'color' domain in palette
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Domain
|
|
83
|
+
|
|
84
|
+
Defines a chip's value space: what values are valid, how they display in
|
|
85
|
+
the chip trigger, and what popup UI appears when the user clicks.
|
|
86
|
+
|
|
87
|
+
### Palette
|
|
88
|
+
|
|
89
|
+
Maps domain names to domain instances. Created with `extendPalette()`:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const palette = extendPalette({
|
|
93
|
+
chips: {
|
|
94
|
+
color: keywordDomain({ color: 'sage', keywords: [{ value: 'red' }, { value: 'blue' }] }),
|
|
95
|
+
name: textDomain({ color: 'rose', placeholder: 'a name' }),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Line
|
|
101
|
+
|
|
102
|
+
Visual grouping. Clauses after `.line()` render on a new row. Lines with
|
|
103
|
+
all-optional or all-contingent clauses auto-indent:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
sentence(palette)
|
|
107
|
+
.clause('trigger', builder().text('Every').chip('cadence').produces('cadence'))
|
|
108
|
+
.line()
|
|
109
|
+
.clause('detail', builder()
|
|
110
|
+
.optional()
|
|
111
|
+
.text('at')
|
|
112
|
+
.chip('time')
|
|
113
|
+
)
|
|
114
|
+
.build();
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Domain Types
|
|
118
|
+
|
|
119
|
+
### Simple Domains
|
|
120
|
+
|
|
121
|
+
These cover the most common chip types. Each is a thin wrapper over the
|
|
122
|
+
engine — start here.
|
|
123
|
+
|
|
124
|
+
#### `keywordDomain(config)`
|
|
125
|
+
|
|
126
|
+
Fixed set of options. The user clicks one.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
keywordDomain({
|
|
130
|
+
color: 'sage',
|
|
131
|
+
keywords: [
|
|
132
|
+
{ value: 'low' },
|
|
133
|
+
{ value: 'medium' },
|
|
134
|
+
{ value: 'high', label: 'High Priority' },
|
|
135
|
+
],
|
|
136
|
+
default: 'medium', // optional — defaults to first keyword
|
|
137
|
+
placeholder: 'a level', // optional — shown when value is invalid
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
- `color` — semantic color key (maps to `--chipper-color-{key}-*` CSS properties)
|
|
142
|
+
- `keywords` — array of `{ value, label?, display? }`. `label` defaults to
|
|
143
|
+
`value`. `display` (shown on the chip trigger) defaults to `label`.
|
|
144
|
+
- `default` — initial value. Defaults to first keyword.
|
|
145
|
+
- `placeholder` — chip trigger text when value is invalid.
|
|
146
|
+
|
|
147
|
+
#### `textDomain(config)`
|
|
148
|
+
|
|
149
|
+
Free-text input. The user types a value.
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
textDomain({
|
|
153
|
+
color: 'rose',
|
|
154
|
+
placeholder: 'a task name',
|
|
155
|
+
maxLength: 200, // default 140
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
- `maxLength` — character limit (default 140)
|
|
160
|
+
- `validate` — custom validation function beyond non-empty
|
|
161
|
+
- `display` — format the value for the chip trigger
|
|
162
|
+
- `keywords` — optional preset values shown as pills above the text input
|
|
163
|
+
|
|
164
|
+
#### `numberDomain(config)`
|
|
165
|
+
|
|
166
|
+
Numeric input with a stepper UI (+/- buttons).
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
numberDomain({
|
|
170
|
+
color: 'copper',
|
|
171
|
+
min: 1,
|
|
172
|
+
max: 365,
|
|
173
|
+
step: 1, // default 1
|
|
174
|
+
suffix: 'days', // shown after the value
|
|
175
|
+
placeholder: 'a number',
|
|
176
|
+
})
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
- `min`, `max`, `step` — stepper bounds
|
|
180
|
+
- `prefix`, `suffix` — text flanking the input. Can be static strings or
|
|
181
|
+
context-aware functions: `suffix: (ctx) => ctx.unit + 's'`
|
|
182
|
+
- `keywords` — optional preset values
|
|
183
|
+
|
|
184
|
+
#### `dateDomain(config)`
|
|
185
|
+
|
|
186
|
+
Calendar date picker. Values are `YYYY-MM-DD` strings.
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
dateDomain({
|
|
190
|
+
color: 'sage',
|
|
191
|
+
keywords: [
|
|
192
|
+
{ value: 'tomorrow', label: 'tomorrow' },
|
|
193
|
+
{ value: 'next-monday', label: 'next Monday' },
|
|
194
|
+
],
|
|
195
|
+
placeholder: 'a date',
|
|
196
|
+
})
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
- `validate` — custom validation beyond YYYY-MM-DD format
|
|
200
|
+
- `display` — format the date for the chip trigger
|
|
201
|
+
- `keywords` — preset date shortcuts
|
|
202
|
+
|
|
203
|
+
### Power-User Domains
|
|
204
|
+
|
|
205
|
+
#### `keywordOrExpressionDomain(config)`
|
|
206
|
+
|
|
207
|
+
Keywords plus optional freeform expression input. The simple domains
|
|
208
|
+
delegate to this internally — use it directly when you need:
|
|
209
|
+
|
|
210
|
+
- **Trigger-gated expression**: a keyword that reveals the freeform input
|
|
211
|
+
- **Context-aware labels**: keyword labels that change based on other chips
|
|
212
|
+
- **Full expression config**: custom input types, prefix/suffix, validation
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
keywordOrExpressionDomain({
|
|
216
|
+
color: 'copper',
|
|
217
|
+
keywords: [
|
|
218
|
+
{ value: 'daily', label: 'day' },
|
|
219
|
+
{ value: 'weekly', label: 'week' },
|
|
220
|
+
],
|
|
221
|
+
expression: numericExpression({
|
|
222
|
+
min: 1,
|
|
223
|
+
max: 365,
|
|
224
|
+
trigger: { label: 'custom interval', default: '2' },
|
|
225
|
+
}),
|
|
226
|
+
default: 'weekly',
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The `trigger` option hides the expression input until the user clicks
|
|
231
|
+
"custom interval". Without a trigger, the input is always visible.
|
|
232
|
+
|
|
233
|
+
Expression helpers: `textExpression()`, `numericExpression()`,
|
|
234
|
+
`dateExpression()` — sugar for building `ExpressionConfig` objects.
|
|
235
|
+
|
|
236
|
+
#### `multiSelectDomain(config)`
|
|
237
|
+
|
|
238
|
+
Toggle grid for selecting multiple values. The chip displays selected
|
|
239
|
+
items (up to 3, then a count).
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
multiSelectDomain({
|
|
243
|
+
color: 'sage',
|
|
244
|
+
options: [
|
|
245
|
+
{ label: 'Mon', value: 'mon' },
|
|
246
|
+
{ label: 'Tue', value: 'tue' },
|
|
247
|
+
// ...
|
|
248
|
+
],
|
|
249
|
+
keywords: [
|
|
250
|
+
{ label: 'weekdays', value: ['mon', 'tue', 'wed', 'thu', 'fri'] },
|
|
251
|
+
],
|
|
252
|
+
placeholder: 'one or more days',
|
|
253
|
+
countLabel: 'days',
|
|
254
|
+
})
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
- `options` — individual toggle items
|
|
258
|
+
- `keywords` — group shortcuts (selecting "weekdays" toggles all five)
|
|
259
|
+
- `countLabel` — label for "N selected" display (e.g., "3 days")
|
|
260
|
+
|
|
261
|
+
#### `alternativeCoordinateDomain(config)`
|
|
262
|
+
|
|
263
|
+
Tabbed popup with multiple input modes. Each mode has slots that compose
|
|
264
|
+
into a single value.
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
alternativeCoordinateDomain({
|
|
268
|
+
color: 'sage',
|
|
269
|
+
modes: [
|
|
270
|
+
{
|
|
271
|
+
id: 'date',
|
|
272
|
+
label: 'Date',
|
|
273
|
+
slots: [{ prefix: 'the', keywords: [{ label: 'first', value: '1' }] }],
|
|
274
|
+
compose: (day) => day,
|
|
275
|
+
decompose: (v) => [v],
|
|
276
|
+
display: (v) => `the ${v}th`,
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
placeholder: 'a day',
|
|
280
|
+
})
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
#### `referenceDomain(config)`
|
|
284
|
+
|
|
285
|
+
Hierarchical navigation + search for external data. Supports async sources.
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
referenceDomain({
|
|
289
|
+
color: 'indigo',
|
|
290
|
+
source: {
|
|
291
|
+
getItems: async (path) => fetchCategories(path),
|
|
292
|
+
search: async (query) => searchCategories(query),
|
|
293
|
+
resolveDisplay: async (id) => getCategoryLabel(id),
|
|
294
|
+
},
|
|
295
|
+
placeholder: 'a category',
|
|
296
|
+
})
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Building Sentences
|
|
300
|
+
|
|
301
|
+
### Optional Clauses
|
|
302
|
+
|
|
303
|
+
Users can toggle optional clauses on and off. Dormant optional clauses
|
|
304
|
+
render as muted italic text showing their configured values:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
.clause('detail', builder()
|
|
308
|
+
.optional()
|
|
309
|
+
.text('with priority')
|
|
310
|
+
.chip('priority')
|
|
311
|
+
)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Contingent Clauses
|
|
315
|
+
|
|
316
|
+
Clauses that appear or disappear based on other chip values. Use
|
|
317
|
+
`.contingentOn()` with the ID of the clause that produces the context:
|
|
318
|
+
|
|
319
|
+
```ts
|
|
320
|
+
.clause('trigger', builder()
|
|
321
|
+
.text('Every')
|
|
322
|
+
.chip('cadence')
|
|
323
|
+
.produces('cadence') // makes cadence value available as context
|
|
324
|
+
)
|
|
325
|
+
.clause('weekday', builder()
|
|
326
|
+
.text('on')
|
|
327
|
+
.chip('day')
|
|
328
|
+
.contingentOn('trigger', (ctx) => ctx.cadence === 'weekly')
|
|
329
|
+
)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
The `weekday` clause only appears when cadence is "weekly". Context flows
|
|
333
|
+
down the contingency tree — a clause reads context from its superclause
|
|
334
|
+
and all ancestors.
|
|
335
|
+
|
|
336
|
+
**Lambda shorthand**: when you only need a presence predicate (no domain
|
|
337
|
+
reconfiguration), pass a bare function:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
.contingentOn('trigger', (ctx) => ctx.cadence === 'weekly')
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Object form**: for cases that also need domain reconfiguration:
|
|
344
|
+
|
|
345
|
+
```ts
|
|
346
|
+
.contingentOn('trigger', {
|
|
347
|
+
present: (ctx) => ctx.cadence === 'weekly',
|
|
348
|
+
configure: (ctx) => ({ keywords: getOptionsFor(ctx.cadence) }),
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Context Propagation
|
|
353
|
+
|
|
354
|
+
`.produces()` declares what context keys a clause makes available to
|
|
355
|
+
contingent clauses:
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
// String shorthand — clause ID as context key, maps to its chip value
|
|
359
|
+
.produces('cadence')
|
|
360
|
+
|
|
361
|
+
// Object form — explicit mapping
|
|
362
|
+
.produces({ cadenceMeasure: 'cadenceMeasure', cadenceUnit: 'cadenceUnit' })
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### Chip-Level Contingency
|
|
366
|
+
|
|
367
|
+
Individual chips within a clause can be shown/hidden based on context:
|
|
368
|
+
|
|
369
|
+
```ts
|
|
370
|
+
builder()
|
|
371
|
+
.text('Every')
|
|
372
|
+
.chip('measure')
|
|
373
|
+
.chip('unit', { present: (ctx) => !isNaN(Number(ctx.measure)) })
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
The `unit` chip only appears when `measure` is numeric. Hidden chips are
|
|
377
|
+
excluded from context production.
|
|
378
|
+
|
|
379
|
+
### Keywords
|
|
380
|
+
|
|
381
|
+
Keywords support several display options:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
{
|
|
385
|
+
value: 'daily', // the stored value
|
|
386
|
+
label: 'day', // popup pill text (defaults to value)
|
|
387
|
+
display: 'every day', // chip trigger text (defaults to label)
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
Labels can be context-aware functions:
|
|
392
|
+
|
|
393
|
+
```ts
|
|
394
|
+
{ value: '1', label: (ctx) => `next ${ctx.unit ?? 'month'}` }
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Keyword Groups
|
|
398
|
+
|
|
399
|
+
Keywords can be organized into visual groups with labels, separators, and
|
|
400
|
+
layout control. Mix plain keywords and groups freely — ungrouped keywords
|
|
401
|
+
collect into an implicit group at the top:
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
keywordDomain({
|
|
405
|
+
color: 'sage',
|
|
406
|
+
keywords: [
|
|
407
|
+
{ value: '1', label: '1st' },
|
|
408
|
+
{ value: '15', label: '15th' },
|
|
409
|
+
{ value: 'last', label: 'last day' },
|
|
410
|
+
{
|
|
411
|
+
label: 'date',
|
|
412
|
+
layout: 'grid',
|
|
413
|
+
columns: 7,
|
|
414
|
+
keywords: Array.from({ length: 31 }, (_, i) => ({
|
|
415
|
+
value: String(i + 1),
|
|
416
|
+
label: String(i + 1),
|
|
417
|
+
})),
|
|
418
|
+
},
|
|
419
|
+
],
|
|
420
|
+
})
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
Group options:
|
|
424
|
+
|
|
425
|
+
| Option | Type | Default | Description |
|
|
426
|
+
|--------|------|---------|-------------|
|
|
427
|
+
| `label` | `string` | — | Header text above the group |
|
|
428
|
+
| `keywords` | `KeywordConfig[]` | *required* | Keywords in this group |
|
|
429
|
+
| `layout` | `'flow' \| 'grid'` | `'flow'` | Layout mode |
|
|
430
|
+
| `columns` | `number` | `7` | Grid columns (only with `layout: 'grid'`) |
|
|
431
|
+
| `prefix` | `string` | — | Text before keyword pills (e.g., "the") |
|
|
432
|
+
|
|
433
|
+
Grouping works across all keyword-accepting domains: `keywordDomain`,
|
|
434
|
+
`textDomain`, `numberDomain`, `dateDomain`, `keywordOrExpressionDomain`,
|
|
435
|
+
`multiSelectDomain` (options), and `alternativeCoordinateDomain` (slot keywords).
|
|
436
|
+
|
|
437
|
+
## Display Chips
|
|
438
|
+
|
|
439
|
+
Not every chip needs user input. **Display chips** show values from
|
|
440
|
+
external sources — fixed strings, derived computations, remote APIs,
|
|
441
|
+
or live subscriptions. Add `display` to any `.chip()` call:
|
|
442
|
+
|
|
443
|
+
```ts
|
|
444
|
+
// Static value — debugging, scaffolding, or contextually fixed data
|
|
445
|
+
.chip('project', 'projectName', { display: 'Praxis' })
|
|
446
|
+
|
|
447
|
+
// Derived from context — uses the same ctx pattern as contingency lambdas
|
|
448
|
+
.chip('cost', 'currency', {
|
|
449
|
+
display: (ctx) => lookupPrice(ctx.item)
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
// Remote fetch — one-shot or polling
|
|
453
|
+
.chip('weather', 'text', {
|
|
454
|
+
display: { url: '/api/weather', extract: (r: any) => r.temp, interval: 60000 }
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
// External subscription — WebSocket, EventSource, etc.
|
|
458
|
+
.chip('price', 'currency', {
|
|
459
|
+
display: { subscribe: (cb) => stockTicker.on('AAPL', cb) }
|
|
460
|
+
})
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Display chips render with no border and a pastel background. They're
|
|
464
|
+
visually distinct from interactive chips — the user can see them but
|
|
465
|
+
can't edit them.
|
|
466
|
+
|
|
467
|
+
### Info Popup
|
|
468
|
+
|
|
469
|
+
Display chips can show provenance info on click:
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
.chip('elapsed', 'text', {
|
|
473
|
+
display: (ctx) => formatElapsed(new Date('2026-05-15')),
|
|
474
|
+
info: 'Time elapsed since May 15, 2026',
|
|
475
|
+
})
|
|
476
|
+
|
|
477
|
+
// Dynamic info content
|
|
478
|
+
.chip('total', 'currency', {
|
|
479
|
+
display: (ctx) => computeTotal(ctx),
|
|
480
|
+
info: (value, state) => `Sum of ${countItems(state)} line items`,
|
|
481
|
+
})
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### Source Types
|
|
485
|
+
|
|
486
|
+
| Shorthand | Source | Description |
|
|
487
|
+
|-----------|--------|-------------|
|
|
488
|
+
| Primitive (`'Praxis'`, `42`) | `static` | Fixed value, set once |
|
|
489
|
+
| Function (`(ctx) => ...`) | `derived` | Recomputes on state change, receives clause context |
|
|
490
|
+
| `{ url, extract, interval? }` | `remote` | Fetches from URL, optional polling |
|
|
491
|
+
| `{ subscribe }` | `external` | Consumer-managed subscription |
|
|
492
|
+
|
|
493
|
+
### Visual States
|
|
494
|
+
|
|
495
|
+
| State | Appearance |
|
|
496
|
+
|-------|-----------|
|
|
497
|
+
| Normal | Pastel background, no border |
|
|
498
|
+
| Loading | Subtle pulse animation |
|
|
499
|
+
| Error | Error-colored border |
|
|
500
|
+
| Info open | Accent glow (same as expanded interactive chips) |
|
|
501
|
+
|
|
502
|
+
### Serialization
|
|
503
|
+
|
|
504
|
+
Static display chips are included in serialized output (they hold real
|
|
505
|
+
values). Derived, remote, and external display chips are excluded — their
|
|
506
|
+
values are ephemeral.
|
|
507
|
+
|
|
508
|
+
## Reading State
|
|
509
|
+
|
|
510
|
+
The `onChange` callback receives a `SentenceState` on every change:
|
|
511
|
+
|
|
512
|
+
```ts
|
|
513
|
+
<Chipper sentence={mySentence} onChange={(state) => {
|
|
514
|
+
// state.valid — is the entire sentence valid?
|
|
515
|
+
// state.clauses — keyed by clause ID
|
|
516
|
+
// .active — is this clause currently shown?
|
|
517
|
+
// .valid — are all chips in this clause valid?
|
|
518
|
+
// .chips — keyed by chip ID
|
|
519
|
+
// .value — the current value
|
|
520
|
+
// .displayValue — formatted for display
|
|
521
|
+
// .valid — does the value pass domain validation?
|
|
522
|
+
// .dirty — has the user changed this chip?
|
|
523
|
+
}} />
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## Theming
|
|
527
|
+
|
|
528
|
+
Chipper's appearance is controlled by CSS custom properties with the
|
|
529
|
+
`--chipper-*` prefix. Override them to match your app:
|
|
530
|
+
|
|
531
|
+
```css
|
|
532
|
+
:root {
|
|
533
|
+
--chipper-bg-primary: #1a1b2e;
|
|
534
|
+
--chipper-text-primary: #e0e0ef;
|
|
535
|
+
--chipper-accent: #7b9fd4;
|
|
536
|
+
}
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Chip Colors
|
|
540
|
+
|
|
541
|
+
Each chip gets a semantic color via the `color` config field. Colors map
|
|
542
|
+
to three CSS properties:
|
|
543
|
+
|
|
544
|
+
```css
|
|
545
|
+
--chipper-color-{name}-text /* chip text color */
|
|
546
|
+
--chipper-color-{name}-bg /* chip background */
|
|
547
|
+
--chipper-color-{name}-hover /* chip hover state */
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
The default theme (praxis) provides: copper, sage, slate, stone, teal,
|
|
551
|
+
rose, umber, plum, indigo.
|
|
552
|
+
|
|
553
|
+
### Font
|
|
554
|
+
|
|
555
|
+
Chipper inherits the consumer's font by default. Override with
|
|
556
|
+
`--chipper-font`.
|
|
557
|
+
|
|
558
|
+
## Headless Mode
|
|
559
|
+
|
|
560
|
+
Import from `chipper/headless` for hooks without components:
|
|
561
|
+
|
|
562
|
+
```ts
|
|
563
|
+
import { SentenceProvider, useSentence, useChip, usePopup } from 'chipper/headless';
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Wrap your custom UI in `SentenceProvider`:
|
|
567
|
+
|
|
568
|
+
```tsx
|
|
569
|
+
<SentenceProvider definition={mySentence} onChange={handleChange}>
|
|
570
|
+
<MyCustomSentenceUI />
|
|
571
|
+
</SentenceProvider>
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
Then use hooks inside:
|
|
575
|
+
|
|
576
|
+
- `useSentence()` — sentence-level state, dispatch, definition, resolved domains
|
|
577
|
+
- `useChip(clauseId, chipId)` — chip state + `setValue` function
|
|
578
|
+
- `usePopup()` — singleton popup state: `open()`, `close()`, `isOpen()`
|
|
579
|
+
|
|
580
|
+
## `<Chipper>` Component
|
|
581
|
+
|
|
582
|
+
The main entry point. Wraps `SentenceProvider` + `Sentence`:
|
|
583
|
+
|
|
584
|
+
```tsx
|
|
585
|
+
<Chipper
|
|
586
|
+
sentence={mySentence} // SentenceDefinition from .build()
|
|
587
|
+
onChange={(state) => {}} // called on every state change
|
|
588
|
+
/>
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
## API Reference
|
|
592
|
+
|
|
593
|
+
### Domain Factories
|
|
594
|
+
|
|
595
|
+
#### `keywordDomain(config)` — Fixed keyword set
|
|
596
|
+
|
|
597
|
+
| Option | Type | Default | Description |
|
|
598
|
+
|--------|------|---------|-------------|
|
|
599
|
+
| `color` | `string` | *required* | Semantic color key |
|
|
600
|
+
| `keywords` | `KeywordGroupItem[]` | *required* | Values: `{ value, label?, display? }` or groups: `{ label?, keywords, layout?, columns?, prefix? }` |
|
|
601
|
+
| `default` | `string` | first keyword | Initial value |
|
|
602
|
+
| `placeholder` | `string` | — | Chip text when value is invalid |
|
|
603
|
+
|
|
604
|
+
#### `textDomain(config)` — Free-text input
|
|
605
|
+
|
|
606
|
+
| Option | Type | Default | Description |
|
|
607
|
+
|--------|------|---------|-------------|
|
|
608
|
+
| `color` | `string` | *required* | Semantic color key |
|
|
609
|
+
| `placeholder` | `string` | — | Chip text when empty |
|
|
610
|
+
| `default` | `string` | `''` | Initial value |
|
|
611
|
+
| `maxLength` | `number` | `140` | Character limit |
|
|
612
|
+
| `validate` | `(v: string) => boolean` | non-empty | Custom validation |
|
|
613
|
+
| `display` | `(v: string) => string` | identity | Format value for chip trigger |
|
|
614
|
+
| `keywords` | `KeywordConfig[]` | — | Optional preset pills |
|
|
615
|
+
|
|
616
|
+
#### `numberDomain(config)` — Numeric stepper
|
|
617
|
+
|
|
618
|
+
| Option | Type | Default | Description |
|
|
619
|
+
|--------|------|---------|-------------|
|
|
620
|
+
| `color` | `string` | *required* | Semantic color key |
|
|
621
|
+
| `placeholder` | `string` | — | Chip text when empty |
|
|
622
|
+
| `default` | `string` | `''` | Initial value |
|
|
623
|
+
| `min` | `number` | — | Minimum value |
|
|
624
|
+
| `max` | `number` | — | Maximum value |
|
|
625
|
+
| `step` | `number` | `1` | Stepper increment |
|
|
626
|
+
| `prefix` | `string \| (ctx) => string` | — | Text before input |
|
|
627
|
+
| `suffix` | `string \| (ctx) => string` | — | Text after input |
|
|
628
|
+
| `validate` | `(v: string) => boolean` | numeric check | Custom validation |
|
|
629
|
+
| `display` | `(v: string) => string` | identity | Format value for chip trigger |
|
|
630
|
+
| `keywords` | `KeywordConfig[]` | — | Optional preset pills |
|
|
631
|
+
|
|
632
|
+
#### `dateDomain(config)` — Calendar date picker
|
|
633
|
+
|
|
634
|
+
| Option | Type | Default | Description |
|
|
635
|
+
|--------|------|---------|-------------|
|
|
636
|
+
| `color` | `string` | *required* | Semantic color key |
|
|
637
|
+
| `placeholder` | `string` | — | Chip text when empty |
|
|
638
|
+
| `default` | `string` | `''` | Initial value |
|
|
639
|
+
| `validate` | `(v: string) => boolean` | YYYY-MM-DD | Custom validation |
|
|
640
|
+
| `display` | `(v: string) => string` | identity | Format date for chip trigger |
|
|
641
|
+
| `keywords` | `KeywordConfig[]` | — | Optional date presets |
|
|
642
|
+
|
|
643
|
+
#### `keywordOrExpressionDomain(config)` — Keywords + freeform expression
|
|
644
|
+
|
|
645
|
+
| Option | Type | Default | Description |
|
|
646
|
+
|--------|------|---------|-------------|
|
|
647
|
+
| `color` | `string` | *required* | Semantic color key |
|
|
648
|
+
| `keywords` | `KeywordGroupItem[]` | `[]` | Preset values (plain or grouped) |
|
|
649
|
+
| `expression` | `ExpressionConfig` | — | Freeform input config (omit for keywords-only) |
|
|
650
|
+
| `default` | `string` | first keyword or `''` | Initial value |
|
|
651
|
+
| `placeholder` | `string` | — | Chip text when value is invalid |
|
|
652
|
+
| `consumes` | `string[]` | — | Context keys read from ancestors |
|
|
653
|
+
| `produces` | `string[]` | — | Context keys written for descendants |
|
|
654
|
+
| `onContextChange` | `(ctx) => Partial<Domain>` | — | Reconfigure when context changes |
|
|
655
|
+
|
|
656
|
+
#### `expressionDomain(config)` — Expression-only (no keywords)
|
|
657
|
+
|
|
658
|
+
Same as `keywordOrExpressionDomain` but `expression` is required and
|
|
659
|
+
`keywords` is always empty.
|
|
660
|
+
|
|
661
|
+
#### `multiSelectDomain(config)` — Toggle grid
|
|
662
|
+
|
|
663
|
+
| Option | Type | Default | Description |
|
|
664
|
+
|--------|------|---------|-------------|
|
|
665
|
+
| `color` | `string` | *required* | Semantic color key |
|
|
666
|
+
| `options` | `KeywordGroupItem[]` | *required* | Individual toggle items (plain or grouped) |
|
|
667
|
+
| `keywords` | `{ label, value: string[] }[]` | `[]` | Group shortcuts |
|
|
668
|
+
| `default` | `string[]` | `[]` | Initially selected values |
|
|
669
|
+
| `placeholder` | `string` | — | Chip text when empty |
|
|
670
|
+
| `maxSelections` | `number` | — | Cap on selected items |
|
|
671
|
+
| `countLabel` | `string` | `'selected'` | Label for "N selected" display |
|
|
672
|
+
|
|
673
|
+
#### `alternativeCoordinateDomain(config)` — Tabbed multi-mode
|
|
674
|
+
|
|
675
|
+
| Option | Type | Default | Description |
|
|
676
|
+
|--------|------|---------|-------------|
|
|
677
|
+
| `color` | `string` | *required* | Semantic color key |
|
|
678
|
+
| `modes` | `AlternativeCoordinateMode[]` | *required* | Tab definitions with slots, compose, decompose, display |
|
|
679
|
+
| `default` | `string` | — | Initial composed value |
|
|
680
|
+
| `placeholder` | `string` | — | Chip text when value is invalid |
|
|
681
|
+
|
|
682
|
+
#### `referenceDomain(config)` — Hierarchical navigation + search
|
|
683
|
+
|
|
684
|
+
| Option | Type | Default | Description |
|
|
685
|
+
|--------|------|---------|-------------|
|
|
686
|
+
| `color` | `string` | *required* | Semantic color key |
|
|
687
|
+
| `source` | `ReferenceSource` | *required* | `{ getItems, search, resolveDisplay }` |
|
|
688
|
+
| `keywords` | `KeywordConfig[]` | `[]` | Static shortcut values |
|
|
689
|
+
| `default` | `string` | `''` | Initial value |
|
|
690
|
+
| `placeholder` | `string` | — | Chip text when value is invalid |
|
|
691
|
+
|
|
692
|
+
### Expression Helpers
|
|
693
|
+
|
|
694
|
+
#### `textExpression(options?)`
|
|
695
|
+
|
|
696
|
+
| Option | Type | Default | Description |
|
|
697
|
+
|--------|------|---------|-------------|
|
|
698
|
+
| `placeholder` | `string` | — | Input placeholder text |
|
|
699
|
+
| `maxLength` | `number` | — | Character limit |
|
|
700
|
+
| `validate` | `(v: string) => boolean` | non-empty | Custom validation |
|
|
701
|
+
| `display` | `(v: string) => string` | identity | Format for chip trigger |
|
|
702
|
+
| `prefix` | `string \| (ctx) => string` | — | Text before input |
|
|
703
|
+
| `suffix` | `string \| (ctx) => string` | — | Text after input |
|
|
704
|
+
| `position` | `'above' \| 'below'` | `'below'` | Input placement relative to keywords |
|
|
705
|
+
| `trigger` | `{ label, default }` | — | Keyword that reveals the input |
|
|
706
|
+
|
|
707
|
+
#### `numericExpression(options?)`
|
|
708
|
+
|
|
709
|
+
Same options as `textExpression`, plus:
|
|
710
|
+
|
|
711
|
+
| Option | Type | Default | Description |
|
|
712
|
+
|--------|------|---------|-------------|
|
|
713
|
+
| `min` | `number` | — | Minimum value |
|
|
714
|
+
| `max` | `number` | — | Maximum value |
|
|
715
|
+
| `step` | `number` | `1` | Stepper increment |
|
|
716
|
+
|
|
717
|
+
Default `validate` rejects empty strings and non-numeric values.
|
|
718
|
+
|
|
719
|
+
#### `dateExpression(options?)`
|
|
720
|
+
|
|
721
|
+
Same options as `textExpression`. Default `validate` checks YYYY-MM-DD
|
|
722
|
+
format and calendar validity.
|
|
723
|
+
|
|
724
|
+
### Builder
|
|
725
|
+
|
|
726
|
+
| Function | Description |
|
|
727
|
+
|----------|-------------|
|
|
728
|
+
| `sentence(palette?)` | Start building a sentence |
|
|
729
|
+
| `builder()` | Start building a clause |
|
|
730
|
+
| `chip(id, domainName?, options?)` | Standalone chip definition |
|
|
731
|
+
| `extendPalette(config)` | Create a palette with domain mappings |
|
|
732
|
+
|
|
733
|
+
#### Sentence Builder
|
|
734
|
+
|
|
735
|
+
| Method | Description |
|
|
736
|
+
|--------|-------------|
|
|
737
|
+
| `.clause(id, builder)` | Add a clause |
|
|
738
|
+
| `.line(options?)` | Start a new visual line |
|
|
739
|
+
| `.build()` | Return the `SentenceDefinition` |
|
|
740
|
+
|
|
741
|
+
#### Clause Builder
|
|
742
|
+
|
|
743
|
+
| Method | Description |
|
|
744
|
+
|--------|-------------|
|
|
745
|
+
| `.text(value)` | Add a text segment |
|
|
746
|
+
| `.chip(id, options?)` | Add a chip segment |
|
|
747
|
+
| `.optional()` | Make clause user-toggleable |
|
|
748
|
+
| `.contingentOn(id, config)` | Make clause context-dependent |
|
|
749
|
+
| `.produces(mapping)` | Declare context keys this clause produces |
|
|
750
|
+
| `.placeholder(text)` | Dormant clause display text |
|
|
751
|
+
|
|
752
|
+
### Hooks (headless)
|
|
753
|
+
|
|
754
|
+
| Hook | Description |
|
|
755
|
+
|------|-------------|
|
|
756
|
+
| `useSentence()` | Sentence state, dispatch, definition, domains |
|
|
757
|
+
| `useChip(clauseId, chipId)` | Chip state + `setValue` |
|
|
758
|
+
| `usePopup()` | Singleton popup: `open`, `close`, `isOpen` |
|
|
759
|
+
|
|
760
|
+
### Components
|
|
761
|
+
|
|
762
|
+
| Component | Description |
|
|
763
|
+
|-----------|-------------|
|
|
764
|
+
| `<Chipper>` | Auto-rendering entry point (`sentence` + `onChange`) |
|
|
765
|
+
| `<SentenceProvider>` | Context provider for headless mode |
|
|
766
|
+
| `<Sentence>` | Renders clauses grouped by lines |
|
|
767
|
+
| `<Clause>` | Renders text + chips for one clause |
|
|
768
|
+
| `<Chip>` | Trigger button + popup mount |
|
|
769
|
+
|
|
770
|
+
## Example
|
|
771
|
+
|
|
772
|
+
See [`demo/src/App.tsx`](demo/src/App.tsx) for a full working example
|
|
773
|
+
with contingent clauses, context propagation, multiple domain types,
|
|
774
|
+
theme switching, and multi-line sentences.
|
|
775
|
+
|
|
776
|
+
## License
|
|
777
|
+
|
|
778
|
+
MIT
|