@lotics/ui 46.14.1 → 47.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/AGENTS.md +20 -0
- package/MIGRATION.md +58 -0
- package/docs/catalog.md +72 -13
- package/docs/composition.md +94 -6
- package/docs/reviewing.md +56 -5
- package/docs/templates.md +8 -2
- package/examples/tpl_calendar.tsx +22 -29
- package/examples/tpl_item_list.tsx +7 -2
- package/package.json +1 -1
- package/src/accordion.tsx +11 -5
- package/src/bar_chart.tsx +3 -1
- package/src/board.tsx +1 -2
- package/src/breakdown.tsx +3 -1
- package/src/calendar/agenda_view.tsx +136 -0
- package/src/calendar/calendar_toolbar.tsx +84 -0
- package/src/calendar/calendar_view.tsx +196 -103
- package/src/calendar/context.ts +47 -0
- package/src/calendar/dates.ts +36 -6
- package/src/calendar/event_chip.tsx +141 -0
- package/src/calendar/index.ts +31 -8
- package/src/calendar/layout.ts +113 -11
- package/src/calendar/month_view.tsx +194 -150
- package/src/calendar/repeat.ts +174 -0
- package/src/calendar/time_grid_view.tsx +182 -202
- package/src/calendar/types.ts +37 -11
- package/src/charge_lines.tsx +24 -5
- package/src/control_surface.ts +28 -0
- package/src/deadline.ts +10 -0
- package/src/file_row.tsx +26 -3
- package/src/finding.tsx +1 -1
- package/src/form_text_input.tsx +9 -2
- package/src/gantt/gantt_view.tsx +212 -119
- package/src/gantt/index.ts +2 -2
- package/src/gantt/scale.ts +38 -2
- package/src/gantt/types.ts +34 -7
- package/src/inline_slot.tsx +16 -2
- package/src/inline_static.tsx +18 -4
- package/src/legend_item.tsx +14 -1
- package/src/locale.tsx +30 -0
- package/src/matrix.tsx +19 -5
- package/src/member_chip.tsx +18 -1
- package/src/menu_button.tsx +12 -2
- package/src/option_list.tsx +11 -1
- package/src/progress_bar.tsx +14 -1
- package/src/record_summary.tsx +5 -0
- package/src/stacked_bar_chart.tsx +4 -1
- package/src/table_fit.ts +18 -1
- package/src/thumbnail_stack.tsx +18 -3
- package/src/use_option_list.ts +13 -1
package/src/inline_slot.tsx
CHANGED
|
@@ -6,6 +6,10 @@ import { INLINE_CONTROL_HEIGHT } from "./inline_edit";
|
|
|
6
6
|
export interface InlineSlotProps {
|
|
7
7
|
/** Whatever sits in the value column — a `DiffValue`, a chip, a pair. */
|
|
8
8
|
children: ReactNode;
|
|
9
|
+
/** Verbs about the value, pinned right ON the slot's surface — the same place
|
|
10
|
+
* an editable `Inline*` puts its `actions`, so a static row and an editable
|
|
11
|
+
* one carry their verbs on one edge. */
|
|
12
|
+
actions?: ReactNode;
|
|
9
13
|
style?: StyleProp<ViewStyle>;
|
|
10
14
|
}
|
|
11
15
|
|
|
@@ -32,11 +36,21 @@ export interface InlineSlotProps {
|
|
|
32
36
|
* NOT for a table cell: a `Table` sets its own row rhythm, and a 40px control
|
|
33
37
|
* band inside one only makes the rows taller.
|
|
34
38
|
*/
|
|
35
|
-
export function InlineSlot({ children, style }: InlineSlotProps) {
|
|
36
|
-
return <View style={[styles.box, style]}>{children}</View>;
|
|
39
|
+
export function InlineSlot({ children, actions, style }: InlineSlotProps) {
|
|
40
|
+
if (!actions) return <View style={[styles.box, style]}>{children}</View>;
|
|
41
|
+
return (
|
|
42
|
+
<View style={[styles.box, styles.withActions, style]}>
|
|
43
|
+
<View style={styles.grow}>{children}</View>
|
|
44
|
+
{actions}
|
|
45
|
+
</View>
|
|
46
|
+
);
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
const styles = StyleSheet.create({
|
|
50
|
+
// The value takes the slack and the verbs pin right, so a column of rows —
|
|
51
|
+
// some with a verb, some without — keeps one right edge.
|
|
52
|
+
withActions: { flexDirection: "row", alignItems: "center", gap: 8 },
|
|
53
|
+
grow: { flex: 1, minWidth: 0 },
|
|
40
54
|
// Matches `inline_edit`'s view box exactly (height, radius, padding, 1px
|
|
41
55
|
// transparent border) so the content's baseline aligns with the editors —
|
|
42
56
|
// minus the FocusRingPressable, so it never reads as an interactive control.
|
package/src/inline_static.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
1
2
|
import { StyleSheet } from "react-native";
|
|
2
3
|
import { Text } from "./text";
|
|
3
4
|
import { colors } from "./colors";
|
|
@@ -28,6 +29,18 @@ export interface InlineStaticProps {
|
|
|
28
29
|
* misalignment this component exists to prevent, just on the other axis.
|
|
29
30
|
*/
|
|
30
31
|
multiline?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Verbs about THIS value, on the value's own surface — an `InlineButton`, the
|
|
34
|
+
* same slot every editable `Inline*` exposes.
|
|
35
|
+
*
|
|
36
|
+
* A read-only value often still has an act attached to it: look this id up,
|
|
37
|
+
* copy it, request the change that is gated behind an approval. Without this
|
|
38
|
+
* slot that verb has nowhere to sit but the row's ground, and an `InlineButton`
|
|
39
|
+
* outside a field surface is the wrong control (see `catalog.md`
|
|
40
|
+
* §`inline_button`). A static value is still a field; it just is not editable
|
|
41
|
+
* in place.
|
|
42
|
+
*/
|
|
43
|
+
actions?: ReactNode;
|
|
31
44
|
}
|
|
32
45
|
|
|
33
46
|
/**
|
|
@@ -37,15 +50,16 @@ export interface InlineStaticProps {
|
|
|
37
50
|
* locked value — sits flush in the same column as the editable rows. It is
|
|
38
51
|
* NON-interactive (no hover, no pointer, no focus ring): it reads as a plain
|
|
39
52
|
* value, NOT a disabled input. Pair with `DetailRow` exactly like the `Inline*`
|
|
40
|
-
* editors
|
|
41
|
-
* slot,
|
|
53
|
+
* editors. A verb about the value goes in `actions`, on the value's own surface —
|
|
54
|
+
* `DetailRow` has no trailing slot, and a verb on the row's ground is an
|
|
55
|
+
* `InlineButton` without a field under it.
|
|
42
56
|
*/
|
|
43
57
|
export function InlineStatic(props: InlineStaticProps) {
|
|
44
|
-
const { value, placeholder, muted, tabular, align = "left", weight, multiline } = props;
|
|
58
|
+
const { value, placeholder, muted, tabular, align = "left", weight, multiline, actions } = props;
|
|
45
59
|
const isEmpty = value.length === 0;
|
|
46
60
|
const display = isEmpty ? (placeholder ?? "—") : value;
|
|
47
61
|
return (
|
|
48
|
-
<InlineSlot>
|
|
62
|
+
<InlineSlot actions={actions}>
|
|
49
63
|
<Text
|
|
50
64
|
numberOfLines={multiline ? undefined : 1}
|
|
51
65
|
tabular={tabular}
|
package/src/legend_item.tsx
CHANGED
|
@@ -24,7 +24,10 @@ export function LegendItem(props: LegendItemProps) {
|
|
|
24
24
|
return (
|
|
25
25
|
<View style={styles.row}>
|
|
26
26
|
<View style={[styles.swatch, { backgroundColor: props.color }]} />
|
|
27
|
-
|
|
27
|
+
{/* Nhãn phải CO ĐƯỢC. Không có `minWidth: 0` thì một hộp chữ trong hàng
|
|
28
|
+
flex không bao giờ nhỏ hơn nội dung của nó, nên nó đẩy cả hàng rộng ra
|
|
29
|
+
và ở màn hẹp hai mục chú giải chồng lên nhau thay vì xuống dòng. */}
|
|
30
|
+
<Text size="sm" color="muted" style={styles.label}>
|
|
28
31
|
{props.label}
|
|
29
32
|
</Text>
|
|
30
33
|
{props.value !== undefined && <Metric value={props.value} size="sm" />}
|
|
@@ -37,7 +40,17 @@ const styles = StyleSheet.create({
|
|
|
37
40
|
flexDirection: "row",
|
|
38
41
|
alignItems: "center",
|
|
39
42
|
gap: SPACE.sm,
|
|
43
|
+
// 130 là bề ngang MONG MUỐN để vài mục chú giải xếp thành cột đều nhau —
|
|
44
|
+
// không phải một sàn cứng. Thiếu `flexShrink` thì nó thành sàn thật: ở màn
|
|
45
|
+
// hẹp hàng không chịu co, và một chú giải bốn chữ vừa bị cắt vừa đè lên
|
|
46
|
+
// mục bên cạnh. Cùng nhầm lẫn mà `table_fit` đã gỡ một lần — hỏi "còn
|
|
47
|
+
// dùng được không" ở chỗ đáng ra phải hỏi "còn đọc được không".
|
|
40
48
|
minWidth: 130,
|
|
49
|
+
flexShrink: 1,
|
|
50
|
+
},
|
|
51
|
+
label: {
|
|
52
|
+
flexShrink: 1,
|
|
53
|
+
minWidth: 0,
|
|
41
54
|
},
|
|
42
55
|
swatch: {
|
|
43
56
|
width: 8,
|
package/src/locale.tsx
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createContext, useContext, type ReactNode } from "react";
|
|
2
2
|
import { type DatePickerLabels } from "./date_picker";
|
|
3
3
|
import { type CalendarLabels } from "./date_calendar";
|
|
4
|
+
import { type CalendarViewLabels } from "./calendar/types";
|
|
5
|
+
import { type GanttLabels } from "./gantt/types";
|
|
4
6
|
import { type PaginationLabels } from "./pagination";
|
|
5
7
|
import { type SortHeaderLabels } from "./sort_header";
|
|
6
8
|
import { type ConfidenceLabels } from "./confidence";
|
|
@@ -55,6 +57,14 @@ export interface LoticsLocale {
|
|
|
55
57
|
picker: { emptyOption: string };
|
|
56
58
|
datePicker: DatePickerLabels;
|
|
57
59
|
calendar: CalendarLabels;
|
|
60
|
+
/** `CalendarView` and its parts — the SCHEDULING surface. Distinct from
|
|
61
|
+
* `calendar` above, which is the date PICKER's month grid: two different
|
|
62
|
+
* components, two different vocabularies, and the collision is why this one
|
|
63
|
+
* carries the longer name. */
|
|
64
|
+
calendarView: CalendarViewLabels;
|
|
65
|
+
/** `GanttView` — the zoom switch, the frozen column header, dependency
|
|
66
|
+
* announcements and the empty state. */
|
|
67
|
+
gantt: GanttLabels;
|
|
58
68
|
/** `FilterChip` (and `ColumnFilter`): the generic clear affordance, used when
|
|
59
69
|
* a call site doesn't pass a dimension-specific `clearLabel`. */
|
|
60
70
|
filterChip: { clear: string };
|
|
@@ -327,6 +337,16 @@ export const en: LoticsLocale = {
|
|
|
327
337
|
picker: { emptyOption: "None" },
|
|
328
338
|
datePicker: { today: "Today", now: "Now", clear: "Clear", done: "Done", openCalendar: "Open calendar", chooseTime: "Choose a time", time: "Time", startTime: "Start time", endTime: "End time", startDate: "Start date", endDate: "End date", addTime: "Add time", removeTime: "Remove time", year: "Year", month: "Month", day: "Day", hour: "Hour", minute: "Minute", dayPeriod: "AM/PM", invalidDate: "Enter a complete date", invalidTime: "Enter a complete time" },
|
|
329
339
|
calendar: { previousMonth: "Previous month", nextMonth: "Next month" },
|
|
340
|
+
calendarView: {
|
|
341
|
+
today: "Today", month: "Month", week: "Week", day: "Day", agenda: "Agenda",
|
|
342
|
+
previous: "Previous", next: "Next", allDay: "all-day",
|
|
343
|
+
more: (n) => `+${n} more`, noEvents: "Nothing scheduled",
|
|
344
|
+
addAt: (when) => `Add at ${when}`,
|
|
345
|
+
},
|
|
346
|
+
gantt: {
|
|
347
|
+
day: "Day", week: "Week", month: "Month", task: "Task",
|
|
348
|
+
empty: "Nothing scheduled",
|
|
349
|
+
},
|
|
330
350
|
filterChip: { clear: "Clear" },
|
|
331
351
|
floatingActionBar: { clear: "Clear" },
|
|
332
352
|
formField: { optional: "Optional" },
|
|
@@ -536,6 +556,16 @@ export const vi: LoticsLocale = {
|
|
|
536
556
|
picker: { emptyOption: "Không có" },
|
|
537
557
|
datePicker: { today: "Hôm nay", now: "Bây giờ", clear: "Xóa", done: "Xong", openCalendar: "Mở lịch", chooseTime: "Chọn giờ", time: "Giờ", startTime: "Giờ bắt đầu", endTime: "Giờ kết thúc", startDate: "Ngày bắt đầu", endDate: "Ngày kết thúc", addTime: "Thêm giờ", removeTime: "Bỏ giờ", year: "Năm", month: "Tháng", day: "Ngày", hour: "Giờ", minute: "Phút", dayPeriod: "SA/CH", invalidDate: "Nhập ngày đầy đủ", invalidTime: "Nhập giờ đầy đủ" },
|
|
538
558
|
calendar: { previousMonth: "Tháng trước", nextMonth: "Tháng sau" },
|
|
559
|
+
calendarView: {
|
|
560
|
+
today: "Hôm nay", month: "Tháng", week: "Tuần", day: "Ngày", agenda: "Danh sách",
|
|
561
|
+
previous: "Trước", next: "Sau", allDay: "cả ngày",
|
|
562
|
+
more: (n) => `+${n} nữa`, noEvents: "Không có lịch nào",
|
|
563
|
+
addAt: (when) => `Thêm vào ${when}`,
|
|
564
|
+
},
|
|
565
|
+
gantt: {
|
|
566
|
+
day: "Ngày", week: "Tuần", month: "Tháng", task: "Công việc",
|
|
567
|
+
empty: "Chưa có việc nào",
|
|
568
|
+
},
|
|
539
569
|
filterChip: { clear: "Xóa" },
|
|
540
570
|
floatingActionBar: { clear: "Bỏ chọn" },
|
|
541
571
|
formField: { optional: "Tùy chọn" },
|
package/src/matrix.tsx
CHANGED
|
@@ -28,6 +28,10 @@ interface MatrixContextValue {
|
|
|
28
28
|
* column budget needs the same number the grid renders with — hard-coding it
|
|
29
29
|
* is how a width budget and the layout drift apart. */
|
|
30
30
|
export const MATRIX_COL_GAP = 3;
|
|
31
|
+
/** Chỗ thở hai bên con số bên trong một ô ma trận. Ô có NỀN, nên con số căn phải
|
|
32
|
+
* cần một khoảng với mép nền; dán sát thì nó đọc như bị cắt. Tiêu đề cột cộng
|
|
33
|
+
* đúng số này vào `paddingRight` của nó để hai bên kết thúc trên một đường. */
|
|
34
|
+
const MATRIX_CELL_PAD = 6;
|
|
31
35
|
|
|
32
36
|
const MatrixContext = createContext<MatrixContextValue | null>(null);
|
|
33
37
|
|
|
@@ -319,10 +323,19 @@ const styles = StyleSheet.create({
|
|
|
319
323
|
},
|
|
320
324
|
cell: {
|
|
321
325
|
flex: 1,
|
|
322
|
-
|
|
326
|
+
// 52 + hai lần MATRIX_CELL_PAD: cái đệm mới ăn vào bề ngang, nên sàn tối
|
|
327
|
+
// thiểu phải cộng thêm đúng ngần ấy, không thì ô chật đi và con số dài nhất
|
|
328
|
+
// là con số đầu tiên bị ép.
|
|
329
|
+
minWidth: 52 + MATRIX_CELL_PAD * 2,
|
|
323
330
|
height: 36,
|
|
324
331
|
borderRadius: 4,
|
|
325
332
|
borderWidth: 2,
|
|
333
|
+
// Con số căn phải trong một ô CÓ NỀN, nên nó cần chỗ thở với mép nền. Trước
|
|
334
|
+
// đây ô không có đệm ngang nào: chữ số cuối dừng cách mép đúng 2px của cái
|
|
335
|
+
// viền, và trên một ô tô màu thì nó đọc ra như bị cắt cụt chứ không như căn
|
|
336
|
+
// phải. Một con số dán vào cạnh khối màu của chính nó là con số trông như
|
|
337
|
+
// còn nữa mà không hiện ra.
|
|
338
|
+
paddingHorizontal: MATRIX_CELL_PAD,
|
|
326
339
|
// STRETCH, not center. The figure inside sets `align="right"`, and a Text
|
|
327
340
|
// centred by its parent SHRINK-WRAPS — so it is exactly as wide as its own
|
|
328
341
|
// glyphs and has nothing to align against. Right-aligning the text while
|
|
@@ -336,10 +349,11 @@ const styles = StyleSheet.create({
|
|
|
336
349
|
},
|
|
337
350
|
colCell: {
|
|
338
351
|
flex: 1,
|
|
339
|
-
minWidth: 52,
|
|
340
|
-
//
|
|
341
|
-
//
|
|
342
|
-
|
|
352
|
+
minWidth: 52 + MATRIX_CELL_PAD * 2,
|
|
353
|
+
// Khớp với tổng phần ô đẩy con số vào: 2px viền cộng cái đệm ngang. Lệch
|
|
354
|
+
// một pixel ở đây là tiêu đề cột và dãy số dưới nó kết thúc ở hai đường x
|
|
355
|
+
// khác nhau — thứ mắt bắt được ngay dù không gọi được tên.
|
|
356
|
+
paddingRight: 2 + MATRIX_CELL_PAD,
|
|
343
357
|
},
|
|
344
358
|
totalCol: {
|
|
345
359
|
// SIZED LIKE A DATA COLUMN, not to a fixed number. It was `width: 64`, and a
|
package/src/member_chip.tsx
CHANGED
|
@@ -29,6 +29,20 @@ interface MemberChipProps {
|
|
|
29
29
|
/** A rung on the shared avatar scale. Default `md`, which is sized to seat in a
|
|
30
30
|
* 40px control band; drop to `sm` for a dense cell. See `AVATAR_PX`. */
|
|
31
31
|
size?: AvatarSize;
|
|
32
|
+
/**
|
|
33
|
+
* Drop the face and render the person as TEXT.
|
|
34
|
+
*
|
|
35
|
+
* Pass it wherever the member is ONE FIELD AMONG COLUMNS rather than the row's
|
|
36
|
+
* subject — an owner column, an assignee cell, a "last edited by". Two reasons,
|
|
37
|
+
* both from `composition.md` §Identity marks: a second mark competes with the
|
|
38
|
+
* mark the row already carries for its own subject, and a handful of people
|
|
39
|
+
* owning hundreds of rows draws the same disc over and over, which "carries
|
|
40
|
+
* nothing and is the largest, brightest thing there".
|
|
41
|
+
*
|
|
42
|
+
* Keep the face where the member IS the subject — a roster, a picker option, a
|
|
43
|
+
* profile, a directory row.
|
|
44
|
+
*/
|
|
45
|
+
showAvatar?: boolean;
|
|
32
46
|
/** This person is no longer active — they left, their account was closed.
|
|
33
47
|
* Mutes the name so a record that still names them reads as history rather
|
|
34
48
|
* than as a current assignment. The chip keeps its height, because its usual
|
|
@@ -58,6 +72,7 @@ export function MemberChip({
|
|
|
58
72
|
image,
|
|
59
73
|
secondary,
|
|
60
74
|
size = "md",
|
|
75
|
+
showAvatar = true,
|
|
61
76
|
inactive,
|
|
62
77
|
style,
|
|
63
78
|
}: MemberChipProps) {
|
|
@@ -101,7 +116,9 @@ export function MemberChip({
|
|
|
101
116
|
secondary && AVATAR_PX[size] < AVATAR_PX.lg ? "lg" : size;
|
|
102
117
|
return (
|
|
103
118
|
<View style={[styles.row, style]}>
|
|
104
|
-
|
|
119
|
+
{showAvatar ? (
|
|
120
|
+
<Avatar size={markSize} name={displayName} source={image ? { uri: image } : undefined} />
|
|
121
|
+
) : null}
|
|
105
122
|
<View style={styles.text}>
|
|
106
123
|
<Text
|
|
107
124
|
userSelect="none"
|
package/src/menu_button.tsx
CHANGED
|
@@ -91,7 +91,11 @@ export function MenuButton(props: MenuButtonProps) {
|
|
|
91
91
|
|
|
92
92
|
const resolvedIcon =
|
|
93
93
|
typeof icon === "string" ? (
|
|
94
|
-
<Icon
|
|
94
|
+
<Icon
|
|
95
|
+
size={20}
|
|
96
|
+
name={icon as IconName}
|
|
97
|
+
color={disabled ? colors.zinc["400"] : danger ? colors.red["900"] : undefined}
|
|
98
|
+
/>
|
|
95
99
|
) : (
|
|
96
100
|
icon
|
|
97
101
|
);
|
|
@@ -102,7 +106,7 @@ export function MenuButton(props: MenuButtonProps) {
|
|
|
102
106
|
weight="medium"
|
|
103
107
|
numberOfLines={1}
|
|
104
108
|
userSelect="none"
|
|
105
|
-
color={danger ? "danger" : undefined}
|
|
109
|
+
color={disabled ? "muted" : danger ? "danger" : undefined}
|
|
106
110
|
>
|
|
107
111
|
{title}
|
|
108
112
|
</Text>
|
|
@@ -125,6 +129,7 @@ export function MenuButton(props: MenuButtonProps) {
|
|
|
125
129
|
styles.container,
|
|
126
130
|
highlighted && styles.highlighted,
|
|
127
131
|
focused && !highlighted && styles.focused,
|
|
132
|
+
disabled && styles.disabled,
|
|
128
133
|
style,
|
|
129
134
|
];
|
|
130
135
|
|
|
@@ -182,6 +187,11 @@ const styles = StyleSheet.create({
|
|
|
182
187
|
highlighted: {
|
|
183
188
|
backgroundColor: colors.zinc["100"],
|
|
184
189
|
},
|
|
190
|
+
// A blocked row keeps its geometry and loses its weight — the label and icon
|
|
191
|
+
// go muted above, and the row itself stops inviting the press.
|
|
192
|
+
disabled: {
|
|
193
|
+
opacity: 0.6,
|
|
194
|
+
},
|
|
185
195
|
focused: {
|
|
186
196
|
backgroundColor: colors.zinc["50"],
|
|
187
197
|
},
|
package/src/option_list.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import { ActivityIndicator } from "./activity_indicator";
|
|
|
11
11
|
import { TextInputField } from "./text_input_field";
|
|
12
12
|
import { useScreenSize } from "./use_screen_size";
|
|
13
13
|
import { useOptionList, type UseOptionListParams } from "./use_option_list";
|
|
14
|
+
import { pickerEmptyOptionLabel } from "./picker_empty_option";
|
|
14
15
|
import { useLoticsLocale } from "./locale";
|
|
15
16
|
import type { PickerOption } from "./picker";
|
|
16
17
|
|
|
@@ -61,7 +62,16 @@ export function OptionList<T extends string, MULTI extends boolean = false, D =
|
|
|
61
62
|
const emptyText = props.emptyText ?? loc.noResults;
|
|
62
63
|
const selectAllLabel = props.selectAllLabel ?? loc.selectAll;
|
|
63
64
|
const deselectAllLabel = props.deselectAllLabel ?? loc.deselectAll;
|
|
64
|
-
|
|
65
|
+
// `Picker` names its clear row from the locale; this list is the same row on
|
|
66
|
+
// the popover path, so it takes the same name from the same helper.
|
|
67
|
+
const emptyWord = useLoticsLocale().picker.emptyOption;
|
|
68
|
+
const list = useOptionList({
|
|
69
|
+
...props,
|
|
70
|
+
emptyOptionLabel: pickerEmptyOptionLabel({
|
|
71
|
+
includeEmptyOption: props.includeEmptyOption,
|
|
72
|
+
localeEmptyOption: emptyWord,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
65
75
|
const { small } = useScreenSize();
|
|
66
76
|
|
|
67
77
|
// The option rows set `activeIndex` on hover but have no hover-OUT, so entering
|
package/src/progress_bar.tsx
CHANGED
|
@@ -92,8 +92,15 @@ export function ProgressBar(props: ProgressBarProps) {
|
|
|
92
92
|
<View style={[styles.track, styles.compactTrack]}>
|
|
93
93
|
<View style={[styles.fill, { width: `${percentage}%`, backgroundColor: isComplete ? completeColor : color }]} />
|
|
94
94
|
</View>
|
|
95
|
+
{/* The caption is MUTED at every ratio, matching the non-compact anatomy.
|
|
96
|
+
A meter can mean "done" (a task, an upload) or "used up" (a credit
|
|
97
|
+
limit, a quota), and the same 100% is good in one and bad in the
|
|
98
|
+
other — so the component cannot colour the number without asserting
|
|
99
|
+
something it does not know. It used to draw a green caption beside a
|
|
100
|
+
red bar on every cap meter. The verdict stays where the caller states
|
|
101
|
+
it: the FILL, via `completeColor`. */}
|
|
95
102
|
{format === "none" ? null : (
|
|
96
|
-
<Text size="sm" tabular color=
|
|
103
|
+
<Text size="sm" tabular color="muted">
|
|
97
104
|
{label}
|
|
98
105
|
</Text>
|
|
99
106
|
)}
|
|
@@ -151,6 +158,12 @@ const styles = StyleSheet.create({
|
|
|
151
158
|
},
|
|
152
159
|
compactTrack: {
|
|
153
160
|
flex: 1,
|
|
161
|
+
// A shrink-to-fit parent — a stacked `Table` cell, a `DetailRow` with no
|
|
162
|
+
// `DetailTable` above it — gives `flex: 1` nothing to divide, so the track
|
|
163
|
+
// measures 0 and the meter renders as its caption alone. It passes every
|
|
164
|
+
// treatment probe because there is nothing left to measure. The floor is
|
|
165
|
+
// what makes it a bar rather than a number.
|
|
166
|
+
minWidth: 64,
|
|
154
167
|
},
|
|
155
168
|
header: {
|
|
156
169
|
flexDirection: "row",
|
package/src/record_summary.tsx
CHANGED
|
@@ -102,6 +102,11 @@ const styles = StyleSheet.create({
|
|
|
102
102
|
flexDirection: "row",
|
|
103
103
|
alignItems: "flex-start",
|
|
104
104
|
gap: 16,
|
|
105
|
+
// The metric sizes to its content while `identity` is `flex: 1`, so without
|
|
106
|
+
// this the title is the only thing that can give — and it gave down to three
|
|
107
|
+
// wrapped lines in a 358px column while the metric's note kept its full width.
|
|
108
|
+
// A record's identity is the last thing on the surface that should yield.
|
|
109
|
+
flexWrap: "wrap",
|
|
105
110
|
},
|
|
106
111
|
identity: {
|
|
107
112
|
flex: 1,
|
|
@@ -129,7 +129,10 @@ export function StackedBarChart(props: StackedBarChartProps) {
|
|
|
129
129
|
<View style={styles.leading}>{row.leading}</View>
|
|
130
130
|
) : null}
|
|
131
131
|
<View style={styles.headText}>
|
|
132
|
-
|
|
132
|
+
{/* regular, matching `Breakdown`'s label: the two components draw
|
|
133
|
+
the same row and a card may sit them side by side. The figure
|
|
134
|
+
carries the weight; the series name does not. */}
|
|
135
|
+
<Text size="sm" numberOfLines={2} leading="tight">
|
|
133
136
|
{row.label}
|
|
134
137
|
</Text>
|
|
135
138
|
{row.meta ? (
|
package/src/table_fit.ts
CHANGED
|
@@ -26,6 +26,23 @@ export interface TableFitColumn {
|
|
|
26
26
|
key: string;
|
|
27
27
|
/** Fixed width in px; omit for a flexible column. */
|
|
28
28
|
width?: number;
|
|
29
|
+
/**
|
|
30
|
+
* Fixed CHROME inside this flexible cell, in px — a leading mark, a badge, a
|
|
31
|
+
* swatch, plus its gap. Added to the read floor so the cell keeps
|
|
32
|
+
* {@link FLEX_READ_WIDTH} for the TEXT rather than spending part of it on
|
|
33
|
+
* furniture.
|
|
34
|
+
*
|
|
35
|
+
* `composition.md` requires every register row to carry a leading mark and
|
|
36
|
+
* `tpl_item_list` puts it INSIDE the subject cell (the `leading` slot holds
|
|
37
|
+
* the checkbox), so obeying the kit's own rule silently adds ~50px the fit
|
|
38
|
+
* could not see: a register reports "fitted" while its subject clips. Measured
|
|
39
|
+
* in the wild — a subject handed 154px for strings needing 283, with every
|
|
40
|
+
* other signal green.
|
|
41
|
+
*
|
|
42
|
+
* Ignored on a fixed-`width` column, which already states its total. Default
|
|
43
|
+
* 0, so a caller that passes nothing gets exactly the previous behaviour.
|
|
44
|
+
*/
|
|
45
|
+
lead?: number;
|
|
29
46
|
/** This column's claim on space, **`1` = highest**, the way P1/P2/P3 rank a
|
|
30
47
|
* bug: the column you least want to lose gets the SMALLEST number. Said the
|
|
31
48
|
* other way round it is the same rule — the larger the number, the sooner the
|
|
@@ -133,7 +150,7 @@ export function computeTableFit(
|
|
|
133
150
|
const requiredWidth = (cols: TableFitColumn[]): number => {
|
|
134
151
|
const slots = cols.length + (leading > 0 ? 1 : 0) + (trailing > 0 ? 1 : 0);
|
|
135
152
|
const gaps = Math.max(0, slots - 1) * COLUMN_GAP;
|
|
136
|
-
const colsWidth = cols.reduce((sum, c) => sum + (c.width ?? FLEX_READ_WIDTH), 0);
|
|
153
|
+
const colsWidth = cols.reduce((sum, c) => sum + (c.width ?? FLEX_READ_WIDTH + (c.lead ?? 0)), 0);
|
|
137
154
|
return ROW_H_PADDING + (leading > 0 ? leading : 0) + (trailing > 0 ? trailing : 0) + colsWidth + gaps;
|
|
138
155
|
};
|
|
139
156
|
|
package/src/thumbnail_stack.tsx
CHANGED
|
@@ -62,6 +62,19 @@ export interface ThumbnailStackProps {
|
|
|
62
62
|
items: readonly ThumbnailStackItem[];
|
|
63
63
|
/** Pictures shown before the remainder becomes `+N`. Default 3. */
|
|
64
64
|
max?: number;
|
|
65
|
+
/**
|
|
66
|
+
* Silence the marks for assistive tech — they become decoration.
|
|
67
|
+
*
|
|
68
|
+
* The DEFAULT is to announce each `label`, and that is deliberate: a stack is
|
|
69
|
+
* often the only statement of what is there, and a run of unlabelled pictures
|
|
70
|
+
* is unreadable. Pass this where the row ALREADY names the thing in text, so
|
|
71
|
+
* the pictures do not read it a second time on every row.
|
|
72
|
+
*
|
|
73
|
+
* Note the default is the opposite of `Avatar.announce`, and for the same
|
|
74
|
+
* reason: an avatar almost always sits beside its name, a stack often does
|
|
75
|
+
* not. The default follows where the component actually sits.
|
|
76
|
+
*/
|
|
77
|
+
decorative?: boolean;
|
|
65
78
|
/**
|
|
66
79
|
* A rung on the shared avatar scale — the same scale the marks in the
|
|
67
80
|
* neighbouring columns take, so a row of goods and a row of people sit on one
|
|
@@ -73,7 +86,7 @@ export interface ThumbnailStackProps {
|
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
export function ThumbnailStack(props: ThumbnailStackProps) {
|
|
76
|
-
const { items, max = DEFAULT_MAX, size = "md", style, testID } = props;
|
|
89
|
+
const { items, max = DEFAULT_MAX, size = "md", decorative, style, testID } = props;
|
|
77
90
|
const locale = useLoticsLocale();
|
|
78
91
|
|
|
79
92
|
if (items.length === 0) return null;
|
|
@@ -92,8 +105,10 @@ export function ThumbnailStack(props: ThumbnailStackProps) {
|
|
|
92
105
|
{shown.map((item, index) => (
|
|
93
106
|
<View
|
|
94
107
|
key={item.id}
|
|
95
|
-
|
|
96
|
-
|
|
108
|
+
// Announced by default — a standalone stack is unreadable otherwise.
|
|
109
|
+
// `decorative` silences it where the row already names the thing.
|
|
110
|
+
accessible={decorative ? undefined : true}
|
|
111
|
+
accessibilityLabel={decorative ? undefined : item.label}
|
|
97
112
|
style={{
|
|
98
113
|
width: px,
|
|
99
114
|
height: px,
|
package/src/use_option_list.ts
CHANGED
|
@@ -65,6 +65,10 @@ export interface UseOptionListParams<
|
|
|
65
65
|
onCustomCommit?: (rawQuery: string) => void;
|
|
66
66
|
/** Single-select: a leading "" row that clears the value. */
|
|
67
67
|
includeEmptyOption?: boolean;
|
|
68
|
+
/** What the clear row is CALLED. `OptionList` supplies the locale's word via
|
|
69
|
+
* `pickerEmptyOptionLabel`; the default keeps older callers rendering as
|
|
70
|
+
* before rather than silently gaining a word they never asked for. */
|
|
71
|
+
emptyOptionLabel?: string;
|
|
68
72
|
enableSelectAll?: boolean;
|
|
69
73
|
}
|
|
70
74
|
|
|
@@ -130,6 +134,7 @@ export function useOptionList<T extends string, MULTI extends boolean = false, D
|
|
|
130
134
|
customOptionPlacement = "bottom",
|
|
131
135
|
onCustomCommit,
|
|
132
136
|
includeEmptyOption = false,
|
|
137
|
+
emptyOptionLabel = "",
|
|
133
138
|
enableSelectAll = false,
|
|
134
139
|
} = params;
|
|
135
140
|
|
|
@@ -211,7 +216,13 @@ export function useOptionList<T extends string, MULTI extends boolean = false, D
|
|
|
211
216
|
kind !== "custom" && (multi ? selectedSet.has(option.value) : option.value === singleValue);
|
|
212
217
|
out.push({ kind, option, index: out.length, selected, nativeID: optionId(out.length) });
|
|
213
218
|
};
|
|
214
|
-
|
|
219
|
+
// The clear row is a CHOICE the reader picks, so it is NAMED — `Picker` has
|
|
220
|
+
// always named it through `pickerEmptyOptionLabel`, and this list did not,
|
|
221
|
+
// so every `Select`/`InlineSelect` offering a way back to empty offered it
|
|
222
|
+
// as a blank row with an empty accessible name.
|
|
223
|
+
if (includeEmptyOption && !multi && !searching) {
|
|
224
|
+
push("empty", { value: "" as T, label: emptyOptionLabel });
|
|
225
|
+
}
|
|
215
226
|
if (customRow && customOptionPlacement === "top") push("custom", customRow);
|
|
216
227
|
for (const o of filtered) push("option", o);
|
|
217
228
|
if (customRow && customOptionPlacement === "bottom") push("custom", customRow);
|
|
@@ -221,6 +232,7 @@ export function useOptionList<T extends string, MULTI extends boolean = false, D
|
|
|
221
232
|
customRow,
|
|
222
233
|
customOptionPlacement,
|
|
223
234
|
includeEmptyOption,
|
|
235
|
+
emptyOptionLabel,
|
|
224
236
|
multi,
|
|
225
237
|
searching,
|
|
226
238
|
selectedSet,
|