@lotics/ui 33.0.0 → 35.0.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 +1 -1
- package/MIGRATION.md +73 -0
- package/docs/catalog.md +23 -5
- package/docs/composition.md +65 -1
- package/examples/tpl_item_list.tsx +17 -3
- package/package.json +1 -1
- package/src/avatar_size.ts +33 -8
- package/src/control_surface.ts +32 -0
- package/src/filter_chip.tsx +36 -22
- package/src/inline_button.tsx +6 -3
- package/src/inline_edit.tsx +10 -6
- package/src/inline_slot.tsx +11 -8
- package/src/member_chip.tsx +8 -1
- package/src/select.tsx +6 -3
package/AGENTS.md
CHANGED
|
@@ -17,7 +17,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
17
17
|
| [docs/catalog.md](./docs/catalog.md) | **The complete inventory** — Reach-by-role (each data role → the ONE canonical component) + every `@lotics/ui/<module>` entry point (incl. `@lotics/ui/vite`'s `loticsOptimizeDeps` + `loticsResolve()` — the pre-bundle list and the whole `resolve` block a custom-code app's `vite.config.ts` imports rather than hand-carries, dev-link included). Read before building any screen; reuse first. |
|
|
18
18
|
| [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, browser-autofill suppression (search controls only), find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (the `FilesEditor` COMPOUND — root owns selection/gallery/confirm, you compose the bar, a HOST verb reads `useFilesEditorSelection` — plus the three-way file INTAKE: CTA + `FileDropTarget` + `usePasteFiles`), stage gates, the commit-on-blur vs action-press ordering law (the kit gates the press — `pending_commits`). |
|
|
19
19
|
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split — it turns on WHO supplied the values (machine → a gate: a diff when something is being replaced, a full editable preview when records are being created from a document; human-typed → save-direct + the `ResultHeader` receipt), findings, provenance, confidence; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md)., the whole run in a dialog (`AgentRunScope`/`AgentRunPane`/`AgentRunActions` — a parked question REPLACES the feed, actions in the footer, **Stop** while streaming), **stopping** (`cancel` stops the run, `abort` only stops listening — so closing a dialog must `cancel` or it keeps billing); **review surfaces compose from atoms** — `DiffValue` (a changed value, droppable in any cell/row/total), `DiffMark` (what happened to the row — ONE circular disc, every surface), `useChangeSet` (accept/reject/undo bookkeeping, no layout) — see [MIGRATION.md](./MIGRATION.md) for the `ChangeReview` family they replace |
|
|
20
|
-
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other), banded cards, register vs inset rows, master-detail `Drawer` on a LIST screen vs a child collection's row EXPANDING inside a record, view controls, RECORD EXTENT (one page, sections scrolled to and never routed to), color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
20
|
+
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other), banded cards, register vs inset rows (incl. the register laws a row centres its cells by: every cell a FIXED height, a pressable cell on the shared hover token, a column sized by what it carries), master-detail `Drawer` on a LIST screen vs a child collection's row EXPANDING inside a record, view controls, RECORD EXTENT (one page, sections scrolled to and never routed to), color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
|
21
21
|
| [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget). |
|
|
22
22
|
|
|
23
23
|
## Iron rules
|
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,79 @@ 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
|
+
## 35.0.0 — `FilterChip` has no Clear footer; the × is the clear
|
|
8
|
+
|
|
9
|
+
`FilterChip` no longer renders a Clear button in its popover footer. **Nothing to
|
|
10
|
+
change at a call site** — `onClear` and `clearLabel` are unchanged, and the × on
|
|
11
|
+
the pill still clears, still carries `clearLabel` as its tooltip, and still
|
|
12
|
+
replaces the chevron whenever the chip is active.
|
|
13
|
+
|
|
14
|
+
**Why.** The pill's × and the footer button did the same thing, and the footer
|
|
15
|
+
cost a whole bordered band to say it twice. The editors this pill exists to wrap
|
|
16
|
+
bring their own bottom actions — an `OptionList` with `enableSelectAll` renders
|
|
17
|
+
"Select all / Deselect all" in a bordered band of its own — so a multi-select
|
|
18
|
+
filter ended with TWO stacked rules, the lower one holding a single button that
|
|
19
|
+
"Deselect all" directly above it already performed. Measured on a live filter:
|
|
20
|
+
bands of 49px and 47px, each with its own 1px top border.
|
|
21
|
+
|
|
22
|
+
The × is also the more reachable of the two: it is on the pill, which is on
|
|
23
|
+
screen whether or not the popover is open, while the footer button needed the
|
|
24
|
+
panel opened first.
|
|
25
|
+
|
|
26
|
+
**One behaviour change worth knowing:** passing a custom `footer` no longer
|
|
27
|
+
suppresses the ×. `showClear` used to gate both, so a Save-footer pill silently
|
|
28
|
+
lost its clear — if you were relying on that to hide the ×, pass no `onClear`
|
|
29
|
+
instead, which is what "not clearable" has always meant.
|
|
30
|
+
|
|
31
|
+
**If your filter had no other way to empty itself**, it still does: the ×. And if
|
|
32
|
+
your editor genuinely needs a clear INSIDE the panel (a range slider, a counter —
|
|
33
|
+
controls with no per-option deselect), put one in the `footer` slot, which is
|
|
34
|
+
still there and is now the only thing that draws a footer band.
|
|
35
|
+
|
|
36
|
+
## 34.0.0 — one avatar rung fits a control band
|
|
37
|
+
|
|
38
|
+
**`AVATAR_PX.md` is 28, not 36.** Nothing to change at a call site — `md` is still
|
|
39
|
+
the default and still shows two initials — but every `Avatar`, `MemberChip` and
|
|
40
|
+
`GroupAvatar` on that rung renders 8px smaller (its initials drop a type rung with
|
|
41
|
+
it, to hold the same share of the circle), and the controls holding them get
|
|
42
|
+
SHORTER, which is the point.
|
|
43
|
+
|
|
44
|
+
`CONTROL_HEIGHT` is a promise: a toolbar row, a record's value column and a
|
|
45
|
+
picker's option list read as one band because every control on them is 40. TEXT
|
|
46
|
+
keeps that promise for free — a line is ~20, so `minHeight` governs and a
|
|
47
|
+
surface's vertical padding never binds. That is exactly why each 40px surface had
|
|
48
|
+
quietly picked its own inset (`Select`'s trigger 4, the inline editor 6,
|
|
49
|
+
`InlineSlot` 8) with no number to pick it from, and nothing on a resting screen
|
|
50
|
+
ever showed the disagreement.
|
|
51
|
+
|
|
52
|
+
Putting a 36px avatar in one broke all three at once. The same member chip
|
|
53
|
+
measured **46** in a `Select` trigger, **50** in an inline editor and **54** in an
|
|
54
|
+
`InlineSlot` — a record whose value column is the only thing telling a reader
|
|
55
|
+
these fields belong to one record, with the member row 10px off every other row
|
|
56
|
+
in it.
|
|
57
|
+
|
|
58
|
+
So the budget is stated once and the surfaces derive from it:
|
|
59
|
+
|
|
60
|
+
- **`CONTROL_CONTENT_HEIGHT`** (28) — the tallest fixed-height node a 40px band
|
|
61
|
+
seats, 6 per side of air. Not a fresh number: it is what `InlineButton` and a
|
|
62
|
+
`md` `IconButton` had already settled on for sitting inside a field. An avatar
|
|
63
|
+
is not a special kind of child, so it gets the same height they do.
|
|
64
|
+
- **`CONTROL_PADDING_V`** (5) — derived, and the 1px border counts inside the 6
|
|
65
|
+
per side. Reading the padding and forgetting the border is how the drift
|
|
66
|
+
started.
|
|
67
|
+
|
|
68
|
+
Both from `@lotics/ui/control_surface`. **If you build your own 40px control, pad
|
|
69
|
+
by `CONTROL_PADDING_V` and keep fixed-height children at or under
|
|
70
|
+
`CONTROL_CONTENT_HEIGHT`** — anything taller belongs in a row that sets its own
|
|
71
|
+
height, not in a control.
|
|
72
|
+
|
|
73
|
+
Two knock-on effects worth looking at once:
|
|
74
|
+
|
|
75
|
+
- A **multi-line control at rest** (a rich select showing title + description) is
|
|
76
|
+
~2px shorter, since its padding came off the same constant.
|
|
77
|
+
- An `InlineSlot`/`InlineStatic` value now actually matches the editor beside it.
|
|
78
|
+
It always claimed to; it was 2px more padded, invisible at one line.
|
|
79
|
+
|
|
7
80
|
## 33.0.0 — `DrawerScrollArea`
|
|
8
81
|
|
|
9
82
|
A `Drawer` gets the content primitive its siblings already had, named to match
|
package/docs/catalog.md
CHANGED
|
@@ -76,12 +76,20 @@ inline: a picker option, an assignee, a `select_member` value. Pure: resolve the
|
|
|
76
76
|
your directory and pass `name` / `image`; never hand-roll `Avatar` + `Text`. (`MemberSelect`
|
|
77
77
|
renders these per option.)
|
|
78
78
|
|
|
79
|
-
**Size is a RUNG, never a pixel count** — `sm | md | lg | xl` = 24 /
|
|
79
|
+
**Size is a RUNG, never a pixel count** — `sm | md | lg | xl` = 24 / 28 / 48 / 72
|
|
80
80
|
(`@lotics/ui/avatar_size`, shared by `Avatar`, `MemberChip` and `GroupAvatar`). Default `md`.
|
|
81
81
|
The number was removed because the same concept had drifted to eleven different values across
|
|
82
82
|
the product and its apps, so one person looked different on every screen. Pick by density:
|
|
83
|
-
`sm` for a dense row or inline cell, `md` for a register or list (the default),
|
|
84
|
-
prominent row or the account menu, `xl` for a profile's own header.
|
|
83
|
+
`sm` for a dense row or inline cell, `md` for a control band, register or list (the default),
|
|
84
|
+
`lg` for a prominent row or the account menu, `xl` for a profile's own header.
|
|
85
|
+
|
|
86
|
+
**`md` is `CONTROL_CONTENT_HEIGHT`** (28) — the largest rung a 40px control band seats without
|
|
87
|
+
growing, and the height `InlineButton` and a `md` `IconButton` already used to sit inside a
|
|
88
|
+
field. An avatar is the tallest thing in whatever row holds it, so a rung that overshoots
|
|
89
|
+
doesn't look slightly big, it RESIZES the control: at the old 36 the same member chip measured
|
|
90
|
+
46 in a `Select` trigger, 50 in an inline editor and 54 in an `InlineSlot`, because each
|
|
91
|
+
surface had picked its own inset with no number to pick it from. Put nothing taller than `md`
|
|
92
|
+
in a control — a bigger avatar belongs in a row that sets its own height.
|
|
85
93
|
|
|
86
94
|
**`sm` shows ONE initial; every larger rung shows two** — two letters are not legible at 24px.
|
|
87
95
|
That is the practical reason not to reach for `sm` by reflex: on a real roster a single letter
|
|
@@ -411,7 +419,13 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
411
419
|
hand-rolled plain DOM/CSS (nothing injects them automatically; `@lotics/ui` components
|
|
412
420
|
don't need them).
|
|
413
421
|
- **`spacing`** — the `SPACE` scale + `SpaceToken`.
|
|
414
|
-
- **`control_surface`** — `CONTROL_HEIGHT` (40), `
|
|
422
|
+
- **`control_surface`** — `CONTROL_HEIGHT` (40), `CONTROL_CONTENT_HEIGHT` (28 — the tallest
|
|
423
|
+
FIXED-height node a 40px band seats, 6 per side of air; what `InlineButton` and a `md`
|
|
424
|
+
`IconButton` already used, now named so an avatar or any other child lands on it too. Text
|
|
425
|
+
never needs it because `minHeight` governs a single line, which is exactly why three surfaces
|
|
426
|
+
could each pick a different inset and only disagree once an avatar went in),
|
|
427
|
+
`CONTROL_PADDING_V` (5 — derived from the pair, BORDER INCLUDED in the 6 per side; every 40px
|
|
428
|
+
surface pads by it instead of a number of its own), `CONTROL_RADIUS` (10), `CONTROL_TEXT_INSET`
|
|
415
429
|
(9 — how far a control insets its OWN text: 1px border + 8px padding; anything that must line
|
|
416
430
|
up with a control's WORDS rather than its box carries it — a caption under an inline editor,
|
|
417
431
|
a note under a figure — rather than a hand-measured 8),
|
|
@@ -1103,7 +1117,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1103
1117
|
`isColumnFilterActive` + `columnFilterSummary`: the typed per-column filter pill; for a
|
|
1104
1118
|
register filtering on several columns.
|
|
1105
1119
|
- **`filter_chip`** — `FilterChip` + `selectSummary`: the toolbar filter pill hosting a
|
|
1106
|
-
facet (options, a `Slider range`, a `Counter`).
|
|
1120
|
+
facet (options, a `Slider range`, a `Counter`). It draws NO bottom band of its own — the ×
|
|
1121
|
+
on the pill is the clear, and the editor inside brings whatever actions it has, so a
|
|
1122
|
+
multi-select's "select all / deselect all" is the only rule in the panel. `footer` is for an
|
|
1123
|
+
editor that must COMMIT (Cancel / Save); reach for it only then, since it adds a second band
|
|
1124
|
+
under an editor that already has one.
|
|
1107
1125
|
- **`summary_line`** — `SummaryLine`: the light inline summary of a register/list's FILTERED
|
|
1108
1126
|
view, sits below the toolbar; NOT the boxed dashboard `kpi_strip` band. Every item is an
|
|
1109
1127
|
AGGREGATE over the rows in view (a count, a sum, a fill), and the strip goes with the set it
|
package/docs/composition.md
CHANGED
|
@@ -397,6 +397,58 @@ so `Table` also serves read-only tabular data (a fee breakdown, a spec sheet), n
|
|
|
397
397
|
registers. A pressable `TableRow` REQUIRES `accessibilityLabel` ("Open …") — its keyboard door is
|
|
398
398
|
an empty overlay with no content to derive a name from.
|
|
399
399
|
|
|
400
|
+
**A CELL THAT DOES NOT SET THE ROW'S HEIGHT MUST NOT VARY WITH THE DATA.** A row centres its
|
|
401
|
+
cells, so such a cell's first line lands at `(rowH − cellH) / 2`: one line on some rows and two
|
|
402
|
+
on others puts that first line at two different offsets, and the column stops having a baseline.
|
|
403
|
+
Uniform ROW heights hide it completely — the rows measure identical, the content inside them does
|
|
404
|
+
not — and the drift is against the row's own centred chrome (ordinal, checkbox) as much as
|
|
405
|
+
against the neighbouring columns.
|
|
406
|
+
|
|
407
|
+
**It takes a height FLOOR to bite**, which is the part worth understanding rather than
|
|
408
|
+
memorising. A `TableRow` with `minHeight` above its tallest cell holds slack that centring then
|
|
409
|
+
distributes; without the floor the tallest cell sets the row, every cell starts at the top of its
|
|
410
|
+
own row, and nothing drifts. So the cell that IS tallest may vary freely — it just makes the rows
|
|
411
|
+
ragged instead, which is a different and far more visible problem. It is the cells living inside
|
|
412
|
+
another cell's slack that have to hold still.
|
|
413
|
+
|
|
414
|
+
**"Fixed" means the same every row, NOT one line.** A two-line cell is fine if it is two lines on
|
|
415
|
+
every row. And the cost is real: bounding a cell usually means `numberOfLines={1}` and
|
|
416
|
+
truncation, so you are buying a baseline with clipped text. Worth it for an identifier beside a
|
|
417
|
+
contact; think harder for a cell whose job is prose, where the honest answer may be to move that
|
|
418
|
+
content off the register entirely.
|
|
419
|
+
|
|
420
|
+
Two corollaries, both counter-intuitive enough to state:
|
|
421
|
+
|
|
422
|
+
- **Shrinking a NEIGHBOURING cell does not fix it.** The spread is a function of the varying
|
|
423
|
+
cell's own range, not of the row's height: a 20/42 cell spreads 11px in a 52px row and 11px in
|
|
424
|
+
a 68px one. Only bounding the cell that varies removes it.
|
|
425
|
+
- **The fix is never a cross-axis knob.** `Table` has no `align` (see MIGRATION 32.0.0): topping
|
|
426
|
+
the cells leaves the chrome centred, so the row reads as scatter instead.
|
|
427
|
+
|
|
428
|
+
Bound a cell by giving every row the same content, not by hoping the data is uniform. A line that
|
|
429
|
+
appears only when its field is set is the defect; RESERVING the line is the fix — **but only when
|
|
430
|
+
the field is nearly always present.** A box reserved for a value that half the rows lack reads as
|
|
431
|
+
a column that forgot something, which is its own defect: prefer a field the rows actually carry
|
|
432
|
+
(an identifier, a contact) over one that is merely interesting. A cell may hold more than one line
|
|
433
|
+
freely when something FIXED occupies its top — a meter, a thumbnail — because the primary text
|
|
434
|
+
then still lands near the row's centre, where the chrome already is. `tpl_item_list`'s identity
|
|
435
|
+
cell is the worked example: name on top, the supporting values UNCONDITIONALLY beneath.
|
|
436
|
+
|
|
437
|
+
**A pressable CELL wears `ROW_CONTROL_HOVER` / `ROW_CONTROL_PRESS`, never a hand-picked grey.**
|
|
438
|
+
Reaching a cell means crossing its row, so both wash at once — a cell painting the row's own
|
|
439
|
+
`zinc-100` vanishes under the pointer, and anything LIGHTER reads as a hole punched in the row.
|
|
440
|
+
The tokens are one step darker for exactly that reason, and they are what `IconButton`,
|
|
441
|
+
`CopyButton` and `CommentsButton` already paint, so a register whose cells use them speaks one
|
|
442
|
+
hover language. Bleed the wash outward (`marginHorizontal: -8` against its own padding) so the
|
|
443
|
+
cell's content stays on the column's edge — see `ROW_WASH_BLEED`.
|
|
444
|
+
|
|
445
|
+
**Size a flexible column by what it CARRIES, measured.** A `flex` share is a claim about content;
|
|
446
|
+
when the content shrinks — a value moves to the record, a stacked pair becomes a count — the
|
|
447
|
+
share left behind is white space, and the columns beside it pay for it. Measure the widest row's
|
|
448
|
+
content per column and set `flex` from that. A column whose cell holds a PROPORTION (a meter, a
|
|
449
|
+
bar) is the exception in one direction only: cap it, because a gauge stops reading as an
|
|
450
|
+
instrument once its track is long enough to be a rule across the row.
|
|
451
|
+
|
|
400
452
|
Make a register SELECTABLE with the `Table` `leading` gutter + `selectAll` slot — a
|
|
401
453
|
`CheckboxInput` per `TableRow` (its `leading` slot) + a select-all in the header band, the ticked
|
|
402
454
|
rows `marked`, paired with a `FloatingActionBar` (its "Clear" escape is locale-resolved — pass
|
|
@@ -517,7 +569,19 @@ shape.
|
|
|
517
569
|
|
|
518
570
|
## One view-control vocabulary
|
|
519
571
|
|
|
520
|
-
All view controls are 40px tall (`CONTROL_HEIGHT`), `sm` labels, in ONE wrapping band
|
|
572
|
+
All view controls are 40px tall (`CONTROL_HEIGHT`), `sm` labels, in ONE wrapping band.
|
|
573
|
+
|
|
574
|
+
**A 40px band affords 28 (`CONTROL_CONTENT_HEIGHT`), and a surface pads by
|
|
575
|
+
`CONTROL_PADDING_V` rather than a number of its own.** The height only holds for free when the
|
|
576
|
+
content is TEXT: a line is ~20, so `minHeight` governs and the padding never binds — which is
|
|
577
|
+
why a surface can pick any inset it likes and nothing on a resting screen ever contradicts it.
|
|
578
|
+
The moment something with a FIXED height goes in — an avatar, a swatch, a thumbnail — that
|
|
579
|
+
inset decides the control's whole height, and three surfaces that had each chosen their own
|
|
580
|
+
(4, 6, 8) rendered one member chip at 46, 50 and 54. So: a fixed-height child is sized against
|
|
581
|
+
the band's budget, never against how it looks alone, and a surface that needs more room than
|
|
582
|
+
the budget is not a control. The budget is 28 because that is what `InlineButton` and a `md`
|
|
583
|
+
`IconButton` were already using inside a field — a control's children must not differ in height
|
|
584
|
+
by what KIND of thing they are, so an avatar takes the same number rather than a fourth one.
|
|
521
585
|
|
|
522
586
|
- **`SearchInput`** is THE search box — a white `TextInputField` preset (the shared
|
|
523
587
|
`CONTROL_RADIUS` like every other input, a thin border, the leading search glyph + a clear ✕).
|
|
@@ -237,7 +237,16 @@ function HoSoRow({ hs, ordinal, daThu, selected, marked, selectable, onToggle, o
|
|
|
237
237
|
because it is the one value copied often enough to earn one (a desk
|
|
238
238
|
pastes it into a message, a rep dials it). The verb rides the value
|
|
239
239
|
inside its own cell: the trailing gutter belongs to the ⋯, and with
|
|
240
|
-
two values in this cell it would have nothing to name.
|
|
240
|
+
two values in this cell it would have nothing to name.
|
|
241
|
+
|
|
242
|
+
UNCONDITIONAL, and that is the load-bearing part of this shape. A
|
|
243
|
+
row centres its cells, so a supporting line that appears only when
|
|
244
|
+
its field is set makes this cell two heights, and the NAME above it
|
|
245
|
+
then sits at two different offsets down the column while every row
|
|
246
|
+
still measures the same. That is why the values here are ones every
|
|
247
|
+
record carries — a key, a contact — rather than whichever field is
|
|
248
|
+
most interesting: the line has to be honest on every row to be
|
|
249
|
+
reservable at all. */}
|
|
241
250
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 2 }}>
|
|
242
251
|
{/* The KEY, muted, on the supporting line — at most this, never a
|
|
243
252
|
column and never the row's name.
|
|
@@ -250,8 +259,13 @@ function HoSoRow({ hs, ordinal, daThu, selected, marked, selectable, onToggle, o
|
|
|
250
259
|
gives way, which is also the right order of loss: the phone has a
|
|
251
260
|
Copy control beside it that hands over the full value regardless. */}
|
|
252
261
|
<Text size="xs" color="muted" tabular numberOfLines={1} style={{ flexShrink: 0 }}>{hs.ma}</Text>
|
|
253
|
-
|
|
254
|
-
|
|
262
|
+
{/* A GAP, not a middot. Two facts of different kinds sat either side
|
|
263
|
+
of a ` · ` here, which is the separator this kit bans outright —
|
|
264
|
+
punctuation claiming a relation it refuses to name, and dropped
|
|
265
|
+
entirely by a screen reader. Spacing separates them without
|
|
266
|
+
asserting anything, and the two are already told apart by shape:
|
|
267
|
+
one is a key, one is a number with a Copy control on it. */}
|
|
268
|
+
<Text size="xs" color="muted" tabular numberOfLines={1} style={{ flexShrink: 1, marginLeft: 8 }}>{hs.dienThoai}</Text>
|
|
255
269
|
<CopyButton value={hs.dienThoai} label="Copy phone number" />
|
|
256
270
|
</View>
|
|
257
271
|
</View>
|
package/package.json
CHANGED
package/src/avatar_size.ts
CHANGED
|
@@ -13,11 +13,28 @@ import type { TextSize } from "./text";
|
|
|
13
13
|
*/
|
|
14
14
|
export type AvatarSize = "sm" | "md" | "lg" | "xl";
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* THE RUNGS.
|
|
18
|
+
*
|
|
19
|
+
* `md` IS `CONTROL_CONTENT_HEIGHT`, and that is the whole design. An avatar is
|
|
20
|
+
* the tallest thing in any row that holds one, so a rung that overshoots its
|
|
21
|
+
* container does not look slightly big — it RESIZES the container. The default
|
|
22
|
+
* rung therefore has to be the largest a 40px control band can seat, and that
|
|
23
|
+
* number was already fixed elsewhere in the kit: 28, what `InlineButton` and a
|
|
24
|
+
* `md` `IconButton` use to sit inside a field. An avatar is not a special kind
|
|
25
|
+
* of child; it gets the same height they do.
|
|
26
|
+
*
|
|
27
|
+
* It was 36, which fits nothing. The same member chip measured 46 in a `Select`
|
|
28
|
+
* trigger, 50 in an inline editor and 54 in an `InlineSlot` — three heights for
|
|
29
|
+
* one control band, because each surface had picked its own inset and none of
|
|
30
|
+
* them had a number to pick it FROM.
|
|
31
|
+
*/
|
|
16
32
|
export const AVATAR_PX: Record<AvatarSize, number> = {
|
|
17
33
|
/** Dense rows and inline chips — one initial, because two do not fit. */
|
|
18
34
|
sm: 24,
|
|
19
|
-
/** The default:
|
|
20
|
-
|
|
35
|
+
/** The default: a 40px control band, a register, a list — anywhere a person is
|
|
36
|
+
* a row. Equals `CONTROL_CONTENT_HEIGHT` (`control_surface`). */
|
|
37
|
+
md: 28,
|
|
21
38
|
/** Prominent rows, the account menu. */
|
|
22
39
|
lg: 48,
|
|
23
40
|
/** A profile's own header. */
|
|
@@ -32,19 +49,27 @@ export const AVATAR_PX: Record<AvatarSize, number> = {
|
|
|
32
49
|
* native, and `text.css` moves again at 768px, so these are the desktop numbers
|
|
33
50
|
* and only a render can give them):
|
|
34
51
|
*
|
|
35
|
-
* sm 12 on 24 = 50%
|
|
36
|
-
*
|
|
52
|
+
* sm 12 on 24 = 50%
|
|
53
|
+
* md 12 on 28 = 43%
|
|
54
|
+
* lg 20 on 48 = 42%
|
|
55
|
+
* xl 32 on 72 = 44%
|
|
37
56
|
*
|
|
38
|
-
* The two-letter rungs sit within
|
|
57
|
+
* The two-letter rungs sit within 2 points of each other, so an avatar reads the
|
|
39
58
|
* same at every size rather than growing emptier as it grows — `lg` was on `md`
|
|
40
59
|
* type (16 on 48 = 33%) and was visibly the thin one. `sm` runs richer by design:
|
|
41
60
|
* one letter in a small circle needs the weight.
|
|
61
|
+
*
|
|
62
|
+
* `md` shares `sm`'s rung and is NOT the odd one out for it: the same 12px reads
|
|
63
|
+
* as 50% of a 24px circle holding one letter and 43% of a 28px circle holding
|
|
64
|
+
* two. Keeping it on `sm` type (14) would have put `md` at 50% — the fattest of
|
|
65
|
+
* the two-letter rungs, and the same defect in the other direction as the `lg`
|
|
66
|
+
* one this table was written to fix.
|
|
42
67
|
*/
|
|
43
68
|
export const AVATAR_TEXT: Record<AvatarSize, TextSize> = {
|
|
44
69
|
sm: "xs", // 12 on 24 — one letter, so it carries a larger share of the circle
|
|
45
|
-
md: "
|
|
46
|
-
lg: "lg", //
|
|
47
|
-
xl: "xxl", //
|
|
70
|
+
md: "xs", // 12 on 28
|
|
71
|
+
lg: "lg", // 20 on 48
|
|
72
|
+
xl: "xxl", // 32 on 72
|
|
48
73
|
};
|
|
49
74
|
|
|
50
75
|
const words = (name: string): string[] => name.trim().split(/\s+/).filter((w) => w !== "");
|
package/src/control_surface.ts
CHANGED
|
@@ -5,6 +5,38 @@ import { colors } from "./colors";
|
|
|
5
5
|
* buttons) aligns to it so a toolbar row reads as one band. */
|
|
6
6
|
export const CONTROL_HEIGHT = 40;
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* THE TALLEST FIXED-HEIGHT NODE A CONTROL BAND CAN SEAT — and so the inner height
|
|
10
|
+
* every 40px surface must afford. 6 per side of air, BORDER INCLUDED.
|
|
11
|
+
*
|
|
12
|
+
* `CONTROL_HEIGHT` is a promise — a toolbar row, a record's value column and a
|
|
13
|
+
* picker's option list read as one band because every control on them is 40. TEXT
|
|
14
|
+
* keeps that promise for free: a line is ~20, so `minHeight` governs and a
|
|
15
|
+
* surface's padding never binds. That is precisely why the promise broke the
|
|
16
|
+
* first time a control was handed something with a FIXED height. Each surface had
|
|
17
|
+
* quietly picked its own inset — 4, 6, 8 — with no number to pick it from, and
|
|
18
|
+
* nothing on a resting screen could reveal the disagreement; the moment a 36px
|
|
19
|
+
* avatar went in, the same member chip came out 46 in a `Select` trigger, 50 in
|
|
20
|
+
* an inline editor and 54 in an `InlineSlot`.
|
|
21
|
+
*
|
|
22
|
+
* 28 is not a fresh choice: it is what `InlineButton` and an `IconButton` at `md`
|
|
23
|
+
* had ALREADY settled on for sitting inside a field, air and all. Naming it once
|
|
24
|
+
* is what stops the next fixed-height child inventing a fourth number, and it is
|
|
25
|
+
* why an avatar on the default rung is 28 too — a control's children should not
|
|
26
|
+
* differ in height by what kind of thing they are.
|
|
27
|
+
*
|
|
28
|
+
* Anything taller does not belong in a control at all; it belongs in a row that
|
|
29
|
+
* sets its own height.
|
|
30
|
+
*/
|
|
31
|
+
export const CONTROL_CONTENT_HEIGHT = 28;
|
|
32
|
+
|
|
33
|
+
/** A control's own vertical inset, derived so the band seats
|
|
34
|
+
* {@link CONTROL_CONTENT_HEIGHT} at exactly {@link CONTROL_HEIGHT}. The 1px
|
|
35
|
+
* border is part of the 6, so a bordered surface pads by this and an unbordered
|
|
36
|
+
* one by this + 1 — reading the padding and forgetting the border is what let
|
|
37
|
+
* three surfaces drift in the first place. */
|
|
38
|
+
export const CONTROL_PADDING_V = (CONTROL_HEIGHT - CONTROL_CONTENT_HEIGHT) / 2 - 1;
|
|
39
|
+
|
|
8
40
|
/** The narrowest a control may be and still be usable — below it, editors get
|
|
9
41
|
* crushed and option text has nowhere to go. It is the threshold at which a
|
|
10
42
|
* `DetailTable` gives up its side-by-side columns and stacks, and the floor
|
package/src/filter_chip.tsx
CHANGED
|
@@ -7,7 +7,6 @@ import { Chip } from "./chip";
|
|
|
7
7
|
import { Popover, PopoverTrigger, PopoverContent, PopoverFooter } from "./popover";
|
|
8
8
|
import type { PopoverSide, PopoverAlign } from "./popover";
|
|
9
9
|
import { useLoticsLocale } from "./locale";
|
|
10
|
-
import { Button } from "./button";
|
|
11
10
|
|
|
12
11
|
export interface FilterChipProps {
|
|
13
12
|
/** The dimension name — shown alone when inactive ("Owner"), prefixed when
|
|
@@ -18,10 +17,12 @@ export interface FilterChipProps {
|
|
|
18
17
|
* selection RICHLY in the trigger (member avatars, colour dots). Empty /
|
|
19
18
|
* undefined renders the inactive pill (label + chevron, no clear). */
|
|
20
19
|
summary?: ReactNode;
|
|
21
|
-
/** Clears the dimension — renders the × on the pill
|
|
22
|
-
*
|
|
20
|
+
/** Clears the dimension — renders the × on the pill whenever a summary is
|
|
21
|
+
* present. The × is the ONLY clear: the editor inside brings its own bottom
|
|
22
|
+
* actions, and a second clear in the popover footer stacked a redundant band
|
|
23
|
+
* under them. */
|
|
23
24
|
onClear?: () => void;
|
|
24
|
-
/**
|
|
25
|
+
/** Tooltip on the pill's × (pass a translated string). */
|
|
25
26
|
clearLabel?: string;
|
|
26
27
|
/** The editor revealed on press — a premium primitive (`Slider range`,
|
|
27
28
|
* `Counter`, `OptionList` multi) or any composed control. `FilterChip` owns
|
|
@@ -35,8 +36,11 @@ export interface FilterChipProps {
|
|
|
35
36
|
/** Optional controlled popover state — for an editor that closes on Save. */
|
|
36
37
|
open?: boolean;
|
|
37
38
|
onOpenChange?: (open: boolean) => void;
|
|
38
|
-
/** A custom popover footer (e.g. Cancel / Save) —
|
|
39
|
-
* Lets a non-filter VALUE pill (a setting gate)
|
|
39
|
+
/** A custom popover footer (e.g. Cancel / Save) — for an editor that commits
|
|
40
|
+
* rather than applying live. Lets a non-filter VALUE pill (a setting gate)
|
|
41
|
+
* reuse the same shell. Reach for it only when the editor NEEDS a commit: an
|
|
42
|
+
* editor that already carries bottom actions of its own gets a second
|
|
43
|
+
* bordered band from this. */
|
|
40
44
|
footer?: ReactNode;
|
|
41
45
|
}
|
|
42
46
|
|
|
@@ -61,9 +65,11 @@ export function selectSummary(
|
|
|
61
65
|
* filter dimension, the sibling of `ChipGroup` (which lays ONE hot dimension's
|
|
62
66
|
* options out inline). Many dimensions stay scannable because each collapses to
|
|
63
67
|
* a single pill: inactive reads "Label ⌄", active reads "Label: summary" with an
|
|
64
|
-
* × to clear. `FilterChip` bakes the consistent chrome — the preview pill
|
|
65
|
-
* padded popover body for the composed editor
|
|
66
|
-
*
|
|
68
|
+
* × to clear. `FilterChip` bakes the consistent chrome — the preview pill and a
|
|
69
|
+
* padded popover body for the composed editor — so every filter looks and
|
|
70
|
+
* behaves the same. It adds NO bottom band of its own: the editor inside brings
|
|
71
|
+
* whatever actions it has, and a shell that also contributed one gave every
|
|
72
|
+
* multi-select filter two stacked rules. Drop a premium primitive inside
|
|
67
73
|
* (`Slider range`, `Counter`, `OptionList` multi). With `footer` (+ controlled
|
|
68
74
|
* `open`/`onOpenChange`) the same shell wraps a non-filter VALUE pill — a
|
|
69
75
|
* setting gate with Cancel/Save — keeping it on the one pill surface. The table
|
|
@@ -73,9 +79,23 @@ export function FilterChip(props: FilterChipProps) {
|
|
|
73
79
|
const { label, summary, onClear, children, side = "bottom", align = "start", open, onOpenChange, footer } = props;
|
|
74
80
|
const clearLabel = props.clearLabel ?? useLoticsLocale().filterChip.clear;
|
|
75
81
|
const active = summary != null && (typeof summary !== "string" || summary.length > 0);
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
82
|
+
// THE × IS THE CLEAR, and it is the only one. A valued, non-clearable pill
|
|
83
|
+
// ("Target: 20") keeps its chevron.
|
|
84
|
+
//
|
|
85
|
+
// There used to be a baked Clear in the popover footer as well, and it was a
|
|
86
|
+
// duplicate that cost a whole extra band. The editors this pill is built for
|
|
87
|
+
// bring their own bottom actions — an `OptionList` with `enableSelectAll`
|
|
88
|
+
// renders "Select all / Deselect all" in a bordered band of its own — so the
|
|
89
|
+
// popover ended with TWO stacked rules whose lower one held a single button
|
|
90
|
+
// that "Deselect all" directly above it already performed. Measured on a live
|
|
91
|
+
// filter: bands at 49px and 47px, each with its own 1px top border.
|
|
92
|
+
//
|
|
93
|
+
// The × is also the more discoverable of the two: it sits on the pill, which
|
|
94
|
+
// is on screen whether or not the popover is open, and it carries `clearLabel`
|
|
95
|
+
// as its tooltip. The footer button was reachable only by opening the panel
|
|
96
|
+
// first. A custom `footer` (Cancel / Save) no longer suppresses it either —
|
|
97
|
+
// that coupling meant a Save-footer pill silently lost its clear.
|
|
98
|
+
const showClear = active && !!onClear;
|
|
79
99
|
|
|
80
100
|
// Own the open state so the editor can close itself via the render-prop
|
|
81
101
|
// `close`, while still honoring a controlled `open`/`onOpenChange` from the
|
|
@@ -112,13 +132,7 @@ export function FilterChip(props: FilterChipProps) {
|
|
|
112
132
|
</PopoverTrigger>
|
|
113
133
|
<PopoverContent style={styles.body} disableBodyScroll>
|
|
114
134
|
{typeof children === "function" ? children({ close: () => setOpen(false) }) : children}
|
|
115
|
-
{footer ?
|
|
116
|
-
<PopoverFooter>{footer}</PopoverFooter>
|
|
117
|
-
) : showClear ? (
|
|
118
|
-
<PopoverFooter align="start">
|
|
119
|
-
<Button title={clearLabel} color="muted" onPress={onClear} />
|
|
120
|
-
</PopoverFooter>
|
|
121
|
-
) : null}
|
|
135
|
+
{footer ? <PopoverFooter>{footer}</PopoverFooter> : null}
|
|
122
136
|
</PopoverContent>
|
|
123
137
|
</Popover>
|
|
124
138
|
);
|
|
@@ -126,9 +140,9 @@ export function FilterChip(props: FilterChipProps) {
|
|
|
126
140
|
|
|
127
141
|
const styles = StyleSheet.create({
|
|
128
142
|
// The popover hugs its content — a wide control (e.g. Slider) sets its
|
|
129
|
-
// OWN fixed width; the shell never forces one. No extra padding: the editor
|
|
130
|
-
//
|
|
131
|
-
//
|
|
143
|
+
// OWN fixed width; the shell never forces one. No extra padding: the editor
|
|
144
|
+
// then sits on the popover's own 8px inset, so a multi-select's options and
|
|
145
|
+
// its select-all band line up on one left edge.
|
|
132
146
|
body: {
|
|
133
147
|
gap: 8,
|
|
134
148
|
},
|
package/src/inline_button.tsx
CHANGED
|
@@ -2,11 +2,14 @@ import { type GestureResponderEvent } from "react-native";
|
|
|
2
2
|
import { Text } from "./text";
|
|
3
3
|
import { colors } from "./colors";
|
|
4
4
|
import { PressableHighlight } from "./pressable_highlight";
|
|
5
|
-
import { CONTROL_TRANSITION } from "./control_surface";
|
|
5
|
+
import { CONTROL_CONTENT_HEIGHT, CONTROL_TRANSITION } from "./control_surface";
|
|
6
6
|
|
|
7
7
|
/** The height that fits INSIDE the 40px inline control band with 6px of air
|
|
8
|
-
* above and below — the same air a `DetailRow`'s label gets against its value.
|
|
9
|
-
|
|
8
|
+
* above and below — the same air a `DetailRow`'s label gets against its value.
|
|
9
|
+
* This file is where that number was FIRST worked out; it now lives in
|
|
10
|
+
* `control_surface` so an avatar or any other fixed-height child of a control
|
|
11
|
+
* lands on the same line rather than inventing its own. */
|
|
12
|
+
const INLINE_BUTTON_HEIGHT = CONTROL_CONTENT_HEIGHT;
|
|
10
13
|
|
|
11
14
|
interface InlineButtonProps {
|
|
12
15
|
/** The verb, as a word. An inline button is never icon-only — it sits on a
|
package/src/inline_edit.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { useLoticsLocale } from "./locale";
|
|
|
6
6
|
import { ActivityIndicator } from "./activity_indicator";
|
|
7
7
|
import { FocusRingPressable } from "./focus_ring_pressable";
|
|
8
8
|
import { colors } from "./colors";
|
|
9
|
-
import { FOCUS_RING, CONTROL_RADIUS, HOVER_BORDER, CONTROL_TRANSITION } from "./control_surface";
|
|
9
|
+
import { FOCUS_RING, CONTROL_RADIUS, CONTROL_PADDING_V, HOVER_BORDER, CONTROL_TRANSITION } from "./control_surface";
|
|
10
10
|
import { fontFamilyRegular, getInputTextStyle, getMultilineInputHeight, getTextColor, type TextColor } from "./text_utils";
|
|
11
11
|
import { getInteractionModality } from "./interaction_modality";
|
|
12
12
|
import { shouldOpenOnFocus, shouldRestoreFocusOnClose } from "./inline_focus";
|
|
@@ -653,11 +653,15 @@ const styles = StyleSheet.create({
|
|
|
653
653
|
borderColor: colors.border,
|
|
654
654
|
backgroundColor: colors.white,
|
|
655
655
|
paddingHorizontal: 8,
|
|
656
|
-
// Vertical padding
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
|
|
656
|
+
// Vertical padding binds on two kinds of content, never on a single line
|
|
657
|
+
// (minHeight governs there, so those rows render pixel-identical whatever
|
|
658
|
+
// this is): content TALLER than one line, which gets breathing room instead
|
|
659
|
+
// of touching the edges, and content of FIXED height — an avatar, a swatch —
|
|
660
|
+
// which this decides the field's whole height for. Hence the shared
|
|
661
|
+
// constant: an editor seats a `CONTROL_CONTENT_HEIGHT` node at exactly 40,
|
|
662
|
+
// the same as every other 40px surface, rather than at a number this file
|
|
663
|
+
// happened to choose.
|
|
664
|
+
paddingVertical: CONTROL_PADDING_V,
|
|
661
665
|
flexDirection: "row",
|
|
662
666
|
alignItems: "center",
|
|
663
667
|
gap: 6,
|
package/src/inline_slot.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StyleSheet, View, type StyleProp, type ViewStyle } from "react-native";
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
|
-
import { CONTROL_RADIUS } from "./control_surface";
|
|
3
|
+
import { CONTROL_PADDING_V, CONTROL_RADIUS } from "./control_surface";
|
|
4
4
|
import { INLINE_CONTROL_HEIGHT } from "./inline_edit";
|
|
5
5
|
|
|
6
6
|
export interface InlineSlotProps {
|
|
@@ -46,13 +46,16 @@ const styles = StyleSheet.create({
|
|
|
46
46
|
borderWidth: 1,
|
|
47
47
|
borderColor: "transparent",
|
|
48
48
|
paddingHorizontal: 8,
|
|
49
|
-
// The editors' vertical padding too, not just the horizontal
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
49
|
+
// The editors' vertical padding too, not just the horizontal — and it comes
|
|
50
|
+
// from the shared constant, NOT from the 8 the horizontal happens to use. A
|
|
51
|
+
// single line hides both the absence and a mismatch: the box is `minHeight`
|
|
52
|
+
// 40 and the text is ~20, so any padding from 0 to 10 lands on 40. It bites
|
|
53
|
+
// from the SECOND line on (an auto-growing input sizes to content PLUS its
|
|
54
|
+
// padding and border, so a value without this sits shorter than the input
|
|
55
|
+
// that replaces it) and on any FIXED-height content, where this slot's old
|
|
56
|
+
// hand-picked 8 made it 4px taller than the editor beside it for the very
|
|
57
|
+
// same child. Matching by coincidence at one line is not matching.
|
|
58
|
+
paddingVertical: CONTROL_PADDING_V,
|
|
56
59
|
justifyContent: "center",
|
|
57
60
|
},
|
|
58
61
|
});
|
package/src/member_chip.tsx
CHANGED
|
@@ -12,7 +12,8 @@ interface MemberChipProps {
|
|
|
12
12
|
image?: string | null;
|
|
13
13
|
/** Optional secondary line under the name — e.g. email, role, department. */
|
|
14
14
|
secondary?: string | null;
|
|
15
|
-
/** A rung on the shared avatar scale. Default `md
|
|
15
|
+
/** A rung on the shared avatar scale. Default `md`, which is sized to seat in a
|
|
16
|
+
* 40px control band; drop to `sm` for a dense cell. See `AVATAR_PX`. */
|
|
16
17
|
size?: AvatarSize;
|
|
17
18
|
style?: StyleProp<ViewStyle>;
|
|
18
19
|
}
|
|
@@ -26,6 +27,12 @@ interface MemberChipProps {
|
|
|
26
27
|
* Pure: pass the member's fields in (from `useMembers`, or a resolved
|
|
27
28
|
* `select_member` cell joined against that roster); this component fetches
|
|
28
29
|
* nothing and carries no domain types.
|
|
30
|
+
*
|
|
31
|
+
* A chip's usual home is a 40px CONTROL BAND — a picker option, a select's
|
|
32
|
+
* trigger, an inline editor at rest, a diff slot — and the default `md` is sized
|
|
33
|
+
* to seat in one (`CONTROL_CONTENT_HEIGHT`). That is not a coincidence to
|
|
34
|
+
* preserve by hand: it is why `md` is 28 rather than the 36 that used to grow
|
|
35
|
+
* those three surfaces to 46, 50 and 54 for the very same chip.
|
|
29
36
|
*/
|
|
30
37
|
export function MemberChip({ name, image, secondary, size = "md", style }: MemberChipProps) {
|
|
31
38
|
const displayName = name?.trim() || "Unknown";
|
package/src/select.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StyleSheet, View, type GestureResponderEvent, type StyleProp, type ViewStyle } from "react-native";
|
|
2
2
|
import { useState, useCallback, useMemo, type ReactNode, type Ref } from "react";
|
|
3
3
|
import { colors } from "./colors";
|
|
4
|
-
import { FOCUS_RING, CONTROL_RADIUS, HOVER_BORDER, CONTROL_TRANSITION } from "./control_surface";
|
|
4
|
+
import { FOCUS_RING, CONTROL_RADIUS, CONTROL_HEIGHT, CONTROL_PADDING_V, HOVER_BORDER, CONTROL_TRANSITION } from "./control_surface";
|
|
5
5
|
import { FocusRingPressable } from "./focus_ring_pressable";
|
|
6
6
|
import { Text } from "./text";
|
|
7
7
|
import { Popover, PopoverTrigger, PopoverContent } from "./popover";
|
|
@@ -293,11 +293,14 @@ const styles = StyleSheet.create({
|
|
|
293
293
|
flexDirection: "row",
|
|
294
294
|
alignItems: "center",
|
|
295
295
|
paddingHorizontal: 6,
|
|
296
|
-
|
|
296
|
+
// Shared, so a trigger seats a `CONTROL_CONTENT_HEIGHT` node at exactly
|
|
297
|
+
// `CONTROL_HEIGHT` — the hand-picked 4 here made this trigger 4px shorter
|
|
298
|
+
// than the inline editor holding the identical chip.
|
|
299
|
+
paddingVertical: CONTROL_PADDING_V,
|
|
297
300
|
borderWidth: 1,
|
|
298
301
|
borderColor: colors.border,
|
|
299
302
|
borderRadius: CONTROL_RADIUS,
|
|
300
|
-
minHeight:
|
|
303
|
+
minHeight: CONTROL_HEIGHT,
|
|
301
304
|
},
|
|
302
305
|
opened: {
|
|
303
306
|
// Mouse-opened, so the trigger never takes keyboard focus — wear the same 2px
|