@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/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. It was briefly a toggle in the top-right of a narrow page and a
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
- - One thing this template no longer demonstrates, so look elsewhere: the drawer does NOT
245
- write back to its row (settling a ledger used to flip the register's paid state live — the
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. A GROUP carries no COMPLETION control (a phase owns none, and
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). Rows tick ONE at a time and position is the HIGHEST stamp, so one click marks
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 row's LABEL is the GIST** — what came of the exchange, in the reader's own words. The
465
- medium and the direction are metadata on the muted supporting line beneath it. Building it the
466
- other way round is the natural mistake and it makes the feed unreadable: every row renders as a
467
- pair of enum values ("Outbound Meeting"), the substance sits behind a chevron, and a column of
468
- near-identical labels defeats the scan the feed exists for. Measured on a real one, that shape
469
- spent 504px of chrome on 70px of content.
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 and the row already says who logged it, so naming the obvious attendees is padding | muted line |
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 a PERSON wrote — a note, a counterparty's email | `Markdown variant="embedded"` on the page's own ground |
487
+ | Prose | what the COUNTERPARTY sentan 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 | documents `FileRow`s (a name identifies them); images `FileThumbnailGrid` (content does) |
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
- | Author | who filed it | `MemberChip` for a person; `WaveAvatar` + name for an automation |
493
-
494
- **The verbs Edit and Delete live at the BOTTOM of the open body, not in the row's trailing
495
- slot.** Three defects came out of putting a control on the label row (a button nested in the
496
- row's own button, a fixed-height box to hold it on the first line, and a gap close enough to
497
- the chevron to read as one cluster) and every one of them exists only because something
498
- interactive shared the row. In the body there is nothing to share with, the verbs sit beside
499
- the content they act on, and the expansion the reader already chose IS the look-before-you-
500
- delete step which makes "two clicks" the feature rather than the cost. Provenance sits left
501
- on that footer, the verbs right; they are two groups, so space pushes them apart rather than a
502
- uniform gap making them peers.
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 and the note are editable — everything else is derived, verbatim, an artifact or
511
- a model's output. The editor is a MODE behind an **Edit** verb rather than a resident field,
512
- because the gist is already the row's label and a resident one renders it twice; an EMPTY gist
513
- shows its field regardless, because that is the write-up path for an entry an automation filed.
514
- The rule and its two corollaries are in data_entry.md §"When the editor is a MODE instead of a
515
- resident field".
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 note and a
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.** Three controls were
540
- tried. `Button color="muted"` measures transparent, borderless and undecorated at rest the
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. A `Button color="muted"` is the reach that feels
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.