@lotics/ui 43.2.0 → 43.3.1
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/AGENTS.md +4 -9
- package/MIGRATION.md +44 -0
- package/docs/ai_patterns.md +11 -18
- package/docs/catalog.md +20 -28
- package/docs/composition.md +5 -16
- package/docs/data_entry.md +54 -38
- package/docs/templates.md +133 -48
- package/examples/tpl_record.tsx +349 -272
- package/package.json +1 -1
- package/src/inline_edit.tsx +34 -1
- package/src/inline_text_input.tsx +19 -4
- package/src/timeline.tsx +16 -5
package/docs/templates.md
CHANGED
|
@@ -235,15 +235,12 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
|
|
|
235
235
|
ANCHORED to its bar button rather than a full-page `Modal` that would escape the drawer.
|
|
236
236
|
**The discussion follows the same measure**: docked beside the record where there is room for
|
|
237
237
|
its column, and a SECTION under General where there is not — which a drawer always is. It is
|
|
238
|
-
never behind a control.
|
|
239
|
-
body-SWAP inside a drawer; both made the reader know a control existed, press it, and give up
|
|
240
|
-
the record to read a note, which is where an unread note goes to be missed.
|
|
238
|
+
never behind a control.
|
|
241
239
|
- **Linked records** still push an editable screen for that record inside the drawer via the
|
|
242
240
|
hosted `ScreenRouter` — while `canGoBack` the drawer header swaps to a back button + the
|
|
243
241
|
pushed id and the ◀ ▶ sequencer hides; back pops with scroll preserved.
|
|
244
|
-
-
|
|
245
|
-
|
|
246
|
-
drawer's money is the record's now). The communications log lives on the record surface as
|
|
242
|
+
- The drawer does NOT write back to its row — the drawer's money is the record's. The
|
|
243
|
+
communications log lives on the record surface as
|
|
247
244
|
[`tpl_record`](../examples/tpl_record.tsx) § Activity.
|
|
248
245
|
- **The "Enter data" INTAKE FORK** (the New CTA) — AI FIRST, the form as fallback, one phased
|
|
249
246
|
dialog. The whole register page is ALSO a `<FileDropTarget … paste>` (the whole-surface intake
|
|
@@ -338,15 +335,11 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
338
335
|
reader has after what it IS, ahead of its files, its route and its money, which are all
|
|
339
336
|
reference. The desk handoff as the ordered positions it is: a `Checklist connected` where the
|
|
340
337
|
desks are `ChecklistGroup`s (Sales → Operations → Accounting) and the milestones they produce
|
|
341
|
-
are the rows.
|
|
342
|
-
a ring that never responds invites the press it refuses) and no body (a condition hung off a
|
|
343
|
-
heading leaves the reader working out which row it was about). The row that OWES the work
|
|
338
|
+
are the rows. The row that OWES the work
|
|
344
339
|
carries all three: how long it has been owed (`meta`), its condition (`ChecklistNote`, e.g.
|
|
345
340
|
past the due date — with an `action` jumping to the section that owns a missing field rather
|
|
346
341
|
than editing it here) and the single act that leaves the desk (`ChecklistActions` → the handoff
|
|
347
|
-
dialog).
|
|
348
|
-
everything below it without writing dates nobody recorded; a rung it passes shows a BLANK date,
|
|
349
|
-
which is the honest answer to "when" and stays one click from being filled. `meta` is prose
|
|
342
|
+
dialog). `meta` is prose
|
|
350
343
|
the reader can't set, never the date that the stage's own field already shows. See § The
|
|
351
344
|
handoff shape for the rules; for a record that genuinely needs a CHECKLIST, the task-list
|
|
352
345
|
grammar is worked in `tpl_item_list`.
|
|
@@ -461,12 +454,18 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
461
454
|
between us to get it there. It is NOT an audit trail and NOT the handoff — see the next bullet
|
|
462
455
|
for the line, which is what keeps this section from becoming the change log Progress refuses.
|
|
463
456
|
|
|
464
|
-
**The
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
457
|
+
**The GIST is what the feed is scanned by** — what came of the exchange, in the reader's own
|
|
458
|
+
words. Building it the other way round is the natural mistake and it makes the feed unreadable:
|
|
459
|
+
every entry renders as a pair of enum values ("Outbound Meeting"), the substance sits behind a
|
|
460
|
+
chevron, and a column of near-identical labels defeats the scan the feed exists for.
|
|
461
|
+
|
|
462
|
+
**An entry is a COMMENT, not a `Timeline` row.** `Timeline` renders its label inside the row's
|
|
463
|
+
press target, which is right for derived read-only text ("Stage changed to Won") and wrong for
|
|
464
|
+
the one thing on an entry a person WROTE — text inside a button cannot be edited where it sits.
|
|
465
|
+
So the gist is a RESIDENT field and the derived
|
|
466
|
+
facts sit above it on a muted `xs` **byline** — which way it went, over what, when. Order does
|
|
467
|
+
not decide the scan; weight does. The byline is small and grey, the prose is `sm` on the page's
|
|
468
|
+
own ground, and the eye lands on the substance even though it reads second.
|
|
470
469
|
|
|
471
470
|
**ONE anatomy, a body that VARIES.** An entry can be a call, a recorded demo, an email, a
|
|
472
471
|
message or a note typed between meetings — and those differ in what they CARRY, not in what
|
|
@@ -481,25 +480,96 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
481
480
|
|
|
482
481
|
| Block | What it is | How it renders |
|
|
483
482
|
|---|---|---|
|
|
484
|
-
| Participants | ONLY what cannot be derived — a third party who joined, an email's From/To. The record is already the counterparty
|
|
483
|
+
| Participants | ONLY what cannot be derived — a third party who joined, an email's From/To. The record is already the counterparty, so naming the obvious attendees is padding | muted line |
|
|
485
484
|
| Subject | the one thing an email has that nothing else does | `sm` medium |
|
|
486
485
|
| Media | the exchange itself, recorded | `MediaPlayer` — **give it a box**, it fills its parent and collapses to nothing without one |
|
|
487
486
|
| Transcript | VERBATIM and long; the source a summary was made from | a muted underlined `TextLink` in your own `Pressable`, revealing PLAIN text — see the disclosure exception in composition.md §"Commit & feedback surfaces" |
|
|
488
|
-
| Prose | what
|
|
487
|
+
| Prose | what the COUNTERPARTY sent — an email body. Not a second field of ours: an entry offering both a gist and a "note" gives one event two free-text boxes and nothing to tell a writer which to use, and the gist auto-grows, so it already carries whatever length someone wants | `Markdown variant="embedded"` on the page's own ground |
|
|
489
488
|
| AI prose | what a MODEL wrote | ONE short label INSIDE the `Inset` ("AI summary") — floating above it, a 12px muted fragment over a tinted box reads as an orphan. Name the evidence only where it is NOT on screen: over a player the reader can see, "from the recording" is four words restating a video. Let the label replace the block's own `## Heading` rather than sit above it |
|
|
490
|
-
| Files | what came with it |
|
|
489
|
+
| Files | what came with it — EVIDENCE, so it is writable | one `InlineFiles` (`onRemove`, no `onAdd`), mixed pdf/image, previewing in place |
|
|
491
490
|
| Source | where it happened, if openable | `Link` |
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
491
|
+
|
|
492
|
+
**An entry's files are WRITABLE, on both paths.** A touchpoint is very often about a document —
|
|
493
|
+
you sent the quote, they sent the PO back — so a feed that displays `files` but offers no way to
|
|
494
|
+
file one is a read-only surface over a writable model, which reads as a missing feature every
|
|
495
|
+
time. Two places, because there are two moments: the section's `Composer` stages them on the
|
|
496
|
+
entry being logged (`actionsButton` + the `files` slot + `sendDisabled`, the three kit slots the
|
|
497
|
+
comment thread already used), and the entry's own menu attaches to one that already exists.
|
|
498
|
+
|
|
499
|
+
**`sendDisabled` is not optional once a composer can attach.** The default blocks send on empty
|
|
500
|
+
text, which is right for a message and wrong the moment an attachment is the whole point — "here
|
|
501
|
+
is the signed PO" needs no sentence. Pass `false` while files are staged, `undefined` otherwise
|
|
502
|
+
so the default returns.
|
|
503
|
+
|
|
504
|
+
**Attaching lives on the MENU, not on the file list.** The list's own CTA cannot work on an entry
|
|
505
|
+
with nothing attached — there is no list to hang it off — so a menu verb that works in every
|
|
506
|
+
state is the single path, and the list takes `onRemove` alone. Removing CONFIRMS: that file is
|
|
507
|
+
already in the customer's history, unlike a staged attachment on the composer, which is not yet
|
|
508
|
+
anything.
|
|
509
|
+
|
|
510
|
+
**An entry is its own `FileDropTarget`**, nested inside the record-level one. A file dropped on a
|
|
511
|
+
conversation belongs to that conversation; routing it to the record's Documents intake instead
|
|
512
|
+
leaves the reader to relate the two by hand. Nesting is sound rather than lucky — the component
|
|
513
|
+
stops propagation on every drag/drop event, so the innermost region wins, the outer one neither
|
|
514
|
+
fires nor lights, and the file is added once. Pass a plain child so the target paints its own
|
|
515
|
+
drag affordance: with two live regions the drag has to say WHICH one it is about to land in.
|
|
516
|
+
Both paths — the menu verb and the drop — call ONE append function, because two implementations
|
|
517
|
+
of "add to this list" is how one of them quietly becomes a replace. A section earns its own
|
|
518
|
+
target when the drop MEANS something different there; "the same thing, closer" does not.
|
|
519
|
+
|
|
520
|
+
**A MESSAGE's attachments are tiles — staged or filed.** The composer's `files` slot takes
|
|
521
|
+
`<FileGrid files={…} itemSize={THUMBNAIL_SIZE} onDisplayRemove={…}>` — the exact component and
|
|
522
|
+
size the product's own chat composer passes — because a staged file is not yet a document in a
|
|
523
|
+
list: it is a thing you are about to send, checked by LOOKING at it, and the control that
|
|
524
|
+
unstages it belongs on the tile. Getting this backwards is easy and invisible: a row of staged
|
|
525
|
+
attachments looks tidy and quietly makes the composer feel like a file manager.
|
|
526
|
+
|
|
527
|
+
Filing it does not turn it into a row — it turns it into a `FilesEditor`. Tiles either way, the
|
|
528
|
+
same as `chat_user_message` renders a posted message's attachments and the same as this record's
|
|
529
|
+
Files section, because evidence is recognised by LOOKING (a scanned PO, a photo of a seal). The
|
|
530
|
+
rows belong to a different object: `InlineFiles` is a labelled FIELD in a `DetailRow` ("Signed
|
|
531
|
+
contract", "Supplier original"), where the document set has a name, a cardinality and a
|
|
532
|
+
`blockedReason`. Reasoning from that field to a message's attachments is the mistake to avoid —
|
|
533
|
+
it is not "the filed version of the same thing".
|
|
534
|
+
|
|
535
|
+
**A FILED attachment must be hard to lose; a STAGED one is not yet anything.** That is the whole
|
|
536
|
+
difference between the two components. A bare `FileGrid` draws a ✕ on every tile, one click from
|
|
537
|
+
gone — right in a composer, wrong on a record's history. `FilesEditor`'s default view never draws
|
|
538
|
+
one at all: removal means opening the file and doing it from the gallery, where you are looking
|
|
539
|
+
at the thing you are about to delete and it still asks, naming the file. Reach for the editor and
|
|
540
|
+
three things you would otherwise hand-roll come with it — the confirmation, the full-screen
|
|
541
|
+
gallery, and press-to-open.
|
|
542
|
+
|
|
543
|
+
**Give it no children unless a toolbar earns its place.** `FilesEditor` renders its bar from its
|
|
544
|
+
children and nothing without them, and select mode is reachable ONLY from that bar — so omitting
|
|
545
|
+
it removes the mode rather than stranding it. A feed of eight entries does not want eight
|
|
546
|
+
toolbars.
|
|
547
|
+
|
|
548
|
+
**Never write a tile size as a number.** The kit exports the two that exist — `THUMBNAIL_SIZE`
|
|
549
|
+
(a composer attachment, a file tile you are meant to recognise) and `COMPACT_THUMBNAIL_SIZE` (the
|
|
550
|
+
badge on a list row) — and the product uses nothing else. A literal is how a surface ends up at
|
|
551
|
+
56 or 84 for no reason anyone can reconstruct, looking subtly unlike the same surface elsewhere;
|
|
552
|
+
the complaint arrives as "why is it smaller here", which is not a question a constant can raise.
|
|
553
|
+
For a grid that should FILL its container instead, omit `itemSize` and let `minItemWidth` derive
|
|
554
|
+
the columns — that is a different decision from the tile's size, not a smaller version of it.
|
|
555
|
+
|
|
556
|
+
**Copy the component, not the layout mode.** Chat's POSTED attachments use `columns={3}
|
|
557
|
+
partialRowAlign="end"`; the `partialRowAlign` is there because a chat bubble is right-aligned, so
|
|
558
|
+
a short last row hugging the end is what lines up. A comment on a record is a left-aligned block
|
|
559
|
+
and inherits neither. Matching chat means the same component and the same size constant, not
|
|
560
|
+
transplanting a decision that belongs to a bubble.
|
|
561
|
+
|
|
562
|
+
**One list, not the documents/images split.** That split — documents as rows because a name
|
|
563
|
+
identifies them, images as tiles because content does — holds where files are the SUBJECT (a
|
|
564
|
+
record's own Files and Photos sections). An entry's files are a small mixed set of evidence
|
|
565
|
+
identified by what they are, which is the shape the fee ledger's "Supplier original" already
|
|
566
|
+
uses: one `InlineFiles` taking `application/pdf,image/*`.
|
|
567
|
+
|
|
568
|
+
**Delete is the only verb on the entry itself, and it sits in an `ActionMenu` at the TOP RIGHT.** Edit
|
|
569
|
+
stopped existing the moment the gist became a resident field — the fix that removes a control
|
|
570
|
+
beats the one that relocates it — and what remains goes where every comment surface puts it,
|
|
571
|
+
beside the byline rather than in a footer a scroll below the sentence it acts on. The menu is a
|
|
572
|
+
SIBLING of the content, so nothing nests a button inside another.
|
|
503
573
|
|
|
504
574
|
Delete belongs there DESPITE the feed being a record — because of what a multi-writer feed
|
|
505
575
|
actually produces. An extraction filing against the wrong contact and an automation
|
|
@@ -507,15 +577,36 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
507
577
|
garbage it also asks you to trust. Name what leaves with the entry in the confirmation: a
|
|
508
578
|
count somewhere else is the part nobody expects.
|
|
509
579
|
|
|
510
|
-
Only the GIST
|
|
511
|
-
a
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
580
|
+
Only the GIST is editable — everything else is derived, verbatim, an artifact or a model's
|
|
581
|
+
output, and a counterparty's own email body is a record of what THEY said for the same reason a
|
|
582
|
+
transcript is not editable. It is resident, with no mode and no verb, and it follows the value:
|
|
583
|
+
`bare` once written up so it reads as prose, `framed` while empty so it is recognisable as an
|
|
584
|
+
input. Both halves are in data_entry.md §"A value rendered TWICE is the bug".
|
|
585
|
+
|
|
586
|
+
**NO AUTHOR on an entry.** A recording is made by the people in the meeting and merely
|
|
587
|
+
transcribed by a service, so a byline reading "Recording bot" names the plumbing and fabricates
|
|
588
|
+
an author for somebody else's conversation. Once that shape is refused, showing an author on the
|
|
589
|
+
entries that happen to have one is worse than showing none — the reader learns only that we
|
|
590
|
+
attribute when we can, which is a fact about the pipeline. The record has an Owner; a customer's
|
|
591
|
+
history does not restate it once per entry. What a machine actually TOUCHED stays visible where
|
|
592
|
+
it changes how you read something: the `Inset` names an "AI summary", a transcript is labelled
|
|
593
|
+
verbatim. That is attribution of the CONTENT, which is the only kind that was load-bearing.
|
|
594
|
+
|
|
595
|
+
**The blocks SHOW; they are not folded behind a disclosure.** They are what the entry IS, and a
|
|
596
|
+
control that hides them makes the reader click to discover what the entry already says is there
|
|
597
|
+
— on every entry, forever. A video is the
|
|
598
|
+
honest counter-case at 358px and it still shows — it is the most informative thing on a recorded
|
|
599
|
+
call, the feed folds its own tail, and the byline and gist stay at the top of every entry, so the
|
|
600
|
+
scan survives a tall one.
|
|
601
|
+
|
|
602
|
+
**The TRANSCRIPT keeps its disclosure, and it is the only one.** It is the one block that is
|
|
603
|
+
unbounded by nature — a machine's verbatim record of half an hour — and the one nobody reads
|
|
604
|
+
straight through: you open it to check a claim the summary above made. That is what a disclosure
|
|
605
|
+
is for. An email BODY is prose of unknown length too and still shows, because it is human-written,
|
|
606
|
+
ordinarily short, and it IS the message rather than the source behind one.
|
|
516
607
|
|
|
517
608
|
**A block earns its place by carrying what nothing else on the row does.** The failure is not
|
|
518
|
-
a missing block, it is three blocks saying one thing: a participants line, a rep's
|
|
609
|
+
a missing block, it is three blocks saying one thing: a participants line, a rep's gist and a
|
|
519
610
|
machine summary all reporting that the spreadsheet owner was absent. When the summary is good
|
|
520
611
|
the rep usually adds nothing, and an entry with no note is the normal case rather than an
|
|
521
612
|
incomplete one. Cut on sight: a label above a block that already labels itself, a fact the
|
|
@@ -536,12 +627,8 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
536
627
|
compare. Reveal it whole rather than into a scroll box; a scroller inside a scrolling drawer
|
|
537
628
|
traps the wheel, and a reader who pressed it asked for the length.
|
|
538
629
|
|
|
539
|
-
**The toggle is underlined text, and that is a deliberate exception
|
|
540
|
-
|
|
541
|
-
wash arrives only on hover, so the affordance is hover-only (banned in §"Commit & feedback surfaces") and it
|
|
542
|
-
reads as a stray label between two content blocks. `Accordion` fixes the affordance and
|
|
543
|
-
overcorrects: it is a LIST-ROW primitive, a full-width header with its own chevron, nested
|
|
544
|
-
inside a row that already has one. What is left is a muted underlined `TextLink` in your own
|
|
630
|
+
**The toggle is underlined text, and that is a deliberate exception** (the in-prose disclosure,
|
|
631
|
+
composition.md §"Commit & feedback surfaces"): a muted underlined `TextLink` in your own
|
|
545
632
|
`Pressable` — visible at rest, on the column's own left edge, and carrying no more weight than
|
|
546
633
|
the words. The ink keeps underline honest: blue leaves the surface, muted stays on it.
|
|
547
634
|
|
|
@@ -558,9 +645,7 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
558
645
|
few with a toggle naming how many are behind it — a register pages, a record's feed folds.
|
|
559
646
|
|
|
560
647
|
The toggle is a `TextDisclosure`, the same control as the transcript's, because it is the same
|
|
561
|
-
act: it reveals more of what is already here.
|
|
562
|
-
right and is not — measured, it is transparent, borderless and undecorated at rest, so the
|
|
563
|
-
affordance only arrives on hover and it reads as a stray line under the feed. It sits on the
|
|
648
|
+
act: it reveals more of what is already here. It sits on the
|
|
564
649
|
SECTION's left edge, not the rows': the rows are inset by the disc rail, and at the fold's line
|
|
565
650
|
the rail has ended, so nothing visible occupies that indent. Same for any other fold on the
|
|
566
651
|
surface — a document set's "show the rest" is this control too.
|