@lotics/ui 46.8.1 → 46.9.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/docs/catalog.md CHANGED
@@ -1032,7 +1032,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1032
1032
  differs from the visible text is what breaks label-in-name.
1033
1033
  - **`select`** — `Select`: rich/custom-rendered, single/multi, select-all, chips via
1034
1034
  `renderSelected(item, { remove })` + `searchable` + `allowCustom` — the tag field is just
1035
- a multi Select; opens `OptionList`. **Set `searchPlaceholder` whenever `allowCustom` is on**:
1035
+ a multi Select; opens `OptionList`. **`emptyText` replaces the list's "No results" when the
1036
+ caller knows what the reader should DO about it** — where the missing thing gets created, why the
1037
+ set is narrowed. A select cannot know that; the reader is stuck at exactly that moment, and it is
1038
+ the one place a list may speak without becoming permanent furniture, since it shows only when the
1039
+ search finds nothing. Reach for it instead of a create-in-place row wherever the missing record
1040
+ cannot be validly made from this screen: a vehicle needs its documents, a party needs its tax id,
1041
+ and a stub minted here is a record that fails the next gate it meets.
1042
+ **Set `searchPlaceholder` whenever `allowCustom` is on**:
1036
1043
  the create row only exists once there is a query, so on an untyped menu the placeholder is the
1037
1044
  only always-visible place that can say a new value is allowed. `customOptionLabel` names what
1038
1045
  the create row will
@@ -1391,9 +1398,22 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1391
1398
  and not interchangeable with it: a ledger has nothing to type into and this has no notion of
1392
1399
  adjustments. A charge is ONE line — `quantity`/`unitPrice` derive the amount and the amount is
1393
1400
  never typed; leave both out and the `amount` itself becomes the editor (`amountActions` puts a
1394
- list price one tap away, `unitPriceActions` a rate). Verbs sit BESIDE the field, never inside
1395
- it, and the row's own verb (`action`, usually removal) sits in a slot held open on every row —
1396
- both so the money column cannot move between rows. `extra` carries a second control belonging
1401
+ list price one tap away, `unitPriceActions` a rate). **A verb is named for the ACT, never for the
1402
+ value it would set** label one with the target figure and every row already at that figure
1403
+ prints the same number twice in a row (three times, once the derived amount agrees), which is the
1404
+ normal state wherever the figures are pre-filled from the same source the verb applies. A fixed
1405
+ label also keeps its width equal across rows, which is what the pass-unconditionally rule exists
1406
+ to protect; put the figure in the accessible name if it is worth announcing. **A money verb renders
1407
+ INSIDE the field it acts on**, on that control's own surface — an `InlineButton` is an inner
1408
+ button and is never a thing standing on its own beside the control it belongs to. As a sibling it
1409
+ also takes its width out of the NAME's column, which is how a charge called "Vệ sinh bến bãi"
1410
+ renders as "Vệ sinh b…". The row's own verb (`action`, usually removal) is the exception that
1411
+ proves it: it belongs to the ROW rather than to any one figure, so it sits in a slot of its own,
1412
+ held open on every row so the money column cannot move between them.
1413
+ **And before reaching for a verb at all, ask whether the value is simply the field's DEFAULT** —
1414
+ a tariff, a list price, the figure a line is created with. Then there is no verb: the field
1415
+ already holds it, and clearing the field restores it. A button that re-applies what the field
1416
+ would hold anyway is a control for a state the reader cannot reach. `extra` carries a second control belonging
1397
1417
  to the charge (how it was paid); `warning` names a problem on the line that has it; `locked`
1398
1418
  turns the editors into values for a settled band and draws no verb of any kind; `empty` speaks for a record nobody has priced
1399
1419
  yet. `formatMoney` is the BAND's, so two lines cannot disagree about a currency, and the `total`
@@ -411,14 +411,19 @@ export function TplMoney() {
411
411
  unitPrice={l.unitPrice}
412
412
  onQuantityChange={(v) => patchLine(l.key, { quantity: v })}
413
413
  onUnitPriceChange={(v) => patchLine(l.key, { unitPrice: v })}
414
- /* the rate card's ceiling, one tap, BESIDE the field it fills */
415
- /* Unconditional + `disabled`, never conditional: a verb passed only when it
414
+ /* The rate card's ceiling, one tap, ON the field it fills — an
415
+ InlineButton is an inner button and never stands on its own beside
416
+ the control it acts on.
417
+ Named for the ACT, with the figure in the accessible name: a verb
418
+ labelled with the value it would set prints that value twice on
419
+ every row already holding it.
420
+ Unconditional + `disabled`, never conditional: a verb passed only when it
416
421
  has work makes the field narrower on the rows that carry it, so those rows
417
422
  stop sharing this money column with the rest. */
418
423
  unitPriceActions={
419
424
  <InlineButton
420
- title={money(CEILING_RATE)}
421
- accessibilityLabel="Apply the ceiling rate"
425
+ title="Ceiling"
426
+ accessibilityLabel={`Apply the ceiling rate of ${money(CEILING_RATE)}`}
422
427
  disabled={l.key !== "runs" || l.unitPrice === CEILING_RATE}
423
428
  onPress={() => patchLine(l.key, { unitPrice: CEILING_RATE })}
424
429
  />
@@ -714,14 +719,9 @@ export function TplMoney() {
714
719
  placeholder="—"
715
720
  accessibilityLabel={`Amount for ${c.label}`}
716
721
  /* The list price, one tap, ON the field it fills —
717
- and this is the fork from `ChargeLines`, which
718
- puts the same verb BESIDE the field. The
719
- discriminator is what the field sits in: here it
720
- is a form row whose value column grows, so a verb
721
- on the field's own surface moves nothing. In a
722
- right-aligned money COLUMN it would eat the
723
- field's width and slide the figure off the
724
- column its neighbours are on. */
722
+ the same placement `ChargeLines` uses, because a
723
+ verb belongs to the control it acts on wherever
724
+ that control sits. */
725
725
  actions={
726
726
  <InlineButton
727
727
  title={money(c.standard)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "46.8.1",
3
+ "version": "46.9.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./accordion": "./src/accordion.tsx",
@@ -246,16 +246,16 @@ export function ChargeLine(props: ChargeLineProps) {
246
246
  placeholder="—"
247
247
  align="right"
248
248
  accessibilityLabel={words.unitPrice(label)}
249
+ /* INSIDE the field, on its own surface — an InlineButton is an inner
250
+ button, never a thing standing on its own beside the control it
251
+ acts on. Rendered as a sibling it also takes its width out of the
252
+ NAME's column, which is how a charge called "Vệ sinh bến bãi" came
253
+ to render as "Vệ sinh b...". */
254
+ actions={unitPriceActions}
249
255
  />
250
256
  )}
251
257
  </View>
252
258
  )}
253
- {/* BESIDE the price field, never inside it: a verb inside eats the field's
254
- width, so the figure slides left and a row carrying a one-tap rate stops
255
- sharing this column with a row that has none. Width is the verb's own —
256
- the contract is that a caller passes it on every row and DISABLES it
257
- where it has nothing to do, which is what keeps the rows equal. */}
258
- {!priced || locked ? null : unitPriceActions}
259
259
  {/* The amount ENDS the expression it derives from. Put it on a second
260
260
  line and the reader parses left to right, then jumps back to find the
261
261
  answer — and it lands in no column, so the total below closes nothing. */}
@@ -273,13 +273,11 @@ export function ChargeLine(props: ChargeLineProps) {
273
273
  placeholder="—"
274
274
  align="right"
275
275
  accessibilityLabel={words.amount(label)}
276
+ /* The flat line's twin, and INSIDE its field for the same reason. */
277
+ actions={amountActions}
276
278
  />
277
279
  )}
278
280
  </View>
279
- {/* The flat line's twin of `unitPriceActions`, on the figure a flat line
280
- actually edits — its amount. Same contract: pass it on every row and
281
- disable it where it has nothing to do. */}
282
- {priced || locked ? null : amountActions}
283
281
  {/* Held open whether or not this row has a verb, so the money column does
284
282
  not shift between a row that can be removed and one that cannot. */}
285
283
  <View style={styles.actionSlot}>{locked ? null : action}</View>
@@ -319,7 +317,13 @@ export function ChargeLine(props: ChargeLineProps) {
319
317
  <View>
320
318
  <View style={styles.row}>
321
319
  <View style={styles.grow}>
322
- <Text size="sm" numberOfLines={1}>
320
+ {/* The name may take a SECOND line rather than lose a word. The fork
321
+ threshold is measured on the band, but what actually runs out is the
322
+ room LEFT for the name — and a money verb takes another slice of it,
323
+ so a band can sit comfortably above the fork width and still have
324
+ under 100px for a charge called "Vệ sinh bến bãi". Capped at two so a
325
+ pathological name cannot push the arithmetic down the page. */}
326
+ <Text size="sm" numberOfLines={2}>
323
327
  {label}
324
328
  </Text>
325
329
  {props.extra ? <View style={styles.extra}>{props.extra}</View> : null}
@@ -45,6 +45,13 @@ interface InlineSelectBaseProps<T extends string, D = unknown> {
45
45
  accessibilityLabel?: string;
46
46
  /** Show an in-menu search box; default false. */
47
47
  searchable?: boolean;
48
+ /** What the list says when it has nothing to offer — no options at all, or a
49
+ * search that matched none. The default states the fact ("No results"); pass
50
+ * this when the caller knows what the reader should DO about it, which the
51
+ * list cannot know: where the missing thing is created, why the set is
52
+ * narrowed. It is the one moment a select may speak without becoming
53
+ * permanent furniture, because it appears only when the reader is stuck. */
54
+ emptyText?: string;
48
55
  /** Offer a "create" row when the query matches no option — picking it commits the
49
56
  * typed value (single) / adds it to the set (multi). Implies `searchable`. */
50
57
  allowCustom?: boolean;
@@ -150,7 +157,7 @@ function InlineSelectShell(props: {
150
157
  }
151
158
 
152
159
  export function InlineSelect<T extends string, D = unknown>(props: InlineSelectProps<T, D>) {
153
- const { options, renderOptionContent, getOptionDescription, placeholder, disabled, accessibilityLabel, searchable = false, allowCustom = false, customOptionLabel, customOptionPlacement, variant, actions, autoFocus = false } = props;
160
+ const { options, renderOptionContent, getOptionDescription, placeholder, disabled, accessibilityLabel, searchable = false, emptyText, allowCustom = false, customOptionLabel, customOptionPlacement, variant, actions, autoFocus = false } = props;
154
161
  const labels = useLoticsLocale().inline;
155
162
  const [open, setOpen] = useState(autoFocus);
156
163
  const [saving, setSaving] = useState(false);
@@ -239,6 +246,8 @@ export function InlineSelect<T extends string, D = unknown>(props: InlineSelectP
239
246
  <OptionList<T, true, D>
240
247
  multi
241
248
  getOptionDescription={getOptionDescription}
249
+ emptyText={emptyText}
250
+
242
251
  search={{ mode: searchMode }}
243
252
  options={options}
244
253
  value={draft}
@@ -286,6 +295,8 @@ export function InlineSelect<T extends string, D = unknown>(props: InlineSelectP
286
295
  >
287
296
  <OptionList<T, false, D>
288
297
  getOptionDescription={getOptionDescription}
298
+ emptyText={emptyText}
299
+
289
300
  search={{ mode: searchMode }}
290
301
  options={options}
291
302
  value={value}