@lotics/ui 46.3.0 → 46.8.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 +14 -1
- package/docs/catalog.md +118 -6
- package/docs/composition.md +26 -0
- package/docs/data_entry.md +56 -2
- package/docs/reviewing.md +34 -0
- package/docs/templates.md +90 -29
- package/examples/tpl_board.tsx +257 -0
- package/examples/tpl_money.tsx +1027 -0
- package/package.json +261 -259
- package/src/accordion.tsx +7 -1
- package/src/board.tsx +611 -0
- package/src/card.tsx +7 -1
- package/src/charge_lines.tsx +373 -0
- package/src/chip_group.tsx +57 -1
- package/src/file_row.tsx +98 -5
- package/src/icon.tsx +6 -0
- package/src/inline_edit.tsx +54 -10
- package/src/inline_number_input.tsx +5 -1
- package/src/inline_text_input.tsx +1 -1
- package/src/locale.tsx +26 -1
- package/src/matrix.tsx +23 -8
- package/src/reference_field.tsx +36 -13
- package/src/table.tsx +6 -1
- package/src/tabs.tsx +1 -1
- package/examples/tpl_report.tsx +0 -410
- package/examples/tpl_statements.tsx +0 -221
package/src/inline_edit.tsx
CHANGED
|
@@ -207,6 +207,12 @@ interface InlineEditFrameProps {
|
|
|
207
207
|
* exists to avoid is not perceptible. `InlineEditView` already renders both;
|
|
208
208
|
* this only stops the frame from narrowing what it forwards. */
|
|
209
209
|
display: string | ReactNode;
|
|
210
|
+
/** Which edge the RESTING value sits on. Money in a column takes "right", so
|
|
211
|
+
* a typed figure shares its edge with the derived one beside it; anything
|
|
212
|
+
* else keeps the default. reviewing.md probe 6: a sound money column lands
|
|
213
|
+
* on TWO right edges (the typed one, the computed one) — a broken one lands
|
|
214
|
+
* on as many as it has rows, and the reader can no longer add it up by eye. */
|
|
215
|
+
align?: "left" | "right";
|
|
210
216
|
placeholder?: string;
|
|
211
217
|
/** Enter edit mode (the view is pressed). */
|
|
212
218
|
onBegin: () => void;
|
|
@@ -256,20 +262,26 @@ interface InlineEditFrameProps {
|
|
|
256
262
|
*/
|
|
257
263
|
function fieldContent(o: {
|
|
258
264
|
display: string | ReactNode;
|
|
265
|
+
/** Which edge the RESTING value sits on. Money in a column takes "right", so
|
|
266
|
+
* a typed figure shares its edge with the derived one beside it; anything
|
|
267
|
+
* else keeps the default. reviewing.md probe 6: a sound money column lands
|
|
268
|
+
* on TWO right edges (the typed one, the computed one) — a broken one lands
|
|
269
|
+
* on as many as it has rows, and the reader can no longer add it up by eye. */
|
|
270
|
+
align?: "left" | "right";
|
|
259
271
|
placeholder?: string | ReactNode;
|
|
260
272
|
struck?: boolean;
|
|
261
273
|
tone?: TextColor;
|
|
262
274
|
trailing?: ReactNode;
|
|
263
275
|
numberOfLines?: number;
|
|
264
276
|
}) {
|
|
265
|
-
const { display, placeholder, struck, tone, trailing, numberOfLines } = o;
|
|
277
|
+
const { display, placeholder, struck, tone, trailing, numberOfLines, align } = o;
|
|
266
278
|
const isEmptyString = typeof display === "string" && display === "";
|
|
267
279
|
return (
|
|
268
280
|
<>
|
|
269
281
|
{isEmptyString && placeholder != null && typeof placeholder !== "string" ? (
|
|
270
282
|
<View style={styles.viewNode}>{placeholder}</View>
|
|
271
283
|
) : typeof display === "string" ? (
|
|
272
|
-
<Text numberOfLines={numberOfLines ?? 1} style={[viewTextStyle, display ? null : styles.placeholder, struck ? styles.struck : null, tone && display && !struck ? { color: getTextColor(tone) } : null]}>
|
|
284
|
+
<Text numberOfLines={numberOfLines ?? 1} style={[viewTextStyle, display ? null : styles.placeholder, struck ? styles.struck : null, tone && display && !struck ? { color: getTextColor(tone) } : null, align === "right" ? styles.viewRight : null]}>
|
|
273
285
|
{display || (typeof placeholder === "string" ? placeholder : "") || "—"}
|
|
274
286
|
</Text>
|
|
275
287
|
) : (
|
|
@@ -293,10 +305,10 @@ function fieldContent(o: {
|
|
|
293
305
|
* ARRIVE (loud on its own), a `framed` field already has one, so it takes the
|
|
294
306
|
* tint as well.
|
|
295
307
|
*/
|
|
296
|
-
function fieldSurface(o: { variant: InlineEditVariant; disabled?: boolean; active?: boolean; editing?: boolean }) {
|
|
308
|
+
function fieldSurface(o: { variant: InlineEditVariant; disabled?: boolean; inert?: boolean; active?: boolean; editing?: boolean }) {
|
|
297
309
|
return (hovered: boolean): StyleProp<ViewStyle>[] => [
|
|
298
310
|
styles.view,
|
|
299
|
-
(o.variant === "bare" || o.disabled) && styles.viewBare,
|
|
311
|
+
(o.variant === "bare" || o.disabled || o.inert) && styles.viewBare,
|
|
300
312
|
o.disabled === true && styles.viewInert,
|
|
301
313
|
// Keyed on the VARIANT alone, never on `disabled` — a disabled `framed`
|
|
302
314
|
// field shares the line above (it must not promise a press) but stays in its
|
|
@@ -305,7 +317,7 @@ function fieldSurface(o: { variant: InlineEditVariant; disabled?: boolean; activ
|
|
|
305
317
|
o.variant === "bare" && styles.viewBareBleed,
|
|
306
318
|
CONTROL_TRANSITION,
|
|
307
319
|
o.active && styles.viewActive,
|
|
308
|
-
!o.active && hovered && !o.disabled && (o.variant === "bare" ? styles.viewHovered : styles.viewHoveredFramed),
|
|
320
|
+
!o.active && hovered && !o.disabled && !o.inert && (o.variant === "bare" ? styles.viewHovered : styles.viewHoveredFramed),
|
|
309
321
|
// While an input is mounted inside, it brings its own height — the surface
|
|
310
322
|
// must not add its resting vertical padding on top.
|
|
311
323
|
o.editing && styles.editHost,
|
|
@@ -373,9 +385,30 @@ interface InlineEditViewProps {
|
|
|
373
385
|
* rendered as-is — e.g. a `MemberChip` / `OptionBadge` for a rich resting
|
|
374
386
|
* value. A node takes the flex slot; the trailing adornment still right-aligns. */
|
|
375
387
|
display: string | ReactNode;
|
|
388
|
+
/** Which edge the RESTING value sits on. Money in a column takes "right", so
|
|
389
|
+
* a typed figure shares its edge with the derived one beside it; anything
|
|
390
|
+
* else keeps the default. reviewing.md probe 6: a sound money column lands
|
|
391
|
+
* on TWO right edges (the typed one, the computed one) — a broken one lands
|
|
392
|
+
* on as many as it has rows, and the reader can no longer add it up by eye. */
|
|
393
|
+
align?: "left" | "right";
|
|
376
394
|
/** Empty-state content. A string reads as muted placeholder text; a NODE (a dashed
|
|
377
395
|
* avatar "add" ghost, a placeholder chip) renders as-is in the value slot. */
|
|
378
396
|
placeholder?: string | ReactNode;
|
|
397
|
+
/**
|
|
398
|
+
* Wear the DISABLED surface — flat, borderless, no hover, no trailing
|
|
399
|
+
* adornment — while staying pressable.
|
|
400
|
+
*
|
|
401
|
+
* The state `disabled` cannot express: a value nothing can CHANGE but anything
|
|
402
|
+
* can still OPEN. A reference on a closed record is the case — its link is
|
|
403
|
+
* fixed and its facts frozen, yet reading who it points at is ordinary. Marked
|
|
404
|
+
* `disabled` it would refuse the press that shows them; left framed it would
|
|
405
|
+
* be the one field on a read-only surface still advertising an edit.
|
|
406
|
+
*
|
|
407
|
+
* Like `disabled` and unlike `variant="bare"`, it does NOT take the −8px
|
|
408
|
+
* bleed: the field stays in the column its editable neighbours sit in, which
|
|
409
|
+
* is the whole reason a read-only surface reads as level.
|
|
410
|
+
*/
|
|
411
|
+
inert?: boolean;
|
|
379
412
|
onPress?: () => void;
|
|
380
413
|
disabled?: boolean;
|
|
381
414
|
accessibilityLabel?: string;
|
|
@@ -402,7 +435,15 @@ interface InlineEditViewProps {
|
|
|
402
435
|
anchorRef?: Ref<View>;
|
|
403
436
|
/** Right-aligned adornment — e.g. a chevron for a select, a spinner while saving.
|
|
404
437
|
* DECORATION only: it renders inside the press target, so it may never hold a
|
|
405
|
-
* control (a button must not contain a button). A control goes in `actions`.
|
|
438
|
+
* control (a button must not contain a button). A control goes in `actions`.
|
|
439
|
+
*
|
|
440
|
+
* **Suppressed while `disabled`.** The adornment is the field's PROMISE that
|
|
441
|
+
* something opens — a chevron says "a list is behind this", a calendar glyph
|
|
442
|
+
* says "a picker is". A disabled field keeps neither promise, and it has
|
|
443
|
+
* already given up the frame and the hover wash, so the glyph is then the only
|
|
444
|
+
* mark left saying "control" — which makes a read-only value read as an
|
|
445
|
+
* editable one that ignores the click. Dropping it is what leaves plain text,
|
|
446
|
+
* which is what a value you cannot change is. */
|
|
406
447
|
trailing?: ReactNode;
|
|
407
448
|
/** Verbs that live ON the field's surface — an `InlineButton` Copy, Open… Use
|
|
408
449
|
* this when the act is ABOUT THE VALUE, so it travels with what it acts on
|
|
@@ -480,7 +521,7 @@ function multilineBox(numberOfLines: number | undefined): ViewStyle | null {
|
|
|
480
521
|
}
|
|
481
522
|
|
|
482
523
|
export function InlineEditView(props: InlineEditViewProps) {
|
|
483
|
-
const { display, placeholder, onPress, disabled, accessibilityLabel, trailing, actions, active, struck, variant = "framed", tone, onFocus, ref, anchorRef, numberOfLines, role = "button" } = props;
|
|
524
|
+
const { display, align, placeholder, onPress, disabled, inert, accessibilityLabel, trailing, actions, active, struck, variant = "framed", tone, onFocus, ref, anchorRef, numberOfLines, role = "button" } = props;
|
|
484
525
|
// A combobox MUST expose its expanded state; a button must not. Deriving it from
|
|
485
526
|
// the `active` the popover callers already pass keeps the two from drifting apart
|
|
486
527
|
// — there is no way to take the role without the announcement.
|
|
@@ -493,8 +534,8 @@ export function InlineEditView(props: InlineEditViewProps) {
|
|
|
493
534
|
onPress();
|
|
494
535
|
}
|
|
495
536
|
: undefined;
|
|
496
|
-
const content = fieldContent({ display, placeholder, struck, tone, trailing, numberOfLines });
|
|
497
|
-
const surface = fieldSurface({ variant, disabled, active });
|
|
537
|
+
const content = fieldContent({ display, align, placeholder, struck, tone, trailing: disabled || inert ? undefined : trailing, numberOfLines });
|
|
538
|
+
const surface = fieldSurface({ variant, disabled, inert, active });
|
|
498
539
|
|
|
499
540
|
// WITH VERBS: the shell owns the surface, the pressable is just the value
|
|
500
541
|
// region inside it. `ref` stays on the PRESSABLE — it means "the focusable
|
|
@@ -557,6 +598,7 @@ export function InlineEditFrame(props: InlineEditFrameProps) {
|
|
|
557
598
|
editing,
|
|
558
599
|
editOpen,
|
|
559
600
|
display,
|
|
601
|
+
align,
|
|
560
602
|
placeholder,
|
|
561
603
|
onBegin,
|
|
562
604
|
children,
|
|
@@ -604,6 +646,7 @@ export function InlineEditFrame(props: InlineEditFrameProps) {
|
|
|
604
646
|
<InlineEditView
|
|
605
647
|
ref={viewRef}
|
|
606
648
|
variant={variant}
|
|
649
|
+
align={align}
|
|
607
650
|
display={display}
|
|
608
651
|
placeholder={placeholder}
|
|
609
652
|
onPress={onBegin}
|
|
@@ -662,7 +705,7 @@ export function InlineEditFrame(props: InlineEditFrameProps) {
|
|
|
662
705
|
accessibilityLabel={accessibilityLabel}
|
|
663
706
|
style={styles.viewInner}
|
|
664
707
|
>
|
|
665
|
-
{fieldContent({ display, placeholder, struck, trailing: affordance, numberOfLines })}
|
|
708
|
+
{fieldContent({ display, align, placeholder, struck, trailing: disabled ? undefined : affordance, numberOfLines })}
|
|
666
709
|
</Pressable>
|
|
667
710
|
)}
|
|
668
711
|
</InlineFieldShell>
|
|
@@ -814,6 +857,7 @@ const styles = StyleSheet.create({
|
|
|
814
857
|
borderColor: HOVER_BORDER,
|
|
815
858
|
},
|
|
816
859
|
placeholder: { color: colors.zinc[400] },
|
|
860
|
+
viewRight: { textAlign: "right" as const },
|
|
817
861
|
struck: { textDecorationLine: "line-through", color: colors.zinc[500] },
|
|
818
862
|
// `textAlign` is not cosmetic here: the resting box is a button-role Pressable,
|
|
819
863
|
// so the UA stylesheet centres every string inside it. A STRING display never
|
|
@@ -20,6 +20,9 @@ export interface InlineNumberInputProps {
|
|
|
20
20
|
* BOTH modes so the field never changes width on focus; pass it
|
|
21
21
|
* UNCONDITIONALLY and `disabled` the verb when it has nothing to act on. */
|
|
22
22
|
actions?: ReactNode;
|
|
23
|
+
/** Which edge the RESTING number sits on. A money column takes "right" so the
|
|
24
|
+
* typed figure shares its edge with the derived one beside it. */
|
|
25
|
+
align?: "left" | "right";
|
|
23
26
|
accessibilityLabel?: string;
|
|
24
27
|
}
|
|
25
28
|
|
|
@@ -29,7 +32,7 @@ export interface InlineNumberInputProps {
|
|
|
29
32
|
* the bare numeric field at the same height, so the form never reflows.
|
|
30
33
|
*/
|
|
31
34
|
export function InlineNumberInput(props: InlineNumberInputProps) {
|
|
32
|
-
const { value, onSave, format, placeholder, min, max, controls = "blur", disabled, accessibilityLabel, variant, actions } = props;
|
|
35
|
+
const { value, onSave, format, placeholder, min, max, controls = "blur", disabled, accessibilityLabel, variant, actions, align } = props;
|
|
33
36
|
const edit = useInlineEdit<number | null>({ value, onSave });
|
|
34
37
|
|
|
35
38
|
const onKeyDown = useCallback(
|
|
@@ -52,6 +55,7 @@ export function InlineNumberInput(props: InlineNumberInputProps) {
|
|
|
52
55
|
return (
|
|
53
56
|
<InlineEditFrame
|
|
54
57
|
variant={variant}
|
|
58
|
+
align={align}
|
|
55
59
|
actions={actions}
|
|
56
60
|
editing={edit.editing}
|
|
57
61
|
display={display}
|
|
@@ -210,7 +210,7 @@ export function InlineTextInput(props: InlineTextInputProps) {
|
|
|
210
210
|
// moving to one element silently dropped it: a done task on the board kept
|
|
211
211
|
// its title upright. Anything else the view used to render has to move the
|
|
212
212
|
// same way, or it goes the same way — quietly.
|
|
213
|
-
variant={variant}
|
|
213
|
+
variant={disabled ? "bare" : variant}
|
|
214
214
|
style={
|
|
215
215
|
struck
|
|
216
216
|
? { textDecorationLine: "line-through" as const, color: colors.zinc[500] }
|
package/src/locale.tsx
CHANGED
|
@@ -72,6 +72,9 @@ export interface LoticsLocale {
|
|
|
72
72
|
inline: { saveError: string; save: string; cancel: string };
|
|
73
73
|
/** `ChoiceList` custom-answer placeholder + `ClarifyWizard` navigation chrome. */
|
|
74
74
|
clarify: { otherPlaceholder: string; back: string; next: string; cancel: string; submit: string };
|
|
75
|
+
/** `ChargeLines`: the screen-reader names of a charge line's three money
|
|
76
|
+
* fields, each qualified by the charge it belongs to. */
|
|
77
|
+
chargeLines: { quantity: (label: string) => string; unitPrice: (label: string) => string; amount: (label: string) => string };
|
|
75
78
|
/** `Ledger`: the screen-reader name of a peekable row. */
|
|
76
79
|
ledger: { rowDetails: (label: string) => string };
|
|
77
80
|
/** `RunningLedger`: the closing line's label ("Số dư hiện tại", "Tồn kho
|
|
@@ -111,6 +114,14 @@ export interface LoticsLocale {
|
|
|
111
114
|
chip: { remove: string };
|
|
112
115
|
/** `SequenceItem`: the reorder + drop controls on one position of a `Sequence`. */
|
|
113
116
|
sequence: { moveUp: string; moveDown: string; remove: string };
|
|
117
|
+
/** `Board` / `BoardCard`: the card's keyboard door, its move control, the
|
|
118
|
+
* destination menu's items, and a column holding nothing. */
|
|
119
|
+
board: {
|
|
120
|
+
open: (title: string) => string;
|
|
121
|
+
move: (title: string) => string;
|
|
122
|
+
moveTo: (column: string) => string;
|
|
123
|
+
empty: string;
|
|
124
|
+
};
|
|
114
125
|
/** `TrendFooter`: the direction words of the trend sentence. */
|
|
115
126
|
trendFooter: { up: string; down: string };
|
|
116
127
|
/** The countdown vocabulary — see `deadline.ts`. 0 and 1 get their own words
|
|
@@ -323,6 +334,7 @@ export const en: LoticsLocale = {
|
|
|
323
334
|
drawer: { previous: "Previous record", next: "Next record", close: "Close" },
|
|
324
335
|
inline: { saveError: "Couldn't save. Try again.", save: "Save", cancel: "Cancel" },
|
|
325
336
|
clarify: { otherPlaceholder: "Or type your own answer…", back: "Back", next: "Next", cancel: "Cancel", submit: "Submit" },
|
|
337
|
+
chargeLines: { quantity: (label) => `Quantity for ${label}`, unitPrice: (label) => `Unit price for ${label}`, amount: (label) => `Amount for ${label}` },
|
|
326
338
|
ledger: { rowDetails: (label) => `${label} details` },
|
|
327
339
|
runningLedger: { currentBalance: "Current balance" },
|
|
328
340
|
stepper: { complete: "Complete step", progress: "Progress" },
|
|
@@ -338,6 +350,12 @@ export const en: LoticsLocale = {
|
|
|
338
350
|
errorState: { retry: "Try again" },
|
|
339
351
|
chip: { remove: "Remove" },
|
|
340
352
|
sequence: { moveUp: "Move up", moveDown: "Move down", remove: "Remove" },
|
|
353
|
+
board: {
|
|
354
|
+
open: (title) => `Open ${title}`,
|
|
355
|
+
move: (title) => `Move ${title}`,
|
|
356
|
+
moveTo: (column) => `Move to ${column}`,
|
|
357
|
+
empty: "Nothing here",
|
|
358
|
+
},
|
|
341
359
|
trendFooter: { up: "Up", down: "Down" },
|
|
342
360
|
deadline: {
|
|
343
361
|
overdue: (days) => `${days} ${days === 1 ? "day" : "days"} overdue`,
|
|
@@ -513,7 +531,7 @@ export const vi: LoticsLocale = {
|
|
|
513
531
|
ascending: " (tăng dần)",
|
|
514
532
|
descending: " (giảm dần)",
|
|
515
533
|
},
|
|
516
|
-
referenceField: { open: "Mở", change: "Đổi", clear: "Bỏ chọn", edit: "Sửa", save: "Lưu", saving: "Đang lưu…", cancel: "
|
|
534
|
+
referenceField: { open: "Mở", change: "Đổi", clear: "Bỏ chọn", edit: "Sửa", save: "Lưu", saving: "Đang lưu…", cancel: "Hủy" },
|
|
517
535
|
optionList: { selectAll: "Chọn tất cả", deselectAll: "Bỏ chọn tất cả", clear: "Xóa", noResults: "Không có kết quả", recent: "Gần đây", searchPlaceholder: "Tìm…", someSelected: "một số đã chọn" },
|
|
518
536
|
picker: { emptyOption: "Không có" },
|
|
519
537
|
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 đủ" },
|
|
@@ -525,6 +543,7 @@ export const vi: LoticsLocale = {
|
|
|
525
543
|
drawer: { previous: "Bản ghi trước", next: "Bản ghi sau", close: "Đóng" },
|
|
526
544
|
inline: { saveError: "Không lưu được. Thử lại.", save: "Lưu", cancel: "Hủy" },
|
|
527
545
|
clarify: { otherPlaceholder: "Hoặc nhập câu trả lời khác…", back: "Quay lại", next: "Tiếp", cancel: "Hủy", submit: "Gửi" },
|
|
546
|
+
chargeLines: { quantity: (label) => `Số lượng ${label}`, unitPrice: (label) => `Đơn giá ${label}`, amount: (label) => `Số tiền ${label}` },
|
|
528
547
|
ledger: { rowDetails: (label) => `Chi tiết ${label}` },
|
|
529
548
|
runningLedger: { currentBalance: "Số dư hiện tại" },
|
|
530
549
|
stepper: { complete: "Hoàn thành bước", progress: "Tiến trình" },
|
|
@@ -540,6 +559,12 @@ export const vi: LoticsLocale = {
|
|
|
540
559
|
errorState: { retry: "Thử lại" },
|
|
541
560
|
chip: { remove: "Xóa" },
|
|
542
561
|
sequence: { moveUp: "Lên trên", moveDown: "Xuống dưới", remove: "Xóa" },
|
|
562
|
+
board: {
|
|
563
|
+
open: (title) => `Mở ${title}`,
|
|
564
|
+
move: (title) => `Chuyển ${title}`,
|
|
565
|
+
moveTo: (column) => `Chuyển sang ${column}`,
|
|
566
|
+
empty: "Chưa có gì",
|
|
567
|
+
},
|
|
543
568
|
trendFooter: { up: "Tăng", down: "Giảm" },
|
|
544
569
|
deadline: {
|
|
545
570
|
overdue: (days) => `Quá hạn ${days} ngày`,
|
package/src/matrix.tsx
CHANGED
|
@@ -22,7 +22,6 @@ interface MatrixContextValue {
|
|
|
22
22
|
heatBase: string;
|
|
23
23
|
formatValue: (n: number) => string;
|
|
24
24
|
rowLabelWidth: number;
|
|
25
|
-
totalColWidth: number;
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
/** Gap between a matrix's columns. Exported because a caller sizing its own
|
|
@@ -110,7 +109,6 @@ export function Matrix(props: MatrixProps) {
|
|
|
110
109
|
heatBase: colors[heatColor][600],
|
|
111
110
|
formatValue,
|
|
112
111
|
rowLabelWidth,
|
|
113
|
-
totalColWidth: 64,
|
|
114
112
|
};
|
|
115
113
|
|
|
116
114
|
return (
|
|
@@ -128,7 +126,7 @@ export interface MatrixHeaderProps {
|
|
|
128
126
|
}
|
|
129
127
|
|
|
130
128
|
function MatrixHeader({ corner, totalLabel }: MatrixHeaderProps) {
|
|
131
|
-
const { cols, rowLabelWidth,
|
|
129
|
+
const { cols, rowLabelWidth, hasTotals } = useMatrix();
|
|
132
130
|
const locale = useLoticsLocale();
|
|
133
131
|
const resolvedTotalLabel = totalLabel ?? locale.matrix.total;
|
|
134
132
|
return (
|
|
@@ -148,7 +146,7 @@ function MatrixHeader({ corner, totalLabel }: MatrixHeaderProps) {
|
|
|
148
146
|
</Text>
|
|
149
147
|
))}
|
|
150
148
|
{hasTotals ? (
|
|
151
|
-
<Text size="sm" color="muted" weight="medium" align="right" numberOfLines={1} style={
|
|
149
|
+
<Text size="sm" color="muted" weight="medium" align="right" numberOfLines={1} style={styles.totalCol}>
|
|
152
150
|
{resolvedTotalLabel}
|
|
153
151
|
</Text>
|
|
154
152
|
) : null}
|
|
@@ -163,7 +161,7 @@ export interface MatrixGridProps {
|
|
|
163
161
|
|
|
164
162
|
function MatrixGrid({ display = "both" }: MatrixGridProps) {
|
|
165
163
|
const ctx = useMatrix();
|
|
166
|
-
const { rows, cols, value, hasTotals, rowTotals, formatValue, rowLabelWidth
|
|
164
|
+
const { rows, cols, value, hasTotals, rowTotals, formatValue, rowLabelWidth } = ctx;
|
|
167
165
|
return (
|
|
168
166
|
<View style={styles.grid}>
|
|
169
167
|
{rows.map((r) => (
|
|
@@ -175,7 +173,7 @@ function MatrixGrid({ display = "both" }: MatrixGridProps) {
|
|
|
175
173
|
<MatrixCell key={c.key} ctx={ctx} display={display} row={r} col={c} value={value(r.key, c.key)} />
|
|
176
174
|
))}
|
|
177
175
|
{hasTotals ? (
|
|
178
|
-
<Text size="sm" weight="semibold" tabular align="right" numberOfLines={1} style={
|
|
176
|
+
<Text size="sm" weight="semibold" tabular align="right" numberOfLines={1} style={styles.totalCol}>
|
|
179
177
|
{formatValue(rowTotals.get(r.key) ?? 0)}
|
|
180
178
|
</Text>
|
|
181
179
|
) : null}
|
|
@@ -251,7 +249,7 @@ export interface MatrixTotalsProps {
|
|
|
251
249
|
}
|
|
252
250
|
|
|
253
251
|
function MatrixTotals({ label }: MatrixTotalsProps) {
|
|
254
|
-
const { cols, colTotals, grandTotal, formatValue, rowLabelWidth
|
|
252
|
+
const { cols, colTotals, grandTotal, formatValue, rowLabelWidth } = useMatrix();
|
|
255
253
|
const locale = useLoticsLocale();
|
|
256
254
|
return (
|
|
257
255
|
<View style={styles.totalsRow}>
|
|
@@ -263,7 +261,7 @@ function MatrixTotals({ label }: MatrixTotalsProps) {
|
|
|
263
261
|
{formatValue(colTotals.get(c.key) ?? 0)}
|
|
264
262
|
</Text>
|
|
265
263
|
))}
|
|
266
|
-
<Text size="sm" weight="semibold" tabular align="right" numberOfLines={1} style={
|
|
264
|
+
<Text size="sm" weight="semibold" tabular align="right" numberOfLines={1} style={styles.totalCol}>
|
|
267
265
|
{formatValue(grandTotal)}
|
|
268
266
|
</Text>
|
|
269
267
|
</View>
|
|
@@ -344,6 +342,23 @@ const styles = StyleSheet.create({
|
|
|
344
342
|
paddingRight: 2,
|
|
345
343
|
},
|
|
346
344
|
totalCol: {
|
|
345
|
+
// SIZED LIKE A DATA COLUMN, not to a fixed number. It was `width: 64`, and a
|
|
346
|
+
// total is by construction the LONGEST figure in its row — so the one number
|
|
347
|
+
// the pivot exists to state was the one guaranteed to hit the clamp first. In
|
|
348
|
+
// VND it does: `formatMoney(n, {compact:true})` gives "1,51 tỷ ₫" for a grand
|
|
349
|
+
// total whose row cells read "231 tr ₫", and at 64px the grand total rendered
|
|
350
|
+
// "1,51 t…" while every cell above it fit. Nothing in the type system or a
|
|
351
|
+
// snapshot sees that — the string is whole in the DOM — so it ships.
|
|
352
|
+
//
|
|
353
|
+
// A per-glyph width estimate would be the wrong repair (a font decides glyph
|
|
354
|
+
// widths, so the arithmetic is right only at the size it was measured at).
|
|
355
|
+
// The structural answer is that a total is the same KIND of figure as a cell
|
|
356
|
+
// at the same type size, so it takes the same sizing law: one flex share of
|
|
357
|
+
// the leftover, floored. The floor is the old fixed width, so a matrix with
|
|
358
|
+
// many columns in a narrow frame never gets a total column narrower than it
|
|
359
|
+
// used to have.
|
|
360
|
+
flex: 1,
|
|
361
|
+
minWidth: 64,
|
|
347
362
|
paddingLeft: 4,
|
|
348
363
|
},
|
|
349
364
|
totalsRow: {
|
package/src/reference_field.tsx
CHANGED
|
@@ -113,15 +113,15 @@ export interface ReferenceFieldProps {
|
|
|
113
113
|
onOpen?: () => void;
|
|
114
114
|
openLabel?: string;
|
|
115
115
|
/**
|
|
116
|
-
* Point this field at a DIFFERENT record.
|
|
116
|
+
* Point this field at a DIFFERENT record.
|
|
117
117
|
*
|
|
118
118
|
* Unsets the reference so the call site's picker returns, and the caller
|
|
119
119
|
* should hand that picker focus: the press said "wrong one", so the next move
|
|
120
120
|
* is choosing the right one and the correction should cost one gesture.
|
|
121
121
|
*/
|
|
122
|
-
onChange
|
|
122
|
+
onChange?: () => void;
|
|
123
123
|
/**
|
|
124
|
-
* Leave the reference EMPTY — the plain detach.
|
|
124
|
+
* Leave the reference EMPTY — the plain detach.
|
|
125
125
|
*
|
|
126
126
|
* The SAME write as `onChange`; they differ only in FOLLOW-THROUGH, and
|
|
127
127
|
* nothing focuses after this one — auto-opening a picker would argue with the
|
|
@@ -131,14 +131,21 @@ export interface ReferenceFieldProps {
|
|
|
131
131
|
* either steals the caret from someone who wanted an empty field or sends
|
|
132
132
|
* someone who wanted a swap hunting for the input.
|
|
133
133
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
134
|
+
* They come as a PAIR — both, or neither. Supplying one alone enforces
|
|
135
|
+
* nothing: Change, then decline to pick, lands on the same empty state Clear
|
|
136
|
+
* reaches, so hiding Clear only removes the direct route to a place the reader
|
|
137
|
+
* can already get to, and makes the footer's shape vary for no gain. Whether
|
|
138
|
+
* empty is VALID is the row's business (its `warning`, its validation), never
|
|
139
|
+
* the peek's.
|
|
140
|
+
*
|
|
141
|
+
* **Omitting BOTH is different, and it is the read-only reference.** On a
|
|
142
|
+
* record that is closed — departed, posted, archived — nothing about the link
|
|
143
|
+
* may change, and a Change button that renders and then declines to act is the
|
|
144
|
+
* same broken promise as a disabled field still wearing its chevron. Without
|
|
145
|
+
* them the peek keeps its facts and its Open and simply carries no link verbs,
|
|
146
|
+
* which is what a reference you can read but not re-point looks like.
|
|
140
147
|
*/
|
|
141
|
-
onClear
|
|
148
|
+
onClear?: () => void;
|
|
142
149
|
/**
|
|
143
150
|
* Commit the draft. Receives ONLY the facts whose value CHANGED, keyed by
|
|
144
151
|
* `name` — never a full snapshot, so a lock or a `before_update` hook sees the
|
|
@@ -163,6 +170,10 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
163
170
|
const [error, setError] = useState<string | null>(null);
|
|
164
171
|
const editing = draft !== null;
|
|
165
172
|
const editable = onSave != null && facts.some((f) => f.name != null);
|
|
173
|
+
/** Chân trang chỉ có lý do tồn tại khi nó chở một động từ. Không có gì để
|
|
174
|
+
* đổi, để bỏ, để sửa hay để mở thì cái peek chỉ còn là các dữ kiện — và một
|
|
175
|
+
* đường kẻ dưới chúng chỉ nói rằng có thêm thứ gì đó, mà không có. */
|
|
176
|
+
const coVerb = editing || onChange != null || onClear != null || editable || onOpen != null;
|
|
166
177
|
|
|
167
178
|
const openDraft = () => {
|
|
168
179
|
const seed: Record<string, string> = {};
|
|
@@ -239,6 +250,12 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
239
250
|
<InlineEditView
|
|
240
251
|
anchorRef={anchor}
|
|
241
252
|
accessibilityLabel={accessibilityLabel}
|
|
253
|
+
/* Nothing here can be changed — no re-point, no detach, no fact edit —
|
|
254
|
+
so the field stops advertising one and rests like the read-only values
|
|
255
|
+
beside it. The peek still opens: reading who a closed record points at
|
|
256
|
+
is ordinary. Derived rather than a prop, so a caller cannot hand out
|
|
257
|
+
an editable surface over verbs it never passed. */
|
|
258
|
+
inert={onChange == null && onClear == null && onSave == null}
|
|
242
259
|
onPress={() => setPeekOpen(true)}
|
|
243
260
|
active={peekOpen}
|
|
244
261
|
/* THE MARKER — what says this value is a RECORD and not typed text.
|
|
@@ -395,6 +412,7 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
395
412
|
record being edited, one navigates off it — so offering either here
|
|
396
413
|
would be offering to lose the typing. Cancel and Save are the only
|
|
397
414
|
two exits, which is also what the pinned popover promised. */}
|
|
415
|
+
{coVerb ? (
|
|
398
416
|
<PopoverFooter align={editing ? "end" : "space-between"}>
|
|
399
417
|
{editing ? (
|
|
400
418
|
<>
|
|
@@ -415,16 +433,18 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
415
433
|
</>
|
|
416
434
|
) : (
|
|
417
435
|
<>
|
|
418
|
-
{/* LEFT acts on the LINK — which record this points at.
|
|
419
|
-
|
|
420
|
-
|
|
436
|
+
{/* LEFT acts on the LINK — which record this points at. The two
|
|
437
|
+
travel together, so the footer has ONE shape wherever the link
|
|
438
|
+
can be re-pointed, and no left group at all where it cannot. */}
|
|
421
439
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
|
|
440
|
+
{onChange ? (
|
|
422
441
|
<Button
|
|
423
442
|
title={t.change}
|
|
424
443
|
color="secondary"
|
|
425
444
|
accessibilityLabel={`${t.change} — ${name}`}
|
|
426
445
|
onPress={() => { setPeekOpen(false); onChange(); }}
|
|
427
446
|
/>
|
|
447
|
+
) : null}
|
|
428
448
|
{/* Same resting surface as `change` beside it. It had NO
|
|
429
449
|
color — which renders transparent, borderless and
|
|
430
450
|
undecorated, i.e. the hover-only affordance composition.md
|
|
@@ -433,12 +453,14 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
433
453
|
before they read any label. Being the least-reached verb is
|
|
434
454
|
said by PLACEMENT — second in the left group, far from the
|
|
435
455
|
primary — not by drawing nothing. */}
|
|
456
|
+
{onClear ? (
|
|
436
457
|
<Button
|
|
437
458
|
title={t.clear}
|
|
438
459
|
color="secondary"
|
|
439
460
|
accessibilityLabel={`${t.clear} — ${name}`}
|
|
440
461
|
onPress={() => { setPeekOpen(false); onClear(); }}
|
|
441
462
|
/>
|
|
463
|
+
) : null}
|
|
442
464
|
</View>
|
|
443
465
|
{/* RIGHT acts on the RECORD the link points at. Edit takes the ONE
|
|
444
466
|
filled-dark rung because it is the only verb here that leads to
|
|
@@ -459,6 +481,7 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
459
481
|
</>
|
|
460
482
|
)}
|
|
461
483
|
</PopoverFooter>
|
|
484
|
+
) : null}
|
|
462
485
|
</PopoverContent>
|
|
463
486
|
</Popover>
|
|
464
487
|
);
|
package/src/table.tsx
CHANGED
|
@@ -331,7 +331,12 @@ export function TableGroup(props: TableGroupProps) {
|
|
|
331
331
|
return (
|
|
332
332
|
<View>
|
|
333
333
|
<View style={styles.groupHeading}>
|
|
334
|
-
{
|
|
334
|
+
{/* Ô này giữ chỗ kể cả khi băng không có màu: bỏ hẳn nó đi thì nhãn của
|
|
335
|
+
băng trung tính bắt đầu sớm hơn nhãn băng có màu đúng một chấm cộng
|
|
336
|
+
khoảng cách, và hai băng nằm ngay cạnh nhau trong cùng một bảng. */}
|
|
337
|
+
<View
|
|
338
|
+
style={[styles.groupDot, color ? { backgroundColor: solid(color) } : { opacity: 0 }]}
|
|
339
|
+
/>
|
|
335
340
|
<Text size="sm" weight="semibold">
|
|
336
341
|
{label}
|
|
337
342
|
</Text>
|
package/src/tabs.tsx
CHANGED
|
@@ -77,7 +77,7 @@ export function Tabs<T extends string>(props: TabsProps<T>) {
|
|
|
77
77
|
|
|
78
78
|
return (
|
|
79
79
|
<View
|
|
80
|
-
style={{ flexDirection: "row",
|
|
80
|
+
style={{ flexDirection: "row", flexWrap: "wrap", columnGap: 4, rowGap: 4 }}
|
|
81
81
|
accessibilityRole="tablist"
|
|
82
82
|
accessibilityLabel={accessibilityLabel}
|
|
83
83
|
>
|