@lotics/ui 42.4.0 → 43.1.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 +74 -0
- package/docs/catalog.md +38 -6
- package/docs/composition.md +45 -3
- package/docs/data_entry.md +9 -4
- package/package.json +12 -2
- package/src/back_button.tsx +4 -1
- package/src/badge.tsx +10 -2
- package/src/button.tsx +69 -8
- package/src/check_circle.tsx +1 -2
- package/src/checkbox_input.tsx +2 -2
- package/src/choice_list.tsx +2 -2
- package/src/color_tokens.ts +27 -3
- package/src/colors.web.ts +4 -2
- package/src/comments_button.tsx +3 -1
- package/src/control_surface.ts +27 -0
- package/src/data_grid.tsx +1 -1
- package/src/date_calendar.tsx +210 -58
- package/src/date_filter.tsx +1 -5
- package/src/date_picker.tsx +2 -0
- package/src/date_range_selection.ts +18 -0
- package/src/date_segments.ts +15 -1
- package/src/display_font.ts +27 -0
- package/src/display_font.web.ts +31 -0
- package/src/file_dropzone.tsx +2 -1
- package/src/file_row.tsx +2 -2
- package/src/file_thumbnail.tsx +4 -1
- package/src/filter_chip.tsx +12 -2
- package/src/focus_ring_pressable.tsx +6 -1
- package/src/font_family.ts +26 -0
- package/src/font_family.web.ts +29 -0
- package/src/icon_button.tsx +3 -1
- package/src/image_gallery.tsx +1 -1
- package/src/index.css +0 -2
- package/src/inline_button.tsx +3 -1
- package/src/inline_time_picker.tsx +97 -48
- package/src/list_item.tsx +109 -11
- package/src/locale.tsx +2 -2
- package/src/menu_button.tsx +19 -0
- package/src/option_list.tsx +8 -0
- package/src/pressable_highlight.tsx +20 -8
- package/src/pressable_row.tsx +7 -3
- package/src/scroll_to_bottom.tsx +3 -0
- package/src/slider.tsx +2 -2
- package/src/summary.tsx +28 -4
- package/src/switch.tsx +3 -1
- package/src/table.tsx +46 -4
- package/src/text.tsx +32 -11
- package/src/text_utils.ts +3 -11
- package/src/theme.web.tsx +16 -1
- package/src/theme_context.ts +63 -1
- package/src/time_columns.tsx +225 -0
- package/src/time_options.ts +138 -0
- package/src/time_picker.tsx +102 -64
- package/src/use_option_list.ts +19 -0
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,80 @@ 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.0.0 — a time is PICKED, and there is nothing to type
|
|
8
|
+
|
|
9
|
+
`TimePicker` is no longer a segmented text field. It is the value on a pressable
|
|
10
|
+
control — the bordered surface a `Select` uses — that opens an **hour / minute /
|
|
11
|
+
day-period** picker. `InlineTimePicker` is the same thing on the inline shell.
|
|
12
|
+
|
|
13
|
+
**Why the segments went.** A time was typed, with a picker bolted on as a small
|
|
14
|
+
glyph inside the field, and each half of that was wrong:
|
|
15
|
+
|
|
16
|
+
- **The glyph was undiscoverable, and worse than undiscoverable.** It sat exactly
|
|
17
|
+
where `DateField` puts a display-only calendar icon — same slot, same size,
|
|
18
|
+
same colour — so the one affordance for picking was indistinguishable from
|
|
19
|
+
decoration. A control nobody can identify is not a control.
|
|
20
|
+
- **A day period is not something to spell.** AM/PM is a choice between two named
|
|
21
|
+
things; asking someone to type "PM" is asking them to type an answer to a
|
|
22
|
+
yes/no question. It is now a column.
|
|
23
|
+
- **Composed times cannot cover the clock.** The old list offered whole times at
|
|
24
|
+
a fixed step, so anything between the rungs — a 13:07 cut-off — was reachable
|
|
25
|
+
only by typing, and the list had to splice odd values back in to stay honest.
|
|
26
|
+
Split by UNIT, every minute is offered and that whole class of special case is
|
|
27
|
+
gone.
|
|
28
|
+
|
|
29
|
+
The value is unchanged: canonical 24-hour `"HH:mm"`. The locale still decides 12-
|
|
30
|
+
vs 24-hour, and now also decides the SHAPE — a 24-hour locale gets two columns
|
|
31
|
+
and hours 00–23, a 12-hour locale gets a clock face starting at 12 plus the
|
|
32
|
+
period column. Tab reaches each column once and arrows move within it (roving
|
|
33
|
+
tabindex), so sixty minutes are not sixty tab stops.
|
|
34
|
+
|
|
35
|
+
**To migrate:**
|
|
36
|
+
|
|
37
|
+
- **`TimePicker` drops `onBlur`, `onEscape`, `onIncompleteChange`, `autoFocus`
|
|
38
|
+
and `segmentLabels`.** They all served typed entry. A pick is always a complete
|
|
39
|
+
time, so there is no half-entered state to report and nothing to cancel back
|
|
40
|
+
to; `placeholder` replaces them as the empty-state control. Delete the props —
|
|
41
|
+
the compiler will point at each one.
|
|
42
|
+
- **`InlineTimePicker` gains `locale` and drops `controls`.** It commits when the
|
|
43
|
+
popover closes, not on blur, so a `"buttons"` mode has nothing to confirm.
|
|
44
|
+
- **Add `chooseTime` to any complete `DatePickerLabels` you build** — it names the
|
|
45
|
+
control and doubles as the empty placeholder. A `labels` prop passed to
|
|
46
|
+
`DatePicker` is `Partial`, so only a hand-built full set breaks.
|
|
47
|
+
|
|
48
|
+
**Two kit-wide `OptionList` fixes ride along**, both pre-existing and both
|
|
49
|
+
invisible until a long list met them:
|
|
50
|
+
|
|
51
|
+
- **A chosen option carries `aria-selected`.** It had only the check GLYPH, so
|
|
52
|
+
which option was selected was information available to whoever could see the
|
|
53
|
+
row and to nobody else. It comes with `MenuButton`'s resting `zinc.100`
|
|
54
|
+
highlight — one weight above the `zinc.50` the keyboard/hover row takes — so
|
|
55
|
+
**every** `Select`, `Combobox` and option list now tints its selected row.
|
|
56
|
+
Nothing to change; expect the pixels to move.
|
|
57
|
+
- **An opening list scrolls to its seat.** The initial highlight was seated
|
|
58
|
+
without scrolling to it, so any list longer than its own box opened at the top
|
|
59
|
+
with the current value below the fold.
|
|
60
|
+
|
|
61
|
+
`MenuButton` gains `tabIndex` + `onKeyDown` so a listbox row can hold a roving
|
|
62
|
+
tab stop. Additive.
|
|
63
|
+
|
|
64
|
+
**A pressable `ListItem` presses BESIDE its `right` slot.** The slot is documented as holding
|
|
65
|
+
a control, and a button may not contain one: `<button>` inside `<button>` is invalid HTML and
|
|
66
|
+
buries a second tab stop and a second accessible name inside one control. The anatomy carries
|
|
67
|
+
this rather than a prop opting into safety, so every call site already passing a control there
|
|
68
|
+
is fixed without being touched, and it applies whether or not a slot is passed — `right` is
|
|
69
|
+
routinely conditional, and an anatomy that flipped on that would make two rows in one list
|
|
70
|
+
press differently. The one visible consequence: a DECORATIVE `right` (a badge, a chevron) is no
|
|
71
|
+
longer part of the press target — the row still presses everywhere else.
|
|
72
|
+
|
|
73
|
+
**The pointer cursor is now reserved for controls that ACT** — buttons, links, disclosures.
|
|
74
|
+
Rows, menu options, cards, tabs, field triggers, checkboxes, switches, chips and calendar
|
|
75
|
+
days all show the arrow. No API changes; every affected component is one you already use, so
|
|
76
|
+
the only thing to expect is that the cursor stops changing over most of a screen. The rule and
|
|
77
|
+
its reasoning are in `docs/composition.md` § The pointer cursor. If a component of your own
|
|
78
|
+
needs the pointer, set `CURSOR_ACTION` in its style — and if it is built on the bare
|
|
79
|
+
react-native `Pressable`, note that the pointer is what you get by DEFAULT, from
|
|
80
|
+
react-native-web, unless you set `CURSOR_DEFAULT`.
|
|
7
81
|
## 42.4.0 — `Picker` is named by its `FormField`, and its way back to empty has a name
|
|
8
82
|
|
|
9
83
|
Two fixes to the same control, both about a thing on screen that the control did
|
package/docs/catalog.md
CHANGED
|
@@ -788,7 +788,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
788
788
|
shell-owned `actions`).
|
|
789
789
|
- **`option_list`** — `OptionList`: the ONE shared searchable listbox body every selector
|
|
790
790
|
opens — single/multi, optional internal search, create row, keyboard + native-`<select>`
|
|
791
|
-
typeahead; host it directly in a `Popover`/`Dialog` for a command palette.
|
|
791
|
+
typeahead; host it directly in a `Popover`/`Dialog` for a command palette. A chosen row is
|
|
792
|
+
`selected`: the kit's `zinc.100` resting highlight AND `aria-selected`, one weight above the
|
|
793
|
+
`zinc.50` the keyboard/hover row takes — a check glyph alone said nothing to a screen reader.
|
|
794
|
+
It opens SCROLLED to its seat (the selected row in `search.mode:"none"`, else the first real
|
|
795
|
+
option), which only shows on a list longer than its own box and is the difference between a
|
|
796
|
+
96-row list opening on your value and opening at the top.
|
|
792
797
|
- **`combobox`** — COMPOUND single-select editable search: `Combobox` root +
|
|
793
798
|
`ComboboxInput` + `ComboboxContent`, optional `ComboboxEmpty`/`ComboboxFooter`,
|
|
794
799
|
`useCombobox()`; over the shared option-list engine; browses on focus; no `multi` —
|
|
@@ -865,6 +870,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
865
870
|
localized weekday/month names via BCP-47 `locale`, `firstDayOfWeek` (default Monday),
|
|
866
871
|
`ref.navigateToMonth`. The engine `DatePicker`/`DateFilter` wrap in field chrome — reach
|
|
867
872
|
for it bare only when the calendar lives permanently on the surface, not behind a field.
|
|
873
|
+
While a range is OPEN (a start, no end), hovering a day fills the span it would close on,
|
|
874
|
+
across both months — including the day under the pointer, which a committed range draws
|
|
875
|
+
dark and a previewed one has nothing else to mark. It shares its open-ness test with the
|
|
876
|
+
click (`previewEndFor` beside `nextRangeSelection`), so the span shown is always the span
|
|
877
|
+
the next click produces; hovering backwards from the start previews just as well.
|
|
868
878
|
- **`date_filter`** — `DateFilter`: the date+time period filter panel — presets
|
|
869
879
|
(`PresetId`), calendar, optional time segments; the body `DateRangeFilterField` opens.
|
|
870
880
|
**Every preset SETS a range** — the list holds no mode and no null case, so a hand-picked
|
|
@@ -880,10 +890,25 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
880
890
|
end that is still open.
|
|
881
891
|
- **`date_range_filter_field`** — `DateRangeFilterField`: the register's period filter field
|
|
882
892
|
(presets + footer + time-segment a11y; localized via the `dateRange` locale slice).
|
|
883
|
-
- **`time_picker`** — `TimePicker`: the time-of-day
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
893
|
+
- **`time_picker`** — `TimePicker`: the time-of-day FIELD. The value on a pressable
|
|
894
|
+
control (the bordered surface a `Select` uses, so it reads as pickable by the same
|
|
895
|
+
shape as every other field that opens something), and `TimeColumns` behind it.
|
|
896
|
+
**No text entry** — a glyph inside a field is indistinguishable from `DateField`'s
|
|
897
|
+
display-only calendar icon, and a day period is a choice between two named things
|
|
898
|
+
rather than a word to spell. `placeholder` covers the empty state (defaults to the
|
|
899
|
+
`chooseTime` label). The value is always canonical 24-hour `"HH:mm"`; `locale`
|
|
900
|
+
decides what is displayed AND the picker's shape.
|
|
901
|
+
- **`time_columns`** — `TimeColumns`: the picking face — hour, minute, and a day
|
|
902
|
+
period **only where the locale uses one** (a 24-hour locale gets two columns and
|
|
903
|
+
hours 00–23). Every minute is offered, so no time is unreachable and no step has
|
|
904
|
+
to be chosen; each column commits on its own, so the value is always a real time
|
|
905
|
+
and there is no draft to confirm. Roving tabindex per column — Tab reaches a
|
|
906
|
+
column once, arrows move within it, and selection follows focus.
|
|
907
|
+
- **`time_options`** — the columns' vocabulary, RN-free and pure: `decomposeTime` /
|
|
908
|
+
`composeTime` (the split into `displayHour` / `minute` / `pm` and back — a
|
|
909
|
+
12-hour locale's displayed hour means two different times and only the period
|
|
910
|
+
says which, which is why composing takes both), `hourOptions`, `minuteOptions`,
|
|
911
|
+
`dayPeriodOptions`, `dayPeriodLabels`, `formatTimeOfDay`, `timeToMinuteOfDay`.
|
|
887
912
|
- **`deadline`** — the COUNTDOWN vocabulary, RN-free and pure: `daysUntil`, `countdownLabel`,
|
|
888
913
|
`deadlineTone` / `deadlineColor`, `deadlineAnnotation`, `nearestDeadline` (+ `DeadlineLabels`,
|
|
889
914
|
`DeadlineThresholds`, `Deadline`). Three contracts a screen must not invent for itself — how
|
|
@@ -1172,7 +1197,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1172
1197
|
### Lists, tables & registers
|
|
1173
1198
|
|
|
1174
1199
|
- **`list`** — `List`: children separated by hairline `Divider`s.
|
|
1175
|
-
- **`list_item`** — `ListItem`: the plain list row.
|
|
1200
|
+
- **`list_item`** — `ListItem`: the plain list row. A pressable row with a `right` slot
|
|
1201
|
+
presses BESIDE it, never around it: the slot holds a control, and a button may not contain
|
|
1202
|
+
one — `<button>` inside `<button>` is invalid HTML and buries a second tab stop and name
|
|
1203
|
+
inside one control. So the surface is a wrapper, the press target is `left` + the text, and
|
|
1204
|
+
the slot sits outside it (the anatomy `InlineEditView`'s `actions` uses). ALWAYS, not only
|
|
1205
|
+
when a slot is passed: `right` is routinely conditional, and an anatomy that flipped on that
|
|
1206
|
+
would make two rows in one list press differently. Nothing to opt into — a decorative
|
|
1207
|
+
`right` simply stops being part of the press target.
|
|
1176
1208
|
- **`table`** — `Table` + `TableRow` + `TableCell`: the paginated high-volume register
|
|
1177
1209
|
(columns defined once, `sortLabels` localizable; rows are `PressableRow`-based).
|
|
1178
1210
|
CONTAINER-RESPONSIVE with no prop (measures itself, like `Breakdown`/`DetailTable`): when
|
package/docs/composition.md
CHANGED
|
@@ -396,7 +396,7 @@ shows a zinc-100 highlight; `marked` (a bulk-ticked row) a resting blue-50 tint.
|
|
|
396
396
|
band + every row's cell widths derive from it and can't drift. ONE hairline under the header; the
|
|
397
397
|
rows below it Divider-separated. A sortable column shows its `SortHeader` glyph ALWAYS (a neutral
|
|
398
398
|
up/down glyph when unsorted) so it reads as sortable; pressing cycles none → asc → desc → none.
|
|
399
|
-
A `TableRow` with no `onPress` renders a STATIC read-only row (no hover wash
|
|
399
|
+
A `TableRow` with no `onPress` renders a STATIC read-only row (no hover wash) —
|
|
400
400
|
so `Table` also serves read-only tabular data (a fee breakdown, a spec sheet), not just interactive
|
|
401
401
|
registers. A pressable `TableRow` REQUIRES `accessibilityLabel` ("Open …") — its keyboard door is
|
|
402
402
|
an empty overlay with no content to derive a name from.
|
|
@@ -966,6 +966,26 @@ gate's SCOPE, once** — never prose beside the button, never revealed only on p
|
|
|
966
966
|
a mix of "Đổi"/"Gỡ"): with a clearable link, changing = remove → the empty-state picker
|
|
967
967
|
re-prompts, so the destructive verb covers both required anchors and optional adds.
|
|
968
968
|
|
|
969
|
+
## The pointer cursor
|
|
970
|
+
|
|
971
|
+
**Only a control that ACTS ON ITS OWN shows a pointer** — a button, a link, a disclosure.
|
|
972
|
+
Everything else shows the arrow: a row, a menu option, a card, a tab, a field trigger, a
|
|
973
|
+
checkbox, a switch, a chip. The line is not "is it pressable" (nearly everything is) but
|
|
974
|
+
"does pressing it DO something, or does it set a value / open the thing you are already
|
|
975
|
+
looking at".
|
|
976
|
+
|
|
977
|
+
Drawn tightly because a signal on half the screen is not a signal. A pointer everywhere says
|
|
978
|
+
only "this is a web page"; reserved, it marks the few things that act. What carries "you can
|
|
979
|
+
press this" elsewhere is the hover treatment the surface already owns — a row's wash, a
|
|
980
|
+
field's border, a chip's fill — which is what a native list or menu uses too, where the
|
|
981
|
+
cursor stays an arrow throughout.
|
|
982
|
+
|
|
983
|
+
The arrow is never the default and always has to be asked for: react-native-web ships
|
|
984
|
+
`cursor: pointer` inside `Pressable`'s own base style, so a component that says nothing gets
|
|
985
|
+
a pointer. `PressableHighlight` and `FocusRingPressable` set the arrow for everything built
|
|
986
|
+
on them (`CURSOR_DEFAULT`); a component on the bare `Pressable` sets it itself, and the few
|
|
987
|
+
that act set `CURSOR_ACTION` back over the top.
|
|
988
|
+
|
|
969
989
|
## Color discipline — solid / tint / ramp + ONE accent
|
|
970
990
|
|
|
971
991
|
Every status / data-viz / accent color comes from a NAMED helper (`@lotics/ui/colors`), never a
|
|
@@ -1013,8 +1033,30 @@ When it IS a status, match weight to prominence — lightest → heaviest:
|
|
|
1013
1033
|
a surface (a drawer/detail header). Opt IN explicitly and sparingly; one per view, never
|
|
1014
1034
|
sprinkled through rows.
|
|
1015
1035
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1036
|
+
**`tonal` IS allowed on a register's ONE primary status column, and that is the rule this
|
|
1037
|
+
paragraph used to get wrong.** It read "a register's dense rows read lighter, the row's primary
|
|
1038
|
+
status is `dot`" while `badge.tsx` named "a register's primary Status column" as a legitimate
|
|
1039
|
+
tonal use — two docs, opposite answers, so the choice fell to whoever read which. What "one per
|
|
1040
|
+
view" governs is the number of badge KINDS on a surface, not the number of rows: badging the
|
|
1041
|
+
stage AND the type AND the city is the clutter the rule exists to stop; one status column that
|
|
1042
|
+
happens to render 133 times is one badge.
|
|
1043
|
+
|
|
1044
|
+
Prefer `dot` when the column is secondary or the row already carries a coloured mark. Reach for
|
|
1045
|
+
`tonal` when the status column is what the register is SCANNED by — the tints are the palette's
|
|
1046
|
+
50-step against its 900-step ink (measured 8.7:1 and 9.5:1, so roughly double the AA floor), and
|
|
1047
|
+
a column of them is the difference between a register that reads as organised and one that reads
|
|
1048
|
+
as a grey spreadsheet. That difference is the commonest "it looks bland" report, and it has a
|
|
1049
|
+
real answer that costs no meaning: make the colour the screen ALREADY earns more present, rather
|
|
1050
|
+
than adding colour somewhere that has to invent a meaning to justify it.
|
|
1051
|
+
|
|
1052
|
+
**A `Badge` in a table cell needs `alignSelf`.** A cell stretches its child on the cross axis, and
|
|
1053
|
+
a `dot` has no ground so nothing shows — swap the variant to `tonal` and the pill becomes a filled
|
|
1054
|
+
block the full width of the column, which reads as a cell state rather than a chip. The kit cannot
|
|
1055
|
+
fix this on the badge: `alignSelf: "flex-start"` means LEFT inside a column-direction cell and TOP
|
|
1056
|
+
inside a row-direction one, so setting it globally would break every badge sitting inline beside
|
|
1057
|
+
text. Pass it at the call site.
|
|
1058
|
+
|
|
1059
|
+
A `Badge` is never a metric value.
|
|
1018
1060
|
|
|
1019
1061
|
## Typography
|
|
1020
1062
|
|
package/docs/data_entry.md
CHANGED
|
@@ -123,7 +123,7 @@ dialog save, a debounced autosave).
|
|
|
123
123
|
### Keyboard entry — type → Tab → type
|
|
124
124
|
|
|
125
125
|
Bulk entry never needs the mouse. KEYBOARD focus (Tab / Shift+Tab) landing on a closed
|
|
126
|
-
SWAP editor — `InlineNumberInput`, `
|
|
126
|
+
SWAP editor — `InlineNumberInput`, `InlineDatePicker` — opens edit mode
|
|
127
127
|
immediately with the input focused; commit-on-blur then makes Tab itself the commit, so the chain
|
|
128
128
|
is type → Tab → type with the next editor already open. Pointer focus never auto-opens (mousedown
|
|
129
129
|
records "pointer" modality before focus fires — `interaction_modality.ts`), so the click path is
|
|
@@ -172,21 +172,26 @@ One per type:
|
|
|
172
172
|
- **`InlineDatePicker`** — `format="datetime"` for always-on time; `optionalTime` to let the user
|
|
173
173
|
ADD/REMOVE a time — the value's own shape, date vs datetime, is the source of truth
|
|
174
174
|
(`optionalTime` is ignored when `format="datetime"`).
|
|
175
|
-
- **`InlineTimePicker`** — a time-only value
|
|
175
|
+
- **`InlineTimePicker`** — a time-only value, PICKED: the value at rest, the hour /
|
|
176
|
+
minute / period columns in a popover anchored to the field, committing when the
|
|
177
|
+
popover closes. On the POPOVER shell beside `InlineDatePicker`, not the typed one
|
|
178
|
+
— `useInlineEdit` commits when the field blurs, and a picker cannot live there,
|
|
179
|
+
because opening one moves focus into it, which reads as a blur and unmounts the
|
|
180
|
+
editor before the picker can paint.
|
|
176
181
|
- **`InlineSelect multi`** — the MULTI mode of `InlineSelect` (there's no separate tag component):
|
|
177
182
|
a tag SET (`value: T[]`), selected tags render as badges inside the standard chip, clicking floats
|
|
178
183
|
a multi `OptionList` (checkbox rows), CLOSING commits the new set in one `onSave` — never a borderless `Select` posing as an inline
|
|
179
184
|
field.
|
|
180
185
|
|
|
181
186
|
They split across two shared shells. The same-height **typed** editors (`InlineTextInput`,
|
|
182
|
-
`InlineNumberInput
|
|
187
|
+
`InlineNumberInput`) render **`InlineEditFrame`** driven by the
|
|
183
188
|
**`useInlineEdit`** hook (the draft buffer, async save, and — for the two that swap — the view⇄edit
|
|
184
189
|
toggle) — pass a custom input to that pair to join the family. On the frame, **`editing` means an
|
|
185
190
|
input is MOUNTED** (which element renders, and whether the surface drops its resting padding to
|
|
186
191
|
host one); an always-mounted editor passes it constant and passes the real state as **`editOpen`**,
|
|
187
192
|
which the ✓/✕ verbs read — they assert "there is an edit to accept or reject", so on a resting
|
|
188
193
|
field they would be both a false claim and dead buttons. The **popover** editors (`InlineSelect`,
|
|
189
|
-
`InlineMemberSelect`, `InlineDatePicker`) instead compose **`InlineEditView`**
|
|
194
|
+
`InlineMemberSelect`, `InlineDatePicker`, `InlineTimePicker`) instead compose **`InlineEditView`**
|
|
190
195
|
(the resting box, wired as the popover trigger) with their OWN `useState` for open/draft/saving/
|
|
191
196
|
error — a pick, not a keystroke, is the edit, so `useInlineEdit` doesn't fit. `onSave` is async
|
|
192
197
|
either way: the saving spinner sits INSIDE the control at its right edge (never a sibling — that
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "43.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./vite": {
|
|
@@ -244,6 +244,8 @@
|
|
|
244
244
|
"./date_filter": "./src/date_filter.tsx",
|
|
245
245
|
"./date_range_filter_field": "./src/date_range_filter_field.tsx",
|
|
246
246
|
"./time_picker": "./src/time_picker.tsx",
|
|
247
|
+
"./time_options": "./src/time_options.ts",
|
|
248
|
+
"./time_columns": "./src/time_columns.tsx",
|
|
247
249
|
"./date_calendar": "./src/date_calendar.tsx",
|
|
248
250
|
"./dialog": "./src/dialog.tsx",
|
|
249
251
|
"./drawer": "./src/drawer.tsx",
|
|
@@ -275,7 +277,15 @@
|
|
|
275
277
|
"./avatar_size": "./src/avatar_size.ts",
|
|
276
278
|
"./diff_value": "./src/diff_value.tsx",
|
|
277
279
|
"./diff_mark": "./src/diff_mark.tsx",
|
|
278
|
-
"./use_change_set": "./src/use_change_set.ts"
|
|
280
|
+
"./use_change_set": "./src/use_change_set.ts",
|
|
281
|
+
"./display_font": {
|
|
282
|
+
"react-native": "./src/display_font.ts",
|
|
283
|
+
"default": "./src/display_font.web.ts"
|
|
284
|
+
},
|
|
285
|
+
"./font_family": {
|
|
286
|
+
"react-native": "./src/font_family.ts",
|
|
287
|
+
"default": "./src/font_family.web.ts"
|
|
288
|
+
}
|
|
279
289
|
},
|
|
280
290
|
"files": [
|
|
281
291
|
"src",
|
package/src/back_button.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { View } from "react-native";
|
|
|
3
3
|
import { Icon } from "./icon";
|
|
4
4
|
import { Text } from "./text";
|
|
5
5
|
import { colors } from "./colors";
|
|
6
|
+
import { CURSOR_ACTION } from "./control_surface";
|
|
6
7
|
import { PressableHighlight } from "./pressable_highlight";
|
|
7
8
|
import { useLoticsLocale } from "./locale";
|
|
8
9
|
|
|
@@ -44,7 +45,9 @@ export function BackButton(props: BackButtonProps) {
|
|
|
44
45
|
accessibilityLabel={accessibilityLabel}
|
|
45
46
|
userSelect="none"
|
|
46
47
|
onPress={onPress}
|
|
47
|
-
|
|
48
|
+
// Going back ACTS, exactly as the bare variant's `IconButton` does —
|
|
49
|
+
// without this, one component shows two cursors decided by a label.
|
|
50
|
+
style={{ cursor: CURSOR_ACTION, flexDirection: "row", alignItems: "center", gap: 8, alignSelf: "flex-start", borderRadius: 999, paddingRight: 14 }}
|
|
48
51
|
>
|
|
49
52
|
<View style={{ width: 40, height: 40, borderRadius: 999, backgroundColor: colors.zinc[100], alignItems: "center", justifyContent: "center" }}>
|
|
50
53
|
<Icon name="chevron-left" size={20} color={colors.zinc[900]} />
|
package/src/badge.tsx
CHANGED
|
@@ -15,8 +15,16 @@ interface BadgeProps {
|
|
|
15
15
|
* Light enough to sit anywhere without shouting. Reach for this by default.
|
|
16
16
|
* - "tonal": a filled pill — HEAVY, and easy to overuse into clutter. Reserve
|
|
17
17
|
* it for the ONE prominent status of a surface (a drawer header, a register's
|
|
18
|
-
* primary Status column). One per view
|
|
19
|
-
*
|
|
18
|
+
* primary Status column). "One per view" counts badge KINDS, not rows: one
|
|
19
|
+
* status column rendering on every row is ONE badge, while the stage plus the
|
|
20
|
+
* type plus the city is the clutter this warns about. Not for option lists.
|
|
21
|
+
* If in doubt, use `dot`.
|
|
22
|
+
*
|
|
23
|
+
* In a TABLE CELL, pass `style={{ alignSelf: "flex-start" }}`: a cell
|
|
24
|
+
* stretches its child, so a tonal pill fills the whole column otherwise. It
|
|
25
|
+
* cannot be defaulted here — `flex-start` means left in a column-direction
|
|
26
|
+
* parent and TOP in a row-direction one, which would misalign every badge
|
|
27
|
+
* sitting inline beside text.
|
|
20
28
|
* (StatusGrid/StatusLegend keep their own dot — raw hex coupled to the grid
|
|
21
29
|
* cells' tint, a separate data-viz concern.)
|
|
22
30
|
*/
|
package/src/button.tsx
CHANGED
|
@@ -27,6 +27,35 @@ interface ButtonPropsBase {
|
|
|
27
27
|
icon?: IconName;
|
|
28
28
|
alignSelf?: "flex-start" | "flex-end" | "center" | "stretch" | "auto";
|
|
29
29
|
color?: ButtonColor;
|
|
30
|
+
/**
|
|
31
|
+
* An action whose DESTINATION carries a brand of its own — an export that
|
|
32
|
+
* produces a spreadsheet, a sign-in with a named provider, a share into a
|
|
33
|
+
* service. Pass that brand's colour and the button wears it: a wash of it as
|
|
34
|
+
* the ground, the colour itself on the label and the mark.
|
|
35
|
+
*
|
|
36
|
+
* **Why this exists rather than a `style` escape hatch.** `ButtonColor` is a
|
|
37
|
+
* closed set on purpose — primary / secondary / danger are the kit's OWN
|
|
38
|
+
* vocabulary, and keeping it small is what lets a reader learn the ladder once.
|
|
39
|
+
* An outside brand genuinely is not on that ladder: it is not a status, not a
|
|
40
|
+
* valence, and not a palette family, so no named variant could ever cover it.
|
|
41
|
+
* The answer is a named CATEGORY with a colour slot, never a hole through
|
|
42
|
+
* which any call site can restyle any button — that is how a design system
|
|
43
|
+
* stops being one.
|
|
44
|
+
*
|
|
45
|
+
* It renders SOLID, with white ink — which is what a brand action looks like
|
|
46
|
+
* everywhere it appears, and what makes the colour recognisable at a glance
|
|
47
|
+
* rather than merely present. A tonal wash was tried first, on the reasoning
|
|
48
|
+
* that a second filled control competes with the surface's one primary; it
|
|
49
|
+
* reads as washed-out rather than restrained, and the hierarchy survives
|
|
50
|
+
* anyway because the primary keeps the last position and the affirmative verb.
|
|
51
|
+
*
|
|
52
|
+
* The cost is real and worth naming: a screen with several of these is a screen
|
|
53
|
+
* with no primary. Reach for it where a destination genuinely has a brand, not
|
|
54
|
+
* to make a button louder.
|
|
55
|
+
*
|
|
56
|
+
* Ignored on `primary` and `danger`, whose own colours are load-bearing.
|
|
57
|
+
*/
|
|
58
|
+
brandColor?: string;
|
|
30
59
|
loading?: boolean;
|
|
31
60
|
disabled?: boolean;
|
|
32
61
|
tooltip?: string | UseTooltipOptions;
|
|
@@ -59,11 +88,15 @@ export function Button(props: ButtonProps) {
|
|
|
59
88
|
onPress,
|
|
60
89
|
testID,
|
|
61
90
|
accessibilityLabel,
|
|
91
|
+
brandColor,
|
|
62
92
|
} = props;
|
|
63
93
|
|
|
64
94
|
// The press waits out an inline commit the same gesture started (pending_commits.ts)
|
|
65
95
|
// and reports busy while it does — so `busy`, not the `loading` prop alone, drives the
|
|
66
96
|
// spinner, aria-busy, and the double-press block.
|
|
97
|
+
// Ignored on the variants whose colour MEANS something: a primary that is not
|
|
98
|
+
// the surface's primary colour, or a danger that is not red, would be lying.
|
|
99
|
+
const brand = color === "primary" || color === "danger" || color === "danger-secondary" ? undefined : brandColor;
|
|
67
100
|
const { handlePress, waiting } = useGatedPress<NativeSyntheticEvent<any>>(onPress);
|
|
68
101
|
const busy = loading || waiting;
|
|
69
102
|
const disabledOrLoading = disabled || busy;
|
|
@@ -77,14 +110,16 @@ export function Button(props: ButtonProps) {
|
|
|
77
110
|
) : (
|
|
78
111
|
<>
|
|
79
112
|
{!!icon && (
|
|
80
|
-
<Icon size={20} name={icon} color={getButtonIconColor(color, disabledOrLoading)} />
|
|
113
|
+
<Icon size={20} name={icon} color={brand && !disabledOrLoading ? colors.white : getButtonIconColor(color, disabledOrLoading)} />
|
|
81
114
|
)}
|
|
82
115
|
{!!title && (
|
|
83
116
|
<Text
|
|
84
117
|
numberOfLines={1}
|
|
85
118
|
size="sm"
|
|
86
119
|
weight="medium"
|
|
87
|
-
|
|
120
|
+
// `inverted` — the kit's own white-on-dark ink, the same token the primary
|
|
121
|
+
// uses, so a brand button and the primary read as one family.
|
|
122
|
+
color={brand && !disabledOrLoading ? "inverted" : getButtonTextColor(color, disabledOrLoading)}
|
|
88
123
|
userSelect="none"
|
|
89
124
|
>
|
|
90
125
|
{title}
|
|
@@ -116,11 +151,16 @@ export function Button(props: ButtonProps) {
|
|
|
116
151
|
borderRadius: CONTROL_RADIUS,
|
|
117
152
|
backgroundColor: disabled
|
|
118
153
|
? getButtonDisabledBackgroundColor(color)
|
|
119
|
-
:
|
|
120
|
-
?
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
:
|
|
154
|
+
: brand
|
|
155
|
+
? // Solid, darkening under the pointer — the same depth ladder the
|
|
156
|
+
// primary walks, so a brand button behaves like a button rather
|
|
157
|
+
// than like a coloured label.
|
|
158
|
+
shade(brand, pressed ? 0.78 : hovered ? 0.88 : 1)
|
|
159
|
+
: pressed
|
|
160
|
+
? getButtonPressedColor(color)
|
|
161
|
+
: hovered
|
|
162
|
+
? getButtonHoverColor(color)
|
|
163
|
+
: getButtonBackgroundColor(color),
|
|
124
164
|
},
|
|
125
165
|
// Subtle depth on the primary: a soft drop shadow lifts it off the
|
|
126
166
|
// surface and a whisper of top highlight + a gentle vertical shade (the
|
|
@@ -169,10 +209,31 @@ export function getButtonTextColor(color?: ButtonColor, disabled?: boolean): Tex
|
|
|
169
209
|
}
|
|
170
210
|
}
|
|
171
211
|
|
|
212
|
+
/**
|
|
213
|
+
* A brand colour at `factor` of its own lightness — the hover/press steps for a
|
|
214
|
+
* solid brand button.
|
|
215
|
+
*
|
|
216
|
+
* Multiplying the channels rather than blending toward black keeps the hue: a
|
|
217
|
+
* dark green stays green as it darkens, where mixing with black walks it toward
|
|
218
|
+
* grey and the recognition goes with it. Returns the input unchanged for a value
|
|
219
|
+
* it cannot parse, so an unusual colour loses its pointer feedback rather than
|
|
220
|
+
* rendering as something else entirely.
|
|
221
|
+
*/
|
|
222
|
+
function shade(hex: string, factor: number): string {
|
|
223
|
+
const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
|
|
224
|
+
if (!m || factor === 1) return hex;
|
|
225
|
+
const n = parseInt(m[1], 16);
|
|
226
|
+
const ch = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((v) => Math.round(v * factor));
|
|
227
|
+
return `rgb(${ch[0]}, ${ch[1]}, ${ch[2]})`;
|
|
228
|
+
}
|
|
229
|
+
|
|
172
230
|
function getButtonBackgroundColor(color?: ButtonColor) {
|
|
173
231
|
switch (color) {
|
|
174
232
|
case "primary":
|
|
175
|
-
|
|
233
|
+
// The themeable role, whose DEFAULT is this exact near-black — so an app
|
|
234
|
+
// that sets no theme renders identical pixels, and one that sets `primary`
|
|
235
|
+
// puts its brand on the surface's single filled button.
|
|
236
|
+
return colors.primary;
|
|
176
237
|
case "secondary":
|
|
177
238
|
return colors.zinc["100"];
|
|
178
239
|
case "danger":
|
package/src/check_circle.tsx
CHANGED
package/src/checkbox_input.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
2
|
import { Pressable } from "react-native";
|
|
3
3
|
import { Checkbox } from "./checkbox";
|
|
4
|
-
import { FOCUS_RING } from "./control_surface";
|
|
4
|
+
import { CURSOR_DEFAULT, FOCUS_RING } from "./control_surface";
|
|
5
5
|
import { useFocusRing } from "./use_focus_ring";
|
|
6
6
|
interface CheckboxInputProps {
|
|
7
7
|
/**
|
|
@@ -32,7 +32,7 @@ export function CheckboxInput(props: CheckboxInputProps) {
|
|
|
32
32
|
onPress={handlePress}
|
|
33
33
|
disabled={disabled}
|
|
34
34
|
{...focusProps}
|
|
35
|
-
style={[{ opacity: disabled ? 0.5 : 1, borderRadius: 6 }, focusVisible && { boxShadow: FOCUS_RING }]}
|
|
35
|
+
style={[{ opacity: disabled ? 0.5 : 1, borderRadius: 6, cursor: CURSOR_DEFAULT }, focusVisible && { boxShadow: FOCUS_RING }]}
|
|
36
36
|
// Use the W3C ARIA props directly: RN-web 0.21 no longer maps
|
|
37
37
|
// `accessibilityState={{ checked }}` to `aria-checked`, which dropped the
|
|
38
38
|
// checked state from the accessibility tree (a role="checkbox" with no
|
package/src/choice_list.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import { Icon } from "./icon";
|
|
|
6
6
|
import { Divider } from "./divider";
|
|
7
7
|
import { composeHandler, useFocusRing } from "./use_focus_ring";
|
|
8
8
|
import { useHover } from "./use_hover";
|
|
9
|
-
import { CONTROL_RADIUS, FOCUS_RING, ROW_WASH_BLEED } from "./control_surface";
|
|
9
|
+
import { CONTROL_RADIUS, CURSOR_DEFAULT, FOCUS_RING, ROW_WASH_BLEED } from "./control_surface";
|
|
10
10
|
import { useAutoGrowHeight } from "./use_auto_grow_height";
|
|
11
11
|
import { fontFamilyMedium, getInputTextStyle } from "./text_utils";
|
|
12
12
|
import { useLoticsLocale } from "./locale";
|
|
@@ -55,7 +55,7 @@ function ChoiceRow({ option, selected, onSelect }: { option: ChoiceOption; selec
|
|
|
55
55
|
// Bleeds like every other washed row, so the option TEXT sits on the
|
|
56
56
|
// container's edge — level with the question above it, which no longer
|
|
57
57
|
// has to hand-pad itself to match.
|
|
58
|
-
{ flexDirection: "row", alignItems: "center", gap: 12, paddingVertical: 14, paddingHorizontal: ROW_WASH_BLEED, marginHorizontal: -ROW_WASH_BLEED, borderRadius: CONTROL_RADIUS },
|
|
58
|
+
{ cursor: CURSOR_DEFAULT, flexDirection: "row", alignItems: "center", gap: 12, paddingVertical: 14, paddingHorizontal: ROW_WASH_BLEED, marginHorizontal: -ROW_WASH_BLEED, borderRadius: CONTROL_RADIUS },
|
|
59
59
|
// Selected reads as a persistent tint (zinc-100); an unselected row only
|
|
60
60
|
// washes on hover (zinc-50). The check below reserves its slot always.
|
|
61
61
|
selected ? { backgroundColor: colors.zinc[100] } : hovered || pressed ? { backgroundColor: colors.zinc[50] } : null,
|
package/src/color_tokens.ts
CHANGED
|
@@ -292,8 +292,8 @@ const palette = {
|
|
|
292
292
|
};
|
|
293
293
|
|
|
294
294
|
/**
|
|
295
|
-
* THE
|
|
296
|
-
* deliberately nothing else. See `colors.web.ts`, which redefines exactly
|
|
295
|
+
* THE FOUR THEMEABLE ROLES are `background`, `border`, `accent` and `primary` —
|
|
296
|
+
* and deliberately nothing else. See `colors.web.ts`, which redefines exactly
|
|
297
297
|
* these as CSS variables so an app can carry its own identity.
|
|
298
298
|
*
|
|
299
299
|
* The line is drawn at MEANING, not at convenience. A palette family
|
|
@@ -327,6 +327,30 @@ export const colors = {
|
|
|
327
327
|
* a palette family, so an accent can never overwrite a meaning.
|
|
328
328
|
*/
|
|
329
329
|
accent: palette.blue["600"],
|
|
330
|
+
/**
|
|
331
|
+
* The PRIMARY action's fill — the one filled button on a surface.
|
|
332
|
+
*
|
|
333
|
+
* Separate from `accent` because their defaults differ and so do their jobs: a
|
|
334
|
+
* brand can own the disc a person is drawn as without owning the shape of a
|
|
335
|
+
* commit, and near-black is a deliberate neutral that works under any brand.
|
|
336
|
+
* Folding them would force an app that themes its avatars to also repaint every
|
|
337
|
+
* CTA, and would turn this near-black into blue for every app that themes
|
|
338
|
+
* nothing.
|
|
339
|
+
*/
|
|
340
|
+
primary: palette.zinc["900"],
|
|
341
|
+
/**
|
|
342
|
+
* The brand's TINT — a row's hover, an active filter's ground, a header band.
|
|
343
|
+
*
|
|
344
|
+
* It exists as its own token because it cannot be derived where it is used:
|
|
345
|
+
* `withAlpha`/`tint` do string surgery on an `rgba()`, and on web `accent` is a
|
|
346
|
+
* `var()`, so asking for "accent at 7%" at the call site produces garbage no
|
|
347
|
+
* type would catch. `LoticsThemeProvider` computes it instead, from the literal
|
|
348
|
+
* hex the app handed it — derivation happens where the literal still exists.
|
|
349
|
+
*
|
|
350
|
+
* The default is the neutral wash these surfaces already wore, so an app that
|
|
351
|
+
* themes nothing is pixel-identical.
|
|
352
|
+
*/
|
|
353
|
+
accent_wash: palette.zinc["100"],
|
|
330
354
|
shadow: `0px 0px 6px 1px ${palette.zinc["300"]}`,
|
|
331
355
|
};
|
|
332
356
|
|
|
@@ -357,7 +381,7 @@ export function withAlpha(color: string, alpha: number): string {
|
|
|
357
381
|
*/
|
|
358
382
|
export type ColorName = Exclude<
|
|
359
383
|
keyof typeof colors,
|
|
360
|
-
"border" | "border_shadow" | "background" | "accent" | "shadow" | "black" | "white"
|
|
384
|
+
"border" | "border_shadow" | "background" | "accent" | "accent_wash" | "primary" | "shadow" | "black" | "white"
|
|
361
385
|
>;
|
|
362
386
|
|
|
363
387
|
/**
|
package/src/colors.web.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { withAlpha, solid, tint, ramp, isColorName, asColorName } from "./color_
|
|
|
4
4
|
export type { ColorName } from "./color_tokens";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* The token contract, with its
|
|
7
|
+
* The token contract, with its four themeable roles indirected through CSS
|
|
8
8
|
* variables so an app can carry its own visual identity.
|
|
9
9
|
*
|
|
10
10
|
* Why this file exists at all, rather than a React context: 136 of the kit's 320
|
|
@@ -28,7 +28,7 @@ export type { ColorName } from "./color_tokens";
|
|
|
28
28
|
* the strictest sense — an app that sets nothing has no `--lotics-*` defined,
|
|
29
29
|
* every `var()` falls back, and the pixels are identical to before this file.
|
|
30
30
|
*
|
|
31
|
-
* ONLY these
|
|
31
|
+
* ONLY these five. The palette families stay literal, and must: `withAlpha`,
|
|
32
32
|
* `tint` and `ramp` do string surgery on an `rgba()` to derive a wash, and a
|
|
33
33
|
* `var()` handed to them would produce garbage no type would catch. Keeping the
|
|
34
34
|
* themeable set to chrome keeps those functions total — the maintainability
|
|
@@ -40,4 +40,6 @@ export const colors = {
|
|
|
40
40
|
background: `var(--lotics-background, ${contract.background})`,
|
|
41
41
|
border: `var(--lotics-border, ${contract.border})`,
|
|
42
42
|
accent: `var(--lotics-accent, ${contract.accent})`,
|
|
43
|
+
primary: `var(--lotics-primary, ${contract.primary})`,
|
|
44
|
+
accent_wash: `var(--lotics-accent-wash, ${contract.accent_wash})`,
|
|
43
45
|
};
|
package/src/comments_button.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { StyleSheet, type GestureResponderEvent } from "react-native";
|
|
|
2
2
|
import { colors } from "./colors";
|
|
3
3
|
import { Icon } from "./icon";
|
|
4
4
|
import { PressableHighlight } from "./pressable_highlight";
|
|
5
|
-
import { ROW_CONTROL_HOVER, ROW_CONTROL_PRESS } from "./control_surface";
|
|
5
|
+
import { CURSOR_ACTION, ROW_CONTROL_HOVER, ROW_CONTROL_PRESS } from "./control_surface";
|
|
6
6
|
import { Text } from "./text";
|
|
7
7
|
import { useLoticsLocale } from "./locale";
|
|
8
8
|
|
|
@@ -105,6 +105,8 @@ export function CommentsButton(props: CommentsButtonProps) {
|
|
|
105
105
|
|
|
106
106
|
const styles = StyleSheet.create({
|
|
107
107
|
button: {
|
|
108
|
+
// Opening the thread ACTS — see `CURSOR_ACTION`.
|
|
109
|
+
cursor: CURSOR_ACTION,
|
|
108
110
|
flexDirection: "row",
|
|
109
111
|
alignItems: "center",
|
|
110
112
|
// Tight: the glyph and its number are ONE word, not two things in a row.
|
package/src/control_surface.ts
CHANGED
|
@@ -159,3 +159,30 @@ export function chipSurfaceStyle(
|
|
|
159
159
|
boxShadow: selected ? `0 0 0 1px ${colors.zinc[900]}` : undefined,
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Which controls show a POINTER, and which show the arrow.
|
|
165
|
+
*
|
|
166
|
+
* **Only a control that ACTS ON ITS OWN gets the pointer** — a button, a link, a
|
|
167
|
+
* disclosure. Everything else takes the arrow: a row, a menu option, a card, a
|
|
168
|
+
* tab, a field trigger, a checkbox, a switch, a chip. The line is not "is it
|
|
169
|
+
* pressable" (nearly everything here is) but "does pressing it DO something, or
|
|
170
|
+
* does it set a value / open the thing you are already looking at".
|
|
171
|
+
*
|
|
172
|
+
* The reason to draw it tightly is that a signal on half the screen is not a
|
|
173
|
+
* signal. A pointer everywhere says only "this is a web page"; reserved, it
|
|
174
|
+
* marks the handful of things that act. What carries "you can press this"
|
|
175
|
+
* elsewhere is the hover treatment each surface already owns — a row's wash, a
|
|
176
|
+
* field's border, a chip's fill — which is the affordance a native list or menu
|
|
177
|
+
* uses too, where the cursor stays an arrow throughout.
|
|
178
|
+
*
|
|
179
|
+
* **The arrow is never the default and always has to be asked for.**
|
|
180
|
+
* react-native-web ships `cursor: pointer` inside `Pressable`'s own base style,
|
|
181
|
+
* so a component that says nothing gets a pointer. `PressableHighlight` and
|
|
182
|
+
* `FocusRingPressable` set the arrow for everything built on them, and a
|
|
183
|
+
* component on the bare `Pressable` sets it itself; the few that act set
|
|
184
|
+
* {@link CURSOR_ACTION} back over the top.
|
|
185
|
+
*/
|
|
186
|
+
export const CURSOR_DEFAULT = "auto";
|
|
187
|
+
/** For a button, a link, a disclosure — see {@link CURSOR_DEFAULT}. */
|
|
188
|
+
export const CURSOR_ACTION = "pointer";
|