@lotics/ui 43.2.0 → 43.3.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/MIGRATION.md CHANGED
@@ -4,6 +4,37 @@ Breaking changes, newest first — normally per major, plus the rare minor that
4
4
  anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
5
5
  this file exists only to move an app from one release to the next.
6
6
 
7
+ ## 43.3.0 — a `bare` inline field sits on the column, and Enter finishes a one-line one
8
+
9
+ Two fixes to `Inline*`, both of them behaviour a caller cannot see in a prop name.
10
+
11
+ **A `bare` field now aligns its TEXT, not its box.** The field surface carries 8px
12
+ of horizontal padding, which is invisible at rest on a variant that draws no
13
+ border — so every bare value rendered 8px right of whatever named it. In a
14
+ `DataGrid` that meant each column's values sat off its own header. The inset is
15
+ now bled back out with a negative LEFT margin: glyphs land on the container's
16
+ edge, the hover/focus border overhangs into the gutter, and hover moves nothing.
17
+
18
+ Left only, deliberately. A matching right bleed aligns nothing on left-aligned
19
+ text and pushes the frame past the edge its neighbours stop at, which shows the
20
+ moment the border paints. **If you compensated with your own negative margin,
21
+ remove it** — you will now be 8px out the other way. A field needs ~8px of slack
22
+ on its left to hover into; in a zero-gap column, adjacent borders will touch. A
23
+ DISABLED `framed` field is unaffected: it drops its border and fill so it cannot
24
+ promise a press, but it stays in line with the enabled fields beside it.
25
+
26
+ **`numberOfLines` now decides what ENTER does — wrapping no longer does.** The two
27
+ were one flag, so `autoGrow` — a prop about fitting the value — silently turned
28
+ Enter from "save" into "insert a newline". A one-line summary field that grew when
29
+ it ran long became a field you could only leave by clicking away, and nothing at
30
+ the call site said so.
31
+
32
+ A field left at one line (or `numberOfLines={1}`) holds a single-line value that
33
+ may WRAP, and Enter commits it. Declaring 2+ lines declares PROSE: Enter is a
34
+ paragraph break and blur saves, exactly as before. **If you wrote
35
+ `numberOfLines={1} autoGrow` and relied on Enter inserting a newline, raise the
36
+ line budget** — that is now what says so.
37
+
7
38
  ## 43.0.0 — a time is PICKED, and there is nothing to type
8
39
 
9
40
  `TimePicker` is no longer a segmented text field. It is the value on a pressable
@@ -78,6 +109,19 @@ its reasoning are in `docs/composition.md` § The pointer cursor. If a component
78
109
  needs the pointer, set `CURSOR_ACTION` in its style — and if it is built on the bare
79
110
  react-native `Pressable`, note that the pointer is what you get by DEFAULT, from
80
111
  react-native-web, unless you set `CURSOR_DEFAULT`.
112
+ ## 42.5.0 — an expanded `Timeline` row shows its whole label
113
+
114
+ The label was clamped to two lines whether the row was open or shut. Expanding a
115
+ row is a request to see more of what is there, so a label that stays truncated
116
+ through it leaves the caller exactly one way to make its own label readable:
117
+ repeat it inside `details`. Then the reader meets the same sentence twice — once
118
+ cut off, once whole — which reads as a rendering bug rather than as disclosure.
119
+
120
+ It now clamps while collapsed only. **If you duplicated a label into `details`
121
+ to work around this, delete the copy**; the row will show it in full on its own.
122
+ Nothing else changes: a collapsed row is identical, and a row whose label already
123
+ fitted in two lines is identical open or shut.
124
+
81
125
  ## 42.4.0 — `Picker` is named by its `FormField`, and its way back to empty has a name
82
126
 
83
127
  Two fixes to the same control, both about a thing on screen that the control did
package/docs/catalog.md CHANGED
@@ -1487,9 +1487,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
1487
1487
  provider-wired — pass `labels` (`DEFAULT_GANTT_LABELS` is English).
1488
1488
  - **`timeline`** — `Timeline`: a heterogeneous event LOG — per-row icon + expandable
1489
1489
  details; models the past, NOT progress. Three things it decides for you, because a row
1490
- cannot be trusted to a caller's data: the **label clamps to two lines** (and the
1491
- description likewise) — a row is a scan target, and an unclamped one lets a single
1492
- user-authored note set the section's height, so put the full text in `details` too;
1490
+ cannot be trusted to a caller's data: the **label clamps to two lines WHILE COLLAPSED**
1491
+ (the description always) — a row is a scan target, and an unclamped one lets a single
1492
+ user-authored note set the section's height. Opening the row un-clamps it, so **never
1493
+ repeat the label inside `details`**: the fact belongs to one element, and a copy means
1494
+ the reader meets the same sentence twice, cut off and then whole. `details` carries
1495
+ what the label does not — the attachments, the machine's reading, the provenance;
1493
1496
  **`placeholder`** marks a label the SURFACE supplied rather than one the record
1494
1497
  holds, drawing it in the unfilled-field ink — reach for it on any feed that fills
1495
1498
  from more than one direction, where an entry routinely arrives with no words in it;
@@ -84,6 +84,14 @@ layout), click OR keyboard focus opens it for editing **with nothing moving** (z
84
84
  whole point), and it commits on blur (Enter saves, Escape reverts) or via `controls="buttons"`
85
85
  (✓ / ✕).
86
86
 
87
+ **`numberOfLines` declares what ENTER means — wrapping does not.** A field left at one line holds a
88
+ single-line value that may WRAP when it runs long (a title, a summary), and Enter finishes it.
89
+ Declaring 2+ lines declares PROSE: Enter is a paragraph break and blur is what saves. The two used
90
+ to be one flag, so `autoGrow` — a prop about fitting the value — silently turned Enter into a
91
+ newline, and a one-line summary field that grew became a field you could only leave by clicking
92
+ away. Nothing at the call site said so, which is what made it worth separating rather than
93
+ documenting.
94
+
87
95
  **How "nothing moves" is achieved differs by editor, and the difference is a rule.** A field whose
88
96
  resting value is the RAW string (`InlineTextInput`) is ONE `<input>` in both states — it never
89
97
  swaps elements, and `editing` is a style state driven by focus. Two elements cannot be made to
@@ -277,6 +285,17 @@ already promises and, repeated down a column, draws the grid a second time. Both
277
285
  identically, so this changes what a field looks like RESTING, never what it does. A hand-rolled
278
286
  pressable cell should match `bare`: nothing at rest, the border on hover — never a background wash.
279
287
 
288
+ **A `bare` field aligns its TEXT, not its box.** Because it looks like text at rest, it sits on the
289
+ column like text: the frame's 8px inset is pulled back out with a negative LEFT margin, so the glyphs
290
+ land on the container's edge. Without that, every bare value rendered 8px right of the header naming
291
+ it. The bleed is one-sided on purpose — a matching right bleed aligns nothing on left-aligned text and
292
+ only pushes the frame past the edge its neighbours stop at, which shows the moment the border paints.
293
+ Two consequences for callers: a `bare` field needs ~8px of slack on its LEFT to hover into (in a
294
+ zero-gap column the borders of adjacent cells will touch), and it is the wrong variant for a surface
295
+ where it must stay strictly inside its box — that is `framed`. A DISABLED `framed` field keeps the
296
+ box: it drops the border and fill so it cannot promise a press, but it stays in line with the enabled
297
+ fields beside it.
298
+
280
299
  A READ-ONLY value in the same column — a computed total, a system ID, a synced/locked field — is
281
300
  **`InlineStatic`**: it copies the editor box metrics exactly (height, padding, 1px transparent
282
301
  border) but stays FLAT and non-interactive, so editable (chip) vs read-only (flat) is legible at a
@@ -331,33 +350,46 @@ row that wants it. See the "General" section of `tpl_record`, which also reads t
331
350
  full record surface. Not every field is a same-height swap — a tag field, a status, or an
332
351
  attachment grid edit in place too (below).
333
352
 
334
- ## When the editor is a MODE instead of a resident field
353
+ ## A value rendered TWICE is the bug not the resident editor
354
+
355
+ The section above is the default and holds everywhere. There is one thing that appears to
356
+ contradict it, and the contradiction is worth naming because the wrong resolution is the
357
+ tempting one: **a value that already appears elsewhere on the same surface.**
335
358
 
336
- The section above is the default and holds almost everywhere: the value IS the editor, nothing
337
- moves, and there is no verb to find. It has exactly one exception, and it is decided by a
338
- question with a factual answer **does this value already appear somewhere else on the same
339
- surface?**
359
+ A feed used to be the example. Its row LABEL was the gist, the body opened underneath, and a
360
+ resident editor rendered the same sentence twice stacked and identical, which reads as a
361
+ mistake at any weight, so quieting the frame does not help. The fix looked like demoting the
362
+ editor to a mode behind an **Edit** verb, and that is a workaround: it keeps the duplication and
363
+ buys silence by hiding one copy behind a click. What it costs shows up immediately after —
364
+ the verb has to live somewhere (a button nested in the row's own button, or a footer a scroll
365
+ away from the sentence it edits), and every repair from there is a repair to the arrangement,
366
+ not to the surface.
340
367
 
341
- On a FEED, it does. A row's identity line is the value, and the body opens under it, so a
342
- resident editor renders the same sentence twice, stacked and identical. Quieting the frame does
343
- not help: a sentence appearing twice reads as a mistake at any weight. So the editor becomes a
344
- mode, reached by an **Edit** verb in the row's trailing slot beside its other actions.
368
+ **Render it once, and the question dissolves.** Text inside a press target cannot be edited where
369
+ it sits, so a value a person WROTE does not belong in a row's label: the label is for derived,
370
+ read-only text ("Stage changed to Won"). Move the prose out and it is an ordinary resident field
371
+ with no mode, no verb, and one copy `tpl_record`'s activity feed is the worked example, and the
372
+ byline above it carries the derived facts that used to fight it for the row.
345
373
 
346
- **Except when the value is EMPTY, and that half matters more.** There is nothing to duplicate,
347
- and a surface fed by automations and extractions receives entries with no words in them
348
- routinely — a recording lands, the row says it has not been written up, and the empty field is
349
- the invitation to write it. Withhold it there and the feed can RECEIVE an entry it gives the
350
- reader no way to finish. **An empty value shows its field; a filled one waits to be asked.**
374
+ So: if a resident editor would duplicate a value, **delete the duplicate**. Make the editor a mode
375
+ only when the second appearance is genuinely load-bearing and cannot move.
351
376
 
352
377
  Two things follow that are easy to get wrong in the other direction:
353
378
 
354
- - **Name the verb for the words, not the row.** "Edit", not "Edit entry" — on a feed the entry
355
- is a record of something that happened, and only the prose a PERSON wrote is editable at all.
356
- Derived values (a header's participants), verbatim ones (a transcript), artifacts (media,
357
- files) and a model's output (a summary you re-run it, you do not hand-edit it) are all
358
- read-only, and a verb naming the whole entry claims otherwise.
359
- - **One row in edit mode at a time.** Two half-finished sentences on one feed leave the reader
360
- unable to say which one the next keystroke lands in.
379
+ - **Only the prose a PERSON wrote is editable.** Derived values (a header's participants),
380
+ verbatim ones (a transcript), artifacts (media, files) and a model's output (a summary you
381
+ re-run it, you do not hand-edit it) are all read-only. If a surface does end up with an edit
382
+ verb, name it for the words rather than the row: "Edit", not "Edit entry", which claims the
383
+ whole entry is yours to rewrite.
384
+ - **An EMPTY value still shows its field, and shows it FRAMED.** A surface fed by automations
385
+ receives entries with no words in them routinely — a recording lands and nobody has written it
386
+ up — so withholding the field lets a feed RECEIVE an entry it gives the reader no way to
387
+ finish. Empty is also the state where `bare` fails: grey placeholder text on an invisible box
388
+ does not read as somewhere you can type. Follow the value — `variant={value ? "bare" : "framed"}`
389
+ — so a written-up entry reads as prose and an empty one is recognisable as an input from across
390
+ the page. A button that reveals a field is the wrong trade: `InlineTextInput` has no
391
+ `autoFocus` (permanently mounted, every field on a record would fight for focus on load), so
392
+ the revealed field cannot take the caret and the reader pays a second click for less clarity.
361
393
 
362
394
  ## Fieldset form — fields lay out on a RESPONSIVE two-column grid
363
395
 
package/docs/templates.md CHANGED
@@ -461,12 +461,21 @@ billing, and quick-capture templates. Top → bottom:
461
461
  between us to get it there. It is NOT an audit trail and NOT the handoff — see the next bullet
462
462
  for the line, which is what keeps this section from becoming the change log Progress refuses.
463
463
 
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.
464
+ **The GIST is what the feed is scanned by** — what came of the exchange, in the reader's own
465
+ words. Building it the other way round is the natural mistake and it makes the feed unreadable:
466
+ every entry renders as a pair of enum values ("Outbound Meeting"), the substance sits behind a
467
+ chevron, and a column of near-identical labels defeats the scan the feed exists for. Measured on
468
+ a real one, that shape spent 504px of chrome on 70px of content.
469
+
470
+ **An entry is a COMMENT, not a `Timeline` row.** `Timeline` renders its label inside the row's
471
+ press target, which is right for derived read-only text ("Stage changed to Won") and wrong for
472
+ the one thing on an entry a person WROTE — text inside a button cannot be edited where it sits.
473
+ Four repairs came out of working around that (the verbs moved off the row, the label learned to
474
+ un-clamp, the body's second copy of the gist came out, and Edit ended up a scroll from the
475
+ sentence it edits) and none were independent. So the gist is a RESIDENT field and the derived
476
+ facts sit above it on a muted `xs` **byline** — which way it went, over what, when. Order does
477
+ not decide the scan; weight does. The byline is small and grey, the prose is `sm` on the page's
478
+ own ground, and the eye lands on the substance even though it reads second.
470
479
 
471
480
  **ONE anatomy, a body that VARIES.** An entry can be a call, a recorded demo, an email, a
472
481
  message or a note typed between meetings — and those differ in what they CARRY, not in what
@@ -481,25 +490,96 @@ billing, and quick-capture templates. Top → bottom:
481
490
 
482
491
  | Block | What it is | How it renders |
483
492
  |---|---|---|
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 |
493
+ | 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
494
  | Subject | the one thing an email has that nothing else does | `sm` medium |
486
495
  | Media | the exchange itself, recorded | `MediaPlayer` — **give it a box**, it fills its parent and collapses to nothing without one |
487
496
  | 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 |
497
+ | 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
498
  | 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) |
499
+ | Files | what came with it — EVIDENCE, so it is writable | one `InlineFiles` (`onRemove`, no `onAdd`), mixed pdf/image, previewing in place |
491
500
  | 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.
501
+
502
+ **An entry's files are WRITABLE, on both paths.** A touchpoint is very often about a document —
503
+ you sent the quote, they sent the PO back so a feed that displays `files` but offers no way to
504
+ file one is a read-only surface over a writable model, which reads as a missing feature every
505
+ time. Two places, because there are two moments: the section's `Composer` stages them on the
506
+ entry being logged (`actionsButton` + the `files` slot + `sendDisabled`, the three kit slots the
507
+ comment thread already used), and the entry's own menu attaches to one that already exists.
508
+
509
+ **`sendDisabled` is not optional once a composer can attach.** The default blocks send on empty
510
+ text, which is right for a message and wrong the moment an attachment is the whole point — "here
511
+ is the signed PO" needs no sentence. Pass `false` while files are staged, `undefined` otherwise
512
+ so the default returns.
513
+
514
+ **Attaching lives on the MENU, not on the file list.** The list's own CTA cannot work on an entry
515
+ with nothing attached — there is no list to hang it off — so a menu verb that works in every
516
+ state is the single path, and the list takes `onRemove` alone. Removing CONFIRMS: that file is
517
+ already in the customer's history, unlike a staged attachment on the composer, which is not yet
518
+ anything.
519
+
520
+ **An entry is its own `FileDropTarget`**, nested inside the record-level one. A file dropped on a
521
+ conversation belongs to that conversation; routing it to the record's Documents intake instead
522
+ leaves the reader to relate the two by hand. Nesting is sound rather than lucky — the component
523
+ stops propagation on every drag/drop event, so the innermost region wins, the outer one neither
524
+ fires nor lights, and the file is added once. Pass a plain child so the target paints its own
525
+ drag affordance: with two live regions the drag has to say WHICH one it is about to land in.
526
+ Both paths — the menu verb and the drop — call ONE append function, because two implementations
527
+ of "add to this list" is how one of them quietly becomes a replace. A section earns its own
528
+ target when the drop MEANS something different there; "the same thing, closer" does not.
529
+
530
+ **A MESSAGE's attachments are tiles — staged or filed.** The composer's `files` slot takes
531
+ `<FileGrid files={…} itemSize={THUMBNAIL_SIZE} onDisplayRemove={…}>` — the exact component and
532
+ size the product's own chat composer passes — because a staged file is not yet a document in a
533
+ list: it is a thing you are about to send, checked by LOOKING at it, and the control that
534
+ unstages it belongs on the tile. Getting this backwards is easy and invisible: a row of staged
535
+ attachments looks tidy and quietly makes the composer feel like a file manager.
536
+
537
+ Filing it does not turn it into a row — it turns it into a `FilesEditor`. Tiles either way, the
538
+ same as `chat_user_message` renders a posted message's attachments and the same as this record's
539
+ Files section, because evidence is recognised by LOOKING (a scanned PO, a photo of a seal). The
540
+ rows belong to a different object: `InlineFiles` is a labelled FIELD in a `DetailRow` ("Signed
541
+ contract", "Supplier original"), where the document set has a name, a cardinality and a
542
+ `blockedReason`. Reasoning from that field to a message's attachments is the mistake to avoid —
543
+ it is not "the filed version of the same thing".
544
+
545
+ **A FILED attachment must be hard to lose; a STAGED one is not yet anything.** That is the whole
546
+ difference between the two components. A bare `FileGrid` draws a ✕ on every tile, one click from
547
+ gone — right in a composer, wrong on a record's history. `FilesEditor`'s default view never draws
548
+ one at all: removal means opening the file and doing it from the gallery, where you are looking
549
+ at the thing you are about to delete and it still asks, naming the file. Reach for the editor and
550
+ three things you would otherwise hand-roll come with it — the confirmation, the full-screen
551
+ gallery, and press-to-open.
552
+
553
+ **Give it no children unless a toolbar earns its place.** `FilesEditor` renders its bar from its
554
+ children and nothing without them, and select mode is reachable ONLY from that bar — so omitting
555
+ it removes the mode rather than stranding it. A feed of eight entries does not want eight
556
+ toolbars.
557
+
558
+ **Never write a tile size as a number.** The kit exports the two that exist — `THUMBNAIL_SIZE`
559
+ (a composer attachment, a file tile you are meant to recognise) and `COMPACT_THUMBNAIL_SIZE` (the
560
+ badge on a list row) — and the product uses nothing else. A literal is how a surface ends up at
561
+ 56 or 84 for no reason anyone can reconstruct, looking subtly unlike the same surface elsewhere;
562
+ the complaint arrives as "why is it smaller here", which is not a question a constant can raise.
563
+ For a grid that should FILL its container instead, omit `itemSize` and let `minItemWidth` derive
564
+ the columns — that is a different decision from the tile's size, not a smaller version of it.
565
+
566
+ **Copy the component, not the layout mode.** Chat's POSTED attachments use `columns={3}
567
+ partialRowAlign="end"`; the `partialRowAlign` is there because a chat bubble is right-aligned, so
568
+ a short last row hugging the end is what lines up. A comment on a record is a left-aligned block
569
+ and inherits neither. Matching chat means the same component and the same size constant, not
570
+ transplanting a decision that belongs to a bubble.
571
+
572
+ **One list, not the documents/images split.** That split — documents as rows because a name
573
+ identifies them, images as tiles because content does — holds where files are the SUBJECT (a
574
+ record's own Files and Photos sections). An entry's files are a small mixed set of evidence
575
+ identified by what they are, which is the shape the fee ledger's "Supplier original" already
576
+ uses: one `InlineFiles` taking `application/pdf,image/*`.
577
+
578
+ **Delete is the only verb on the entry itself, and it sits in an `ActionMenu` at the TOP RIGHT.** Edit
579
+ stopped existing the moment the gist became a resident field — the fix that removes a control
580
+ beats the one that relocates it — and what remains goes where every comment surface puts it,
581
+ beside the byline rather than in a footer a scroll below the sentence it acts on. The menu is a
582
+ SIBLING of the content, so nothing nests a button inside another.
503
583
 
504
584
  Delete belongs there DESPITE the feed being a record — because of what a multi-writer feed
505
585
  actually produces. An extraction filing against the wrong contact and an automation
@@ -507,15 +587,38 @@ billing, and quick-capture templates. Top → bottom:
507
587
  garbage it also asks you to trust. Name what leaves with the entry in the confirmation: a
508
588
  count somewhere else is the part nobody expects.
509
589
 
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".
590
+ Only the GIST is editable — everything else is derived, verbatim, an artifact or a model's
591
+ output, and a counterparty's own email body is a record of what THEY said for the same reason a
592
+ transcript is not editable. It is resident, with no mode and no verb, and it follows the value:
593
+ `bare` once written up so it reads as prose, `framed` while empty so it is recognisable as an
594
+ input. Both halves are in data_entry.md §"A value rendered TWICE is the bug".
595
+
596
+ **NO AUTHOR on an entry.** A recording is made by the people in the meeting and merely
597
+ transcribed by a service, so a byline reading "Recording bot" names the plumbing and fabricates
598
+ an author for somebody else's conversation. Once that shape is refused, showing an author on the
599
+ entries that happen to have one is worse than showing none — the reader learns only that we
600
+ attribute when we can, which is a fact about the pipeline. The record has an Owner; a customer's
601
+ history does not restate it once per entry. What a machine actually TOUCHED stays visible where
602
+ it changes how you read something: the `Inset` names an "AI summary", a transcript is labelled
603
+ verbatim. That is attribution of the CONTENT, which is the only kind that was load-bearing.
604
+
605
+ **The blocks SHOW; they are not folded behind a disclosure.** They are what the entry IS, and a
606
+ control that hides them makes the reader click to discover what the entry already says is there
607
+ — on every entry, forever. The fold that used to sit here was defended as "each is tall enough to
608
+ bury the next entry", which measurement did not support: an audio player is 54px and a file tile
609
+ row is 96, so hiding one behind a 20px disclosure buys 34px and costs a click. A video is the
610
+ honest counter-case at 358px and it still shows — it is the most informative thing on a recorded
611
+ call, the feed folds its own tail, and the byline and gist stay at the top of every entry, so the
612
+ scan survives a tall one.
613
+
614
+ **The TRANSCRIPT keeps its disclosure, and it is the only one.** It is the one block that is
615
+ unbounded by nature — a machine's verbatim record of half an hour — and the one nobody reads
616
+ straight through: you open it to check a claim the summary above made. That is what a disclosure
617
+ is for. An email BODY is prose of unknown length too and still shows, because it is human-written,
618
+ ordinarily short, and it IS the message rather than the source behind one.
516
619
 
517
620
  **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
621
+ a missing block, it is three blocks saying one thing: a participants line, a rep's gist and a
519
622
  machine summary all reporting that the spreadsheet owner was absent. When the summary is good
520
623
  the rep usually adds nothing, and an entry with no note is the normal case rather than an
521
624
  incomplete one. Cut on sight: a label above a block that already labels itself, a fact the