@lotics/ui 41.0.1 → 41.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/AGENTS.md +4 -4
- package/MIGRATION.md +106 -0
- package/docs/ai_patterns.md +36 -0
- package/docs/catalog.md +70 -12
- package/docs/composition.md +89 -11
- package/docs/data_entry.md +107 -1
- package/docs/templates.md +120 -9
- package/examples/tpl_item_list.tsx +21 -0
- package/examples/tpl_record.tsx +1006 -45
- package/package.json +4 -1
- package/src/file_rows.tsx +13 -3
- package/src/inline_files.tsx +219 -0
- package/src/inline_text_input.tsx +33 -2
- package/src/markdown.css +43 -0
- package/src/markdown.tsx +7 -1
- package/src/markdown.web.tsx +4 -2
- package/src/markdown_types.ts +21 -0
- package/src/member_chip.tsx +12 -2
- package/src/text_disclosure.tsx +70 -0
- package/src/text_link.tsx +13 -3
- package/src/timeline.tsx +194 -34
package/docs/templates.md
CHANGED
|
@@ -241,11 +241,10 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
|
|
|
241
241
|
- **Linked records** still push an editable screen for that record inside the drawer via the
|
|
242
242
|
hosted `ScreenRouter` — while `canGoBack` the drawer header swaps to a back button + the
|
|
243
243
|
pushed id and the ◀ ▶ sequencer hides; back pops with scroll preserved.
|
|
244
|
-
-
|
|
244
|
+
- One thing this template no longer demonstrates, so look elsewhere: the drawer does NOT
|
|
245
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)
|
|
247
|
-
|
|
248
|
-
the record surface has no counterpart for.
|
|
246
|
+
drawer's money is the record's now). The communications log lives on the record surface as
|
|
247
|
+
[`tpl_record`](../examples/tpl_record.tsx) § Activity.
|
|
249
248
|
- **The "Enter data" INTAKE FORK** (the New CTA) — AI FIRST, the form as fallback, one phased
|
|
250
249
|
dialog. The whole register page is ALSO a `<FileDropTarget … paste>` (the whole-surface intake
|
|
251
250
|
standard): a file dropped or Ctrl/Cmd+V pasted ANYWHERE on the register opens this same dialog
|
|
@@ -457,11 +456,123 @@ billing, and quick-capture templates. Top → bottom:
|
|
|
457
456
|
a label-left capture form (auto-stamped date/receipt-no, record-prefilled fields, the
|
|
458
457
|
right input each) whose CTA and produced `FileRow` ride an empty-label `DetailRow` — the
|
|
459
458
|
form-action alignment law (data_entry.md), worked.
|
|
460
|
-
- **
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
459
|
+
- **Activity — what was SAID with the other party**, newest first, directly under Progress: the
|
|
460
|
+
two answer a reader's first two questions in order, where the record stands and what passed
|
|
461
|
+
between us to get it there. It is NOT an audit trail and NOT the handoff — see the next bullet
|
|
462
|
+
for the line, which is what keeps this section from becoming the change log Progress refuses.
|
|
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.
|
|
470
|
+
|
|
471
|
+
**ONE anatomy, a body that VARIES.** An entry can be a call, a recorded demo, an email, a
|
|
472
|
+
message or a note typed between meetings — and those differ in what they CARRY, not in what
|
|
473
|
+
they are: all of them answer what-came-of-it, which way, over what, and when. So the four
|
|
474
|
+
invariants are the row, and the body is a SET OF BLOCKS rather than a shape per kind. A shape
|
|
475
|
+
per kind writes the same block five times and lets the copies drift, and the sixth kind — the
|
|
476
|
+
one nobody has thought of — needs a sixth. **Every block is optional and any combination is
|
|
477
|
+
legal**, because what an entry carries is independent of what it is: a call may arrive as a
|
|
478
|
+
recording alone, gain a transcript minutes later and a summary after that.
|
|
479
|
+
|
|
480
|
+
The blocks, in the order they render (`ActivityBody` in `tpl_record` is the worked one):
|
|
481
|
+
|
|
482
|
+
| Block | What it is | How it renders |
|
|
483
|
+
|---|---|---|
|
|
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 |
|
|
485
|
+
| Subject | the one thing an email has that nothing else does | `sm` medium |
|
|
486
|
+
| Media | the exchange itself, recorded | `MediaPlayer` — **give it a box**, it fills its parent and collapses to nothing without one |
|
|
487
|
+
| 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 |
|
|
489
|
+
| 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) |
|
|
491
|
+
| 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.
|
|
503
|
+
|
|
504
|
+
Delete belongs there DESPITE the feed being a record — because of what a multi-writer feed
|
|
505
|
+
actually produces. An extraction filing against the wrong contact and an automation
|
|
506
|
+
double-filing a recording are not history, and a feed with no way to remove them accumulates
|
|
507
|
+
garbage it also asks you to trust. Name what leaves with the entry in the confirmation: a
|
|
508
|
+
count somewhere else is the part nobody expects.
|
|
509
|
+
|
|
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".
|
|
516
|
+
|
|
517
|
+
**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
|
|
519
|
+
machine summary all reporting that the spreadsheet owner was absent. When the summary is good
|
|
520
|
+
the rep usually adds nothing, and an entry with no note is the normal case rather than an
|
|
521
|
+
incomplete one. Cut on sight: a label above a block that already labels itself, a fact the
|
|
522
|
+
record or the row already states, and any block whose content the reader can see rendered
|
|
523
|
+
directly above it.
|
|
524
|
+
|
|
525
|
+
**Two of those rows are the whole point of the section on a modern record.** A feed fills from
|
|
526
|
+
more than one direction — a person, a chat agent, an extraction off an upload, an automation
|
|
527
|
+
firing on a file — so the surface has to say which values nobody wrote and which a machine did.
|
|
528
|
+
An entry with no prose renders `placeholder` (see `Timeline`), never a phrase assembled from
|
|
529
|
+
the enums; and a machine's text is separated from a person's by SHAPE — an attribution line
|
|
530
|
+
plus a recessed ground — never by a weight nudge. The reasoning and the failure modes are in
|
|
531
|
+
ai_patterns.md §After the run.
|
|
532
|
+
|
|
533
|
+
**The transcript folds, and it reveals in place.** It is the source a summary was made from,
|
|
534
|
+
read rarely and in full when at all — and the reason anyone opens it is to check a claim the
|
|
535
|
+
summary made, so a dialog is wrong: it takes the claim off screen at the moment they want to
|
|
536
|
+
compare. Reveal it whole rather than into a scroll box; a scroller inside a scrolling drawer
|
|
537
|
+
traps the wheel, and a reader who pressed it asked for the length.
|
|
538
|
+
|
|
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
|
|
545
|
+
`Pressable` — visible at rest, on the column's own left edge, and carrying no more weight than
|
|
546
|
+
the words. The ink keeps underline honest: blue leaves the surface, muted stays on it.
|
|
547
|
+
|
|
548
|
+
**Reveal it PLAIN, with no tinted well.** The toggle directly above already says what the text
|
|
549
|
+
is; a panel would put two identical recessed boxes on one row meaning two different things — a
|
|
550
|
+
verbatim record and a machine's reading of it. Keep the `Inset` for the machine's.
|
|
551
|
+
|
|
552
|
+
**The capture sits AT the feed**, as a `Composer` — compact until typed into. Logging an entry
|
|
553
|
+
is this section's input, not a distinct AREA of the record, and a section of its own costs a
|
|
554
|
+
rail entry, which is a claim that there is somewhere else to go.
|
|
555
|
+
|
|
556
|
+
**The tail FOLDS, it does not page.** A record's feed is read newest-first and the rest is
|
|
557
|
+
reference; an unbounded one makes every section below it unreachable by scroll. Show the recent
|
|
558
|
+
few with a toggle naming how many are behind it — a register pages, a record's feed folds.
|
|
559
|
+
|
|
560
|
+
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
|
|
564
|
+
SECTION's left edge, not the rows': the rows are inset by the disc rail, and at the fold's line
|
|
565
|
+
the rail has ended, so nothing visible occupies that indent. Same for any other fold on the
|
|
566
|
+
surface — a document set's "show the rest" is this control too.
|
|
567
|
+
- **No Handoff section.** Progress owns it: it says where the record SITS and carries the act
|
|
568
|
+
that moves it, so a second section repeating the handoff was one more place to look for one
|
|
569
|
+
answer. What that costs is explicit: the sibling record a handoff creates has no home on this
|
|
570
|
+
surface. It belongs on the Progress stage that produced it if an app needs it back.
|
|
571
|
+
|
|
572
|
+
**Activity is not that trail, and the line is the SUBJECT.** Activity carries what a person
|
|
573
|
+
said or sent to a counterparty; the handoff trail carries where the record moved and which
|
|
574
|
+
field changed. Put desk transitions in Activity and it becomes the audit log this template
|
|
575
|
+
declined to build — a feed of everything, scanned for nothing.
|
|
465
576
|
|
|
466
577
|
Recall is the undo: confirmed, it withdraws the sibling, clears the custody stamp that
|
|
467
578
|
handoff wrote, and returns the record to the desk BEFORE it — one step, never a jump to the
|
|
@@ -49,6 +49,7 @@ import { FileDropTarget } from "@lotics/ui/file_drop_target";
|
|
|
49
49
|
import { Link } from "@lotics/ui/link";
|
|
50
50
|
import { TextInputField } from "@lotics/ui/text_input_field";
|
|
51
51
|
import type { DisplayFile } from "@lotics/ui/file_thumbnail";
|
|
52
|
+
import { InlineFiles } from "@lotics/ui/inline_files";
|
|
52
53
|
import { InlineSelect } from "@lotics/ui/inline_select";
|
|
53
54
|
import { InlineTextInput } from "@lotics/ui/inline_text_input";
|
|
54
55
|
import { useSelection } from "@lotics/ui/use_selection";
|
|
@@ -361,6 +362,7 @@ function LinkedRecordScreen({ ma }: { ma: string }) {
|
|
|
361
362
|
const [ngayNhan, setNgayNhan] = useState(() => (r ? isoReceived(r.ngayNhan) : ""));
|
|
362
363
|
const [collected, setCollected] = useState(r?.daThu ? "yes" : "no");
|
|
363
364
|
const [fee, setFee] = useState<number | null>(r?.phi ?? null);
|
|
365
|
+
const [docs, setDocs] = useState<DisplayFile[]>([]);
|
|
364
366
|
if (!r) return null;
|
|
365
367
|
return (
|
|
366
368
|
<>
|
|
@@ -380,6 +382,25 @@ function LinkedRecordScreen({ ma }: { ma: string }) {
|
|
|
380
382
|
<DetailRow label="Fee">
|
|
381
383
|
<InlineNumberInput value={fee} onSave={persist(setFee)} min={0} format={(v) => (v == null ? "" : formatMoney(v))} accessibilityLabel="Fee" />
|
|
382
384
|
</DetailRow>
|
|
385
|
+
{/* FILES AS A FIELD — the `Inline*` member that used to be missing,
|
|
386
|
+
so a record's documents got hand-rolled per app as `FileRows` +
|
|
387
|
+
`pickFiles` + a `Button`, each copy re-deriving the same
|
|
388
|
+
decisions and usually missing `InlineSlot` (which is what keeps
|
|
389
|
+
this value column a column rather than two left edges).
|
|
390
|
+
|
|
391
|
+
The MANY case: `multiple` defaults true, the CTA becomes
|
|
392
|
+
"Add document" once something is attached, and the host APPENDS.
|
|
393
|
+
`tpl_record` shows the single-file case, where the verb is
|
|
394
|
+
replace — see the table in data_entry.md §InlineFiles. */}
|
|
395
|
+
<DetailRow label="Documents">
|
|
396
|
+
<InlineFiles
|
|
397
|
+
files={docs}
|
|
398
|
+
onAdd={(picked) => setDocs((p) => [...p, ...toDisplayFiles(picked)])}
|
|
399
|
+
onRemove={(f) => setDocs((p) => p.filter((x) => x.id !== f.id))}
|
|
400
|
+
addLabel="Attach document"
|
|
401
|
+
addMoreLabel="Add document"
|
|
402
|
+
/>
|
|
403
|
+
</DetailRow>
|
|
383
404
|
<DetailRow label="Collected">
|
|
384
405
|
<InlineSelect
|
|
385
406
|
value={collected}
|