@lotics/ui 47.13.0 → 47.14.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 +16 -2
- package/docs/catalog.md +225 -27
- package/docs/composition.md +83 -4
- package/docs/data_entry.md +17 -7
- package/docs/reviewing.md +30 -0
- package/docs/templates.md +1 -1
- package/examples/tpl_attendance.tsx +51 -80
- package/examples/tpl_pivot.tsx +10 -1
- package/examples/tpl_record.tsx +33 -107
- package/package.json +9 -1
- package/src/avatar_size.ts +9 -8
- package/src/axis_ticks.ts +63 -0
- package/src/bar_chart.tsx +69 -50
- package/src/cell_stack.tsx +43 -7
- package/src/chip_group.tsx +12 -6
- package/src/danger_zone.tsx +23 -10
- package/src/date_filter.tsx +17 -7
- package/src/date_filter_presets.ts +209 -1
- package/src/date_range_filter_field.tsx +14 -4
- package/src/detail_row.tsx +18 -18
- package/src/drawer.tsx +66 -28
- package/src/format_date.ts +31 -3
- package/src/index.css +18 -0
- package/src/ledger.tsx +23 -15
- package/src/line_chart.tsx +194 -61
- package/src/list_item.tsx +22 -3
- package/src/locale.tsx +25 -0
- package/src/matrix.tsx +116 -16
- package/src/matrix_totals.ts +71 -3
- package/src/month_math.ts +16 -0
- package/src/month_stepper.tsx +62 -0
- package/src/organization_name.ts +94 -0
- package/src/search_match.ts +19 -0
- package/src/section_nav.tsx +163 -0
- package/src/state_matrix.tsx +265 -0
- package/src/table.tsx +67 -5
- package/src/timetable.tsx +167 -0
- package/src/toggle_strip.tsx +259 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { View } from "react-native";
|
|
3
|
+
import { colors } from "./colors";
|
|
4
|
+
import { useLoticsLocale } from "./locale";
|
|
5
|
+
import { Table, TableCell, TableRow, type TableColumn } from "./table";
|
|
6
|
+
import { Text } from "./text";
|
|
7
|
+
import { TOGGLE_STRIP_CELL_GAP, TOGGLE_STRIP_CELL_WIDTH, ToggleStrip, toggleStripWidth } from "./toggle_strip";
|
|
8
|
+
|
|
9
|
+
export interface TimetableLabels {
|
|
10
|
+
/** Header of the time column. */
|
|
11
|
+
time: string;
|
|
12
|
+
/** Header of the column naming what is scheduled — a vehicle, a person, a room. */
|
|
13
|
+
subject: string;
|
|
14
|
+
/** Header of the optional state column. */
|
|
15
|
+
status: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** One FIXED position on the repeating axis — a weekday, an hour band. */
|
|
19
|
+
export interface TimetableSlot<T extends string> {
|
|
20
|
+
value: T;
|
|
21
|
+
/** One or two characters, shown ONCE in the header. */
|
|
22
|
+
label: string;
|
|
23
|
+
/** What a screen reader says for a cell in this column. */
|
|
24
|
+
name: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TimetableRow<T extends string> {
|
|
28
|
+
id: string;
|
|
29
|
+
/** The row's identity on the time axis — "07:30". Text, never an editor:
|
|
30
|
+
* the subject of a row is read, and edits in the row's `detail`. */
|
|
31
|
+
time: string;
|
|
32
|
+
/** The positions this row is ON. */
|
|
33
|
+
active: T[];
|
|
34
|
+
/** What runs in this slot. A node, so an identity mark rides it. */
|
|
35
|
+
subject: ReactNode;
|
|
36
|
+
/** A state worth a word — "withdrawn". The column exists only when some row has one. */
|
|
37
|
+
status?: ReactNode;
|
|
38
|
+
disabled?: boolean;
|
|
39
|
+
/** The row's verbs — an `ActionMenu`. Rendered in the trailing gutter. */
|
|
40
|
+
trailing?: ReactNode;
|
|
41
|
+
/** Expands under the row: where the time and the subject are EDITED. */
|
|
42
|
+
detail?: ReactNode;
|
|
43
|
+
expanded?: boolean;
|
|
44
|
+
onPress?: () => void;
|
|
45
|
+
accessibilityLabel?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TimetableProps<T extends string> {
|
|
49
|
+
slots: TimetableSlot<T>[];
|
|
50
|
+
/** The position the reader is standing in — today's weekday. Its column is
|
|
51
|
+
* tinted down the whole grid and its header carries `aria-current`. */
|
|
52
|
+
current?: T;
|
|
53
|
+
rows: TimetableRow<T>[];
|
|
54
|
+
onToggle: (rowId: string, next: T[]) => void;
|
|
55
|
+
/** Refuse to turn a row's LAST position off — see `ToggleStrip`. */
|
|
56
|
+
minOne?: boolean;
|
|
57
|
+
labels?: Partial<TimetableLabels>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const TIME_WIDTH = 92;
|
|
61
|
+
const STATUS_WIDTH = 120;
|
|
62
|
+
const TRAILING = 40;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A TIMETABLE: slots down the side, a fixed repeating axis across the top, and
|
|
66
|
+
* a mark wherever a slot is on — a departure board, a shift roster, a lane's
|
|
67
|
+
* opening hours.
|
|
68
|
+
*
|
|
69
|
+
* It is not `CalendarView`: nothing here has a DATE, so there is no week to
|
|
70
|
+
* page to and no duration to draw; the whole subject is a pattern across fixed
|
|
71
|
+
* positions, and a column of rows lets the eye run down position 3 and see the
|
|
72
|
+
* shape of the week. It is not a plain `Table` of `ToggleStrip`s either — that
|
|
73
|
+
* was the first draft, and it labelled every cell: seven letters per row, the
|
|
74
|
+
* same seven on every row, twenty-eight labels for four facts. Here the axis is
|
|
75
|
+
* named ONCE in the header and the cells are marks under it.
|
|
76
|
+
*
|
|
77
|
+
* The row's time and subject are TEXT. A column of always-open editors makes a
|
|
78
|
+
* list look like a form (composition.md § Master-detail), and a timetable is
|
|
79
|
+
* read every day and changed a few times a month — so a row's edits live in its
|
|
80
|
+
* `detail`, opened by pressing the row. The pattern is the one thing edited in
|
|
81
|
+
* place, because a mark IS its own editor.
|
|
82
|
+
*/
|
|
83
|
+
export function Timetable<T extends string>(props: TimetableProps<T>) {
|
|
84
|
+
const { slots, current, rows, onToggle, minOne = false, labels } = props;
|
|
85
|
+
const pack = useLoticsLocale().timetable;
|
|
86
|
+
const l = { ...pack, ...labels };
|
|
87
|
+
const coStatus = rows.some((r) => r.status != null);
|
|
88
|
+
const coTrailing = rows.some((r) => r.trailing != null);
|
|
89
|
+
|
|
90
|
+
const columns: TableColumn[] = [
|
|
91
|
+
{ key: "time", label: l.time, width: TIME_WIDTH, priority: 1 },
|
|
92
|
+
{
|
|
93
|
+
key: "pattern",
|
|
94
|
+
header: <SlotHeader slots={slots} current={current} />,
|
|
95
|
+
width: toggleStripWidth(slots.length),
|
|
96
|
+
priority: 2,
|
|
97
|
+
},
|
|
98
|
+
{ key: "subject", label: l.subject, flex: 1, priority: 3 },
|
|
99
|
+
...(coStatus ? [{ key: "status", label: l.status, width: STATUS_WIDTH, priority: 4 }] : []),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<Table columns={columns} trailing={coTrailing ? TRAILING : 0} align={coTrailing ? "top" : "center"}>
|
|
104
|
+
{rows.map((r) => {
|
|
105
|
+
// An ARRAY, never a fragment or a conditional child: `Table` hands each
|
|
106
|
+
// cell its column by POSITION.
|
|
107
|
+
const cells = [
|
|
108
|
+
<TableCell key="time">
|
|
109
|
+
<Text size="sm" weight="medium" tabular numberOfLines={1}>
|
|
110
|
+
{r.time}
|
|
111
|
+
</Text>
|
|
112
|
+
</TableCell>,
|
|
113
|
+
<TableCell key="pattern">
|
|
114
|
+
<ToggleStrip
|
|
115
|
+
variant="mark"
|
|
116
|
+
current={current}
|
|
117
|
+
accessibilityLabel={r.time}
|
|
118
|
+
options={slots}
|
|
119
|
+
value={r.active}
|
|
120
|
+
onValueChange={(next) => onToggle(r.id, next)}
|
|
121
|
+
minOne={minOne}
|
|
122
|
+
disabled={r.disabled}
|
|
123
|
+
/>
|
|
124
|
+
</TableCell>,
|
|
125
|
+
<TableCell key="subject">{r.subject}</TableCell>,
|
|
126
|
+
];
|
|
127
|
+
if (coStatus) cells.push(<TableCell key="status">{r.status ?? null}</TableCell>);
|
|
128
|
+
// A door and its name come together, or not at all — `TableRow`'s own contract.
|
|
129
|
+
const door = r.onPress === undefined ? {} : { onPress: r.onPress, accessibilityLabel: r.accessibilityLabel ?? r.time };
|
|
130
|
+
return (
|
|
131
|
+
<TableRow key={r.id} expanded={r.expanded} detail={r.detail} trailing={r.trailing} {...door}>
|
|
132
|
+
{cells}
|
|
133
|
+
</TableRow>
|
|
134
|
+
);
|
|
135
|
+
})}
|
|
136
|
+
</Table>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The axis, named once. Cells sit on `ToggleStrip`'s own geometry so the
|
|
141
|
+
* labels land over the marks they name. */
|
|
142
|
+
function SlotHeader<T extends string>({ slots, current }: { slots: TimetableSlot<T>[]; current?: T }) {
|
|
143
|
+
return (
|
|
144
|
+
<View style={{ flexDirection: "row", gap: TOGGLE_STRIP_CELL_GAP }}>
|
|
145
|
+
{slots.map((s) => {
|
|
146
|
+
const now = s.value === current;
|
|
147
|
+
return (
|
|
148
|
+
<View
|
|
149
|
+
key={s.value}
|
|
150
|
+
aria-current={now ? "date" : undefined}
|
|
151
|
+
style={{
|
|
152
|
+
width: TOGGLE_STRIP_CELL_WIDTH,
|
|
153
|
+
alignItems: "center",
|
|
154
|
+
paddingVertical: 2,
|
|
155
|
+
borderRadius: 6,
|
|
156
|
+
backgroundColor: now ? colors.zinc[100] : undefined,
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<Text size="sm" weight="medium" color={now ? "default" : "muted"} numberOfLines={1}>
|
|
160
|
+
{s.label}
|
|
161
|
+
</Text>
|
|
162
|
+
</View>
|
|
163
|
+
);
|
|
164
|
+
})}
|
|
165
|
+
</View>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { Pressable, View, type StyleProp, type ViewStyle } from "react-native";
|
|
2
|
+
import { colors } from "./colors";
|
|
3
|
+
import { Text } from "./text";
|
|
4
|
+
import {
|
|
5
|
+
CONTROL_CONTENT_HEIGHT,
|
|
6
|
+
CONTROL_TRANSITION,
|
|
7
|
+
CURSOR_DEFAULT,
|
|
8
|
+
FOCUS_RING,
|
|
9
|
+
ROW_CONTROL_HOVER,
|
|
10
|
+
ROW_CONTROL_PRESS,
|
|
11
|
+
chipSurfaceStyle,
|
|
12
|
+
proportionalRadius,
|
|
13
|
+
} from "./control_surface";
|
|
14
|
+
import { useFocusRing } from "./use_focus_ring";
|
|
15
|
+
|
|
16
|
+
/** The default cell width — two characters at `xs` with air, which is what a
|
|
17
|
+
* weekday initial, an hour band or a channel letter actually needs. */
|
|
18
|
+
const CELL_WIDTH = 30;
|
|
19
|
+
|
|
20
|
+
/** The gap between cells. Tight on purpose: the cells are ONE object — a week, a
|
|
21
|
+
* band, a set — and a chip-row gap would read them as separate controls. */
|
|
22
|
+
const CELL_GAP = 2;
|
|
23
|
+
|
|
24
|
+
/** The strip's geometry, for a header that names the positions ONCE above a
|
|
25
|
+
* column of strips (`Timetable`): its labels must land over the cells. */
|
|
26
|
+
export const TOGGLE_STRIP_CELL_WIDTH = CELL_WIDTH;
|
|
27
|
+
export const TOGGLE_STRIP_CELL_GAP = CELL_GAP;
|
|
28
|
+
|
|
29
|
+
/** The mark a `mark` cell draws — a filled square when on, a hollow one when off. */
|
|
30
|
+
const MARK = 14;
|
|
31
|
+
|
|
32
|
+
export interface ToggleStripOption<T extends string> {
|
|
33
|
+
value: T;
|
|
34
|
+
/** What the cell SHOWS — one or two characters ("T2", "CN", "08"). */
|
|
35
|
+
label: string;
|
|
36
|
+
/** What a screen reader says instead. Required, because a two-letter cell
|
|
37
|
+
* announces as nonsense: seven "T2" switches in a table are told apart only
|
|
38
|
+
* by voice, and the label is not a name. */
|
|
39
|
+
name: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ToggleStripProps<T extends string> {
|
|
43
|
+
/** Names the SET — "Ngày chạy của nốt 05:30". Every cell's own name is
|
|
44
|
+
* prefixed with it, so a register of these strips is navigable by voice. */
|
|
45
|
+
accessibilityLabel: string;
|
|
46
|
+
/** The fixed positions, in reading order. They do not move with the data —
|
|
47
|
+
* the whole point is that position N means the same thing on every row. */
|
|
48
|
+
options: ToggleStripOption<T>[];
|
|
49
|
+
/** The values that are ON. */
|
|
50
|
+
value: T[];
|
|
51
|
+
onValueChange: (next: T[]) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Refuse to turn the LAST cell off.
|
|
54
|
+
*
|
|
55
|
+
* For a set where empty is not a neutral state but a DIFFERENT reading — a
|
|
56
|
+
* schedule with no day is not a schedule, and where the empty set already
|
|
57
|
+
* means "every day" the two readings would silently swap. The held cell wears
|
|
58
|
+
* the disabled treatment so the floor is visible rather than a press that
|
|
59
|
+
* does nothing.
|
|
60
|
+
*/
|
|
61
|
+
minOne?: boolean;
|
|
62
|
+
disabled?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* `label` (default) prints each option's one or two letters in a chip — the
|
|
65
|
+
* strip stands alone, in a field or a lone row, and names its own positions.
|
|
66
|
+
* `mark` draws a bare square instead, for a COLUMN of strips whose positions
|
|
67
|
+
* a header names once (`Timetable`): the same seven letters on every row were
|
|
68
|
+
* twenty-eight labels for four facts.
|
|
69
|
+
*/
|
|
70
|
+
variant?: "label" | "mark";
|
|
71
|
+
/** The position the reader is standing in — today's weekday. Its cell is
|
|
72
|
+
* tinted so a column of strips shows the day as a band. */
|
|
73
|
+
current?: T;
|
|
74
|
+
/** Cell width. Raise it for three-character labels; the strip's own width is
|
|
75
|
+
* {@link toggleStripWidth}, which a table column budgets against. */
|
|
76
|
+
cellWidth?: number;
|
|
77
|
+
style?: StyleProp<ViewStyle>;
|
|
78
|
+
testID?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A COMPACT MULTI-TOGGLE — N fixed cells, each independently on or off, sized
|
|
83
|
+
* to sit inside a register row rather than on a control band.
|
|
84
|
+
*
|
|
85
|
+
* The subject is a PATTERN across fixed positions: which days a departure runs,
|
|
86
|
+
* which hours a gate is open, which channels a notice goes out on. What makes it
|
|
87
|
+
* its own component rather than a `ChipGroup` is that the positions are fixed and
|
|
88
|
+
* the reading is VERTICAL — a column of these strips has to let the eye run down
|
|
89
|
+
* position 3 and see the shape of the week. Chips wrap and re-flow, which is
|
|
90
|
+
* right for a filter band and destroys exactly that.
|
|
91
|
+
*
|
|
92
|
+
* It is also not `SegmentedControl`: that is one choice among peers (a
|
|
93
|
+
* radiogroup); this is N independent booleans, so each cell is a `checkbox`.
|
|
94
|
+
*
|
|
95
|
+
* **The selected treatment is `chipSurfaceStyle`'s**, not a ground of its own.
|
|
96
|
+
* A filled brand ground was the obvious answer and is the wrong one twice over:
|
|
97
|
+
* a ground is the ROW's language and these are controls, so the two altitudes
|
|
98
|
+
* end up saying the same thing in the same paint; and it spends the brand on a
|
|
99
|
+
* selection, which then has to be walked back everywhere at once. Seven ON cells
|
|
100
|
+
* per row also made the primary button on the same screen the twenty-third copy
|
|
101
|
+
* of its own ground. The doubled zinc-900 edge is the kit's one answer to "which
|
|
102
|
+
* one is on", and it survives the hover wash instead of being replaced by it.
|
|
103
|
+
*
|
|
104
|
+
* Height is {@link CONTROL_CONTENT_HEIGHT}, not `CONTROL_HEIGHT`: this rides
|
|
105
|
+
* inside a row or a field, where 40 is the box and 28 is what the box may seat.
|
|
106
|
+
*/
|
|
107
|
+
export function ToggleStrip<T extends string>(props: ToggleStripProps<T>) {
|
|
108
|
+
const {
|
|
109
|
+
accessibilityLabel,
|
|
110
|
+
options,
|
|
111
|
+
value,
|
|
112
|
+
onValueChange,
|
|
113
|
+
minOne = false,
|
|
114
|
+
disabled = false,
|
|
115
|
+
variant = "label",
|
|
116
|
+
current,
|
|
117
|
+
cellWidth = CELL_WIDTH,
|
|
118
|
+
style,
|
|
119
|
+
testID,
|
|
120
|
+
} = props;
|
|
121
|
+
const on = new Set(value);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<View
|
|
125
|
+
testID={testID}
|
|
126
|
+
role="group"
|
|
127
|
+
aria-label={accessibilityLabel}
|
|
128
|
+
style={[{ flexDirection: "row", gap: CELL_GAP }, style]}
|
|
129
|
+
>
|
|
130
|
+
{options.map((o) => {
|
|
131
|
+
const bat = on.has(o.value);
|
|
132
|
+
const khoa = disabled || (minOne && bat && on.size === 1);
|
|
133
|
+
return (
|
|
134
|
+
<ToggleCell
|
|
135
|
+
key={o.value}
|
|
136
|
+
label={o.label}
|
|
137
|
+
name={`${accessibilityLabel} — ${o.name}`}
|
|
138
|
+
on={bat}
|
|
139
|
+
disabled={khoa}
|
|
140
|
+
width={cellWidth}
|
|
141
|
+
variant={variant}
|
|
142
|
+
current={o.value === current}
|
|
143
|
+
// The result keeps the OPTIONS' order, never press order: the set is
|
|
144
|
+
// a pattern across fixed positions, and a caller comparing it against
|
|
145
|
+
// a stored one must not see a diff because two days were toggled in
|
|
146
|
+
// the other sequence.
|
|
147
|
+
onPress={() =>
|
|
148
|
+
onValueChange(
|
|
149
|
+
options.filter((x) => (x.value === o.value ? !bat : on.has(x.value))).map((x) => x.value),
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
})}
|
|
155
|
+
</View>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** The width the strip occupies, so a table column can be budgeted for it
|
|
160
|
+
* EXACTLY. A column guessed at is a column the fit sheds for the wrong reason. */
|
|
161
|
+
export function toggleStripWidth(count: number, cellWidth: number = CELL_WIDTH): number {
|
|
162
|
+
return count * cellWidth + Math.max(0, count - 1) * CELL_GAP;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function ToggleCell(props: {
|
|
166
|
+
label: string;
|
|
167
|
+
name: string;
|
|
168
|
+
on: boolean;
|
|
169
|
+
disabled: boolean;
|
|
170
|
+
width: number;
|
|
171
|
+
variant: "label" | "mark";
|
|
172
|
+
current: boolean;
|
|
173
|
+
onPress: () => void;
|
|
174
|
+
}) {
|
|
175
|
+
const { label, name, on, disabled, width, variant, current, onPress } = props;
|
|
176
|
+
const { focusVisible, focusProps } = useFocusRing();
|
|
177
|
+
if (variant === "mark") {
|
|
178
|
+
return (
|
|
179
|
+
<Pressable
|
|
180
|
+
{...focusProps}
|
|
181
|
+
disabled={disabled}
|
|
182
|
+
onPress={onPress}
|
|
183
|
+
role="checkbox"
|
|
184
|
+
aria-label={name}
|
|
185
|
+
aria-checked={on}
|
|
186
|
+
aria-disabled={disabled || undefined}
|
|
187
|
+
style={(state) => [
|
|
188
|
+
{
|
|
189
|
+
height: CONTROL_CONTENT_HEIGHT,
|
|
190
|
+
width,
|
|
191
|
+
borderRadius: proportionalRadius(CONTROL_CONTENT_HEIGHT),
|
|
192
|
+
alignItems: "center",
|
|
193
|
+
justifyContent: "center",
|
|
194
|
+
cursor: CURSOR_DEFAULT,
|
|
195
|
+
opacity: disabled ? 0.55 : 1,
|
|
196
|
+
// The cell has no chrome of its own — the MARK is the state. Today's
|
|
197
|
+
// column rests on a tint, and the pointer deepens whatever the cell
|
|
198
|
+
// rests on, so the wash never vanishes on the tinted column.
|
|
199
|
+
backgroundColor: state.pressed
|
|
200
|
+
? ROW_CONTROL_PRESS
|
|
201
|
+
: state.hovered
|
|
202
|
+
? ROW_CONTROL_HOVER
|
|
203
|
+
: current
|
|
204
|
+
? colors.zinc[100]
|
|
205
|
+
: "transparent",
|
|
206
|
+
...CONTROL_TRANSITION,
|
|
207
|
+
},
|
|
208
|
+
focusVisible ? { boxShadow: FOCUS_RING } : null,
|
|
209
|
+
]}
|
|
210
|
+
>
|
|
211
|
+
<View
|
|
212
|
+
style={{
|
|
213
|
+
width: MARK,
|
|
214
|
+
height: MARK,
|
|
215
|
+
borderRadius: 4,
|
|
216
|
+
backgroundColor: on ? colors.zinc[900] : "transparent",
|
|
217
|
+
borderWidth: on ? 0 : 1.5,
|
|
218
|
+
borderColor: colors.zinc[300],
|
|
219
|
+
}}
|
|
220
|
+
/>
|
|
221
|
+
</Pressable>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return (
|
|
225
|
+
<Pressable
|
|
226
|
+
{...focusProps}
|
|
227
|
+
disabled={disabled}
|
|
228
|
+
onPress={onPress}
|
|
229
|
+
role="checkbox"
|
|
230
|
+
aria-label={name}
|
|
231
|
+
aria-checked={on}
|
|
232
|
+
aria-disabled={disabled || undefined}
|
|
233
|
+
style={(state) => [
|
|
234
|
+
chipSurfaceStyle(state, { selected: on }),
|
|
235
|
+
{
|
|
236
|
+
// The chip surface is a BAND control at 40; inside a row it seats at
|
|
237
|
+
// the content rung, and its corner scales with the height so a strip
|
|
238
|
+
// and a chip read as one family rather than two radii.
|
|
239
|
+
height: CONTROL_CONTENT_HEIGHT,
|
|
240
|
+
borderRadius: proportionalRadius(CONTROL_CONTENT_HEIGHT),
|
|
241
|
+
width,
|
|
242
|
+
alignItems: "center",
|
|
243
|
+
justifyContent: "center",
|
|
244
|
+
// Setting a VALUE is not acting on its own — see CURSOR_DEFAULT.
|
|
245
|
+
// react-native-web's Pressable ships `cursor: pointer` in its base
|
|
246
|
+
// style, so this has to be asked for.
|
|
247
|
+
cursor: CURSOR_DEFAULT,
|
|
248
|
+
opacity: disabled ? 0.55 : 1,
|
|
249
|
+
...CONTROL_TRANSITION,
|
|
250
|
+
},
|
|
251
|
+
focusVisible ? { boxShadow: FOCUS_RING } : null,
|
|
252
|
+
]}
|
|
253
|
+
>
|
|
254
|
+
<Text size="xs" weight="medium" color={on ? "default" : "muted"}>
|
|
255
|
+
{label}
|
|
256
|
+
</Text>
|
|
257
|
+
</Pressable>
|
|
258
|
+
);
|
|
259
|
+
}
|