@lotics/ui 19.0.0 → 19.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 +3 -2
- package/docs/catalog.md +40 -20
- package/docs/data_entry.md +33 -17
- package/package.json +1 -1
- package/src/control_surface.ts +10 -0
- package/src/detail_row.tsx +17 -49
- package/src/field_annotations.tsx +76 -0
- package/src/sources.tsx +11 -1
- package/src/task.tsx +81 -63
package/MIGRATION.md
CHANGED
|
@@ -44,8 +44,9 @@ Porting a row, cell by cell:
|
|
|
44
44
|
- **Every cell becomes one `TaskSubRow`, and it needs a NAME.** The column header the cells
|
|
45
45
|
never had is now the label — write what the value IS ("Due", "Assignee", "Completed on"), not
|
|
46
46
|
what it does.
|
|
47
|
-
- **Drop the wrapper `View`s and the widths.**
|
|
48
|
-
|
|
47
|
+
- **Drop the wrapper `View`s and the per-cell widths.** The label column is the LIST's
|
|
48
|
+
(`TaskList labelWidth`, default `DETAIL_LABEL_WIDTH`) and the value takes the slack, so a row
|
|
49
|
+
passes no width at all — set `labelWidth` once on the list if the vocabulary needs more.
|
|
49
50
|
- **Drop `variant="cell"`.** A cell variant is for a grid; a sub-row's value is a form-variant
|
|
50
51
|
`Inline*` editor (the default) — the zinc-50 chip that says "editable".
|
|
51
52
|
- **A row that needed SCANNABLE columns — the same four values compared down twenty rows — was
|
package/docs/catalog.md
CHANGED
|
@@ -334,9 +334,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
334
334
|
hand-rolled plain DOM/CSS (nothing injects them automatically; `@lotics/ui` components
|
|
335
335
|
don't need them).
|
|
336
336
|
- **`spacing`** — the `SPACE` scale + `SpaceToken`.
|
|
337
|
-
- **`control_surface`** — `CONTROL_HEIGHT` (40) · `CONTROL_RADIUS` (10) · `
|
|
338
|
-
|
|
339
|
-
|
|
337
|
+
- **`control_surface`** — `CONTROL_HEIGHT` (40) · `CONTROL_RADIUS` (10) · `CONTROL_TEXT_INSET`
|
|
338
|
+
(9 — how far a control insets its OWN text: 1px border + 8px padding; anything that must line
|
|
339
|
+
up with a control's WORDS rather than its box carries it, and `TASK_TEXT_INSET` IS it) ·
|
|
340
|
+
`FOCUS_RING` · `HOVER_BORDER` · `CONTROL_TRANSITION` · `chipSurfaceStyle` — the shared
|
|
341
|
+
control-surface tokens.
|
|
340
342
|
- **`fonts.css`** — the Inter sheet (400/500/600, served by absolute URL so it resolves on
|
|
341
343
|
every origin an app runs from); the app entry imports it ONCE or every `Text` falls back
|
|
342
344
|
to system fonts.
|
|
@@ -708,9 +710,12 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
708
710
|
wire both); invalid/cross-field STATE = a co-located `Callout`; **`flat`**
|
|
709
711
|
on a flat-text value row (`InlineStatic`, plain `Text`) tucks the annotation up by the
|
|
710
712
|
control band's slack so its gap matches a chip row's.
|
|
713
|
+
The three annotations are ONE anatomy shared with a task's `TaskSubRow` — same names, same
|
|
714
|
+
order, same alert semantics — because a record's field and a task's field are the same thing
|
|
715
|
+
on two surfaces.
|
|
711
716
|
`DetailTable`: the compound parent of
|
|
712
|
-
a row STACK — `labelWidth` (default `DETAIL_LABEL_WIDTH`, 130 —
|
|
713
|
-
|
|
717
|
+
a row STACK — `labelWidth` (default `DETAIL_LABEL_WIDTH`, 130 — THE kit's label column, and
|
|
718
|
+
a `TaskList`'s default too) / `trailingWidth` / `minHeight` (default 40, the
|
|
714
719
|
inline-control grid) declared ONCE + the 8px row gap; with `trailingWidth` every row
|
|
715
720
|
reserves the trailing column so value cells share one width and trailing items align at
|
|
716
721
|
one x, like a table. RESPONSIVE with no prop: it measures its own container (onLayout, not
|
|
@@ -793,9 +798,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
793
798
|
from a 5-item drawer checklist to a grouped desk board. `TaskList` owns geometry only: the two
|
|
794
799
|
GUTTERS — `controlWidth` (the left one, that every title aligns on, and so the INDENT step) and
|
|
795
800
|
`actionWidth` (the right one, that every ⋯ pins into; `0` on a list whose rows carry no
|
|
796
|
-
actions) — plus `
|
|
797
|
-
|
|
798
|
-
|
|
801
|
+
actions) — plus `labelWidth` (the column every `TaskSubRow` renders its NAME in; default
|
|
802
|
+
`DETAIL_LABEL_WIDTH`, 130) and `density` (`comfortable` = a 44px minimum tap target, `dense` =
|
|
803
|
+
32 for a pointer-driven register). All four are inherited by nested lists. Everything else is
|
|
804
|
+
composed, and JSX order is screen order — nothing inspects child types.
|
|
799
805
|
|
|
800
806
|
`TaskStatus` takes the `CheckCircle` (omit `onChange` for a read-only ring; a PICKER list
|
|
801
807
|
puts a `CheckboxInput` here and sets `controlWidth={24}`). `TaskTitle` takes the
|
|
@@ -805,7 +811,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
805
811
|
`TASK_TEXT_INSET` is never needed and a long title that wraps keeps its first line beside the
|
|
806
812
|
control. It spans the content box between the gutters, like every line hung beneath it.
|
|
807
813
|
`TaskCaption` is the row's state IN WORDS on its own line under the title — a
|
|
808
|
-
sentence, never a field
|
|
814
|
+
sentence about the ROW, never a field, and never a sentence about one FIELD (that is the
|
|
815
|
+
sub-row's own `description` / `warning` / `error`).
|
|
816
|
+
`TaskActions` carries the row's ⋯ `ActionMenu`, pinned into the RIGHT
|
|
809
817
|
gutter on the first line (Delete lives BEHIND it, danger-styled and last, never a bare ✕).
|
|
810
818
|
`TaskDetail` is a FREE-FORM block under the row on the title's text edge — a chart, a table, a
|
|
811
819
|
form with its own submit — rendered only while open. The module also exports
|
|
@@ -822,23 +830,35 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
822
830
|
step inward per level. Declare `actionWidth={0}` when NO row in the list carries actions.
|
|
823
831
|
|
|
824
832
|
**THE ROW CARRIES THE TITLE; THE FIELDS THE USER CAN SET ARE SUB-ROWS BENEATH IT.** One task's
|
|
825
|
-
own fields hang under it as **`TaskSubRow`** (`label` · the control ·
|
|
826
|
-
`
|
|
827
|
-
is expressed by indentation and there is only one device for it. Label and value sit
|
|
828
|
-
so the eye pairs them
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
833
|
+
own fields hang under it as **`TaskSubRow`** (`label` · the control · `description` /
|
|
834
|
+
`warning` / `error`), indented ONE step — the same step a nested `TaskList` takes, because
|
|
835
|
+
belonging is expressed by indentation and there is only one device for it. Label and value sit
|
|
836
|
+
ADJACENT so the eye pairs them. **The label column is the LIST's, not the row's** — one
|
|
837
|
+
`labelWidth` for every sub-row at every depth, exactly as `DetailTable labelWidth` is the
|
|
838
|
+
record's — so every value on the surface starts on ONE x. A name longer than the column WRAPS
|
|
839
|
+
inside it and its value does not move; a column each row sized to its own words is not a
|
|
840
|
+
column (it started a short label's value at one x and a long one's at another, and any
|
|
841
|
+
vocabulary with longer field names staggered nearly every row). The value takes the slack from
|
|
842
|
+
a readable minimum — on a phone or in a narrow drawer it drops onto its own line under the
|
|
843
|
+
label rather than ellipsizing beside it. Pass a form-variant `Inline*` editor (the default),
|
|
844
|
+
not `variant="cell"`: a cell variant belongs to a grid.
|
|
845
|
+
|
|
846
|
+
**A TASK'S FIELD ANNOTATES EXACTLY LIKE A RECORD'S FIELD.** `description` (persistent
|
|
847
|
+
guidance), `warning` (a consequence to weigh — amber, announced) and `error` (field-level
|
|
848
|
+
failure — danger, alert semantics) are the same three props with the same order and a11y as
|
|
849
|
+
`DetailRow`'s, from ONE shared implementation. A fault in a single field belongs ON that
|
|
850
|
+
field; `TaskCaption` is a sentence about the whole ROW, so demoting a field fault into one
|
|
851
|
+
says something different.
|
|
834
852
|
|
|
835
853
|
**ANYTHING THAT NEEDS SCANNABLE COLUMNS IS A `Table`, NOT A `TaskList`.** The compound spent a
|
|
836
854
|
version carrying a declared value column so cells would line up down the list; it aligned and
|
|
837
855
|
it read worse — each field's name ended up a quarter of the surface from its control, sized for
|
|
838
856
|
the widest value on the page. If the job is comparing the same four values across twenty rows,
|
|
839
857
|
that is a table, and `Table`/`DataGrid` are built for it. A `DetailTable labelWidth={…}` inside
|
|
840
|
-
a `TaskDetail` is the other shape to avoid:
|
|
841
|
-
two x positions matching nothing above them — a form
|
|
858
|
+
a `TaskDetail` is the other shape to avoid: the list ALREADY has a label column, so a second
|
|
859
|
+
one lands its label and its value at two more x positions matching nothing above them — a form
|
|
860
|
+
pasted into a list. Fields belonging to the task are `TaskSubRow`s, which annotate the same way
|
|
861
|
+
a `DetailRow` does, so there is nothing left that a nested `DetailTable` can say.
|
|
842
862
|
|
|
843
863
|
**A row's FIRST LINE is a BAND** — 44px `comfortable`, 32 `dense`. Both gutter controls centre
|
|
844
864
|
in it and `TaskTitle` claims it, which is what keeps both columns of
|
package/docs/data_entry.md
CHANGED
|
@@ -272,30 +272,46 @@ second line — hangs below it. That is what keeps a column of controls straight
|
|
|
272
272
|
mixed-height rows, so a control never drifts into the gap under the title it belongs to.
|
|
273
273
|
|
|
274
274
|
A task's own free text is its TITLE, a **`TaskCaption`** or a **`TaskDetail`** — there is no
|
|
275
|
-
fourth place to write, because a fourth place gets written in twice. `TaskCaption`
|
|
276
|
-
state IN WORDS ("3 of 5 papers received", "waiting on the yard") on its own line
|
|
277
|
-
it is not a field, because a field is a NAMED VALUE the reader sets and a caption
|
|
278
|
-
about one row — labelling prose makes it read as a field nobody can edit.
|
|
275
|
+
fourth place to write about the TASK, because a fourth place gets written in twice. `TaskCaption`
|
|
276
|
+
is the row's state IN WORDS ("3 of 5 papers received", "waiting on the yard") on its own line
|
|
277
|
+
under the title; it is not a field, because a field is a NAMED VALUE the reader sets and a caption
|
|
278
|
+
is a SENTENCE about one row — labelling prose makes it read as a field nobody can edit. A sentence
|
|
279
|
+
about one FIELD is not a caption either: it is that sub-row's own `description` / `warning` /
|
|
280
|
+
`error`, which is why the sub-row carries all three.
|
|
279
281
|
|
|
280
282
|
**THE ROW CARRIES THE TITLE; THE FIELDS THE USER CAN SET ARE SUB-ROWS BENEATH IT.** A field
|
|
281
283
|
belonging to ONE task — its due date, its assignee, a portal login, a reference number — hangs
|
|
282
|
-
under it as a **`TaskSubRow`** (`label` · the control ·
|
|
283
|
-
ONE step, with the NAME beside its VALUE so the eye pairs them. Belonging is expressed by
|
|
284
|
-
indentation, which is the same step a nested `TaskList` takes — one device, at every depth.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
284
|
+
under it as a **`TaskSubRow`** (`label` · the control · `description` / `warning` / `error`),
|
|
285
|
+
indented ONE step, with the NAME beside its VALUE so the eye pairs them. Belonging is expressed by
|
|
286
|
+
that indentation, which is the same step a nested `TaskList` takes — one device, at every depth.
|
|
287
|
+
|
|
288
|
+
**The label column belongs to the LIST, not to the row** — `TaskList labelWidth`, defaulting to
|
|
289
|
+
`DETAIL_LABEL_WIDTH` (130), inherited to every depth, exactly as `DetailTable labelWidth` is the
|
|
290
|
+
record's. Every sub-row on the surface therefore starts its value on ONE x, and a name longer than
|
|
291
|
+
the column WRAPS inside it instead of pushing its own value right. A column each row sized to its
|
|
292
|
+
own words is not a column: it lined up the labels of a task whose names happened to be short and
|
|
293
|
+
staggered every other one, and a vocabulary with longer field names than English staggered nearly
|
|
294
|
+
all of them. Raise it once, on the list, for a vocabulary that genuinely needs more. The value
|
|
295
|
+
takes the slack from a readable minimum, dropping onto its own line under the label on a surface
|
|
296
|
+
too narrow to seat both. Use the form-variant `Inline*` editor (the default) — `variant="cell"`
|
|
297
|
+
belongs to a grid.
|
|
298
|
+
|
|
299
|
+
**A TASK'S FIELD ANNOTATES EXACTLY LIKE A RECORD'S FIELD.** `TaskSubRow` carries the same
|
|
300
|
+
`description` / `warning` / `error` as `DetailRow`, from one shared implementation — same tones,
|
|
301
|
+
same severity order (error nearest the control, then warning, then the guidance), same alert
|
|
302
|
+
semantics. A fault in ONE field says so on that field. Demoting it into a `TaskCaption` — a
|
|
303
|
+
sentence about the whole ROW — says something different, and that asymmetry is what forced apps
|
|
304
|
+
to do it.
|
|
290
305
|
|
|
291
306
|
**Anything that needs SCANNABLE COLUMNS is a `Table`, not a `TaskList`.** The compound once
|
|
292
|
-
carried a declared
|
|
307
|
+
carried a declared VALUE column so cells lined up down the list; it aligned, and it read worse —
|
|
293
308
|
each name ended up a quarter of the surface from its control, sized for the widest value on the
|
|
294
309
|
page. Comparing the same four values across twenty rows is a table's job. A `DetailTable` inside a
|
|
295
|
-
`TaskDetail` is the same mistake in miniature:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
310
|
+
`TaskDetail` is the same mistake in miniature: the list already HAS a label column, so a second
|
|
311
|
+
one lands a label and a control at two more x positions matching nothing above them — a form
|
|
312
|
+
pasted into a list, and with sub-row annotations there is nothing left for it to say.
|
|
313
|
+
`TaskDetail` keeps the FREE-FORM block (a chart, a table, a form with its own submit) — it has no
|
|
314
|
+
name to hang on the indent, so a rule down its left edge is what ties it to the row.
|
|
299
315
|
|
|
300
316
|
**The ⋯ is a COLUMN — the mirror of the control gutter.** Both pinned controls sit outside the
|
|
301
317
|
row's flow and the ROW reserves each edge (`controlWidth` on the left, `actionWidth` on the
|
package/package.json
CHANGED
package/src/control_surface.ts
CHANGED
|
@@ -25,6 +25,16 @@ export const CONTROL_RADIUS = 10;
|
|
|
25
25
|
* one look. Apps building their own controls reuse it. */
|
|
26
26
|
export const FOCUS_RING = `0 0 0 2px ${colors.zinc[900]}`;
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* How far a control surface insets its OWN text: 1px of border (transparent on an
|
|
30
|
+
* inline editor at rest) plus 8px of padding. Anything that has to line up with a
|
|
31
|
+
* control's WORDS rather than with its invisible box — a field's annotation under
|
|
32
|
+
* its value, a caption hung beneath a title — carries this same number. It is 9,
|
|
33
|
+
* not 8: reading the padding alone and forgetting the border is what put a whole
|
|
34
|
+
* family of hanging lines 1px left of the text they belonged to.
|
|
35
|
+
*/
|
|
36
|
+
export const CONTROL_TEXT_INSET = 9;
|
|
37
|
+
|
|
28
38
|
/** A bordered control's pointer-hover edge: the resting `border` darkens to this on
|
|
29
39
|
* hover — the premium "this is interactive" affordance that complements the focus
|
|
30
40
|
* ring (focus = the ring; hover = a darker border). Drive it with `useHover` on a
|
package/src/detail_row.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createContext, ReactNode, useContext, useState } from "react";
|
|
2
2
|
import { StyleProp, StyleSheet, View, ViewStyle } from "react-native";
|
|
3
|
+
import { FieldAnnotationProps, FieldAnnotations, hasFieldAnnotation } from "./field_annotations";
|
|
3
4
|
import { INLINE_CONTROL_HEIGHT } from "./inline_edit";
|
|
4
5
|
import { getInputLineHeight } from "./text_utils";
|
|
5
6
|
import { Text } from "./text";
|
|
@@ -20,9 +21,11 @@ const DetailTableContext = createContext<DetailTableColumns | null>(null);
|
|
|
20
21
|
const MIN_VALUE_WIDTH = 160;
|
|
21
22
|
|
|
22
23
|
/** THE kit's label column — ~18 characters, where real field names live. It is
|
|
23
|
-
* this table's default AND
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* this table's default AND a `TaskList`'s, so a record's fields and a task's
|
|
25
|
+
* read as one language on a surface carrying both; that only holds while the
|
|
26
|
+
* two are ONE number. A name longer than the column WRAPS inside it: the
|
|
27
|
+
* column belongs to the SURFACE, so no one field's name may push its own value
|
|
28
|
+
* out of line with the field above it. */
|
|
26
29
|
export const DETAIL_LABEL_WIDTH = 130;
|
|
27
30
|
|
|
28
31
|
export interface DetailTableProps {
|
|
@@ -89,7 +92,7 @@ export function DetailTable(props: DetailTableProps) {
|
|
|
89
92
|
);
|
|
90
93
|
}
|
|
91
94
|
|
|
92
|
-
export interface DetailRowProps {
|
|
95
|
+
export interface DetailRowProps extends FieldAnnotationProps {
|
|
93
96
|
/** The field label — rendered `sm muted`. */
|
|
94
97
|
label: ReactNode;
|
|
95
98
|
/** The value — any node (a `Text`, a `Badge`, a stack). */
|
|
@@ -110,23 +113,6 @@ export interface DetailRowProps {
|
|
|
110
113
|
labelSize?: "xs" | "sm";
|
|
111
114
|
/** Min row height. Default 28 (or the `DetailTable`'s `minHeight`). */
|
|
112
115
|
minHeight?: number;
|
|
113
|
-
/** PERSISTENT guidance — what to enter, or the rule that applies ("Printed
|
|
114
|
-
* on every document"). Renders under the VALUE (the label column has no
|
|
115
|
-
* room for prose); stacked mode mirrors `FormField` (label · description ·
|
|
116
|
-
* control). Always visible — field annotations are EXPLICIT, never a
|
|
117
|
-
* hidden ⓘ gloss. Invalid STATE → `error` or a co-located `Callout`. Same
|
|
118
|
-
* name, same meaning as `FormField.description`. */
|
|
119
|
-
description?: string;
|
|
120
|
-
/** A consequence the user should WEIGH before acting — renders in the same
|
|
121
|
-
* annotation slot in amber (`Text color="warning"`), one tone between the
|
|
122
|
-
* muted `description` and the danger `error`, announced on appearance. Same
|
|
123
|
-
* name, same meaning as `FormField.warning`. */
|
|
124
|
-
warning?: string;
|
|
125
|
-
/** Field-level failure, under the value in danger ink with `FormField`'s
|
|
126
|
-
* alert semantics (announced on appearance). For CONSUMER-validated state —
|
|
127
|
-
* the `Inline*` editors already render their own transient save errors;
|
|
128
|
-
* don't wire both to the same failure. */
|
|
129
|
-
error?: string;
|
|
130
116
|
/** The value renders as FLAT text (an `InlineStatic`, a plain `Text`) rather
|
|
131
117
|
* than a control-height chip. Annotations then tuck up by the control
|
|
132
118
|
* band's slack, so the perceived gap under the TEXT equals the gap under a
|
|
@@ -153,7 +139,7 @@ export function DetailRow(props: DetailRowProps) {
|
|
|
153
139
|
const form = labelWidth != null;
|
|
154
140
|
// Annotations (description / warning / error) live UNDER THE VALUE — the row
|
|
155
141
|
// grows, so the label + trailing center against the CONTROL LINE, not the block.
|
|
156
|
-
const annotated =
|
|
142
|
+
const annotated = hasFieldAnnotation(props);
|
|
157
143
|
// A FLAT value (no painted chip) centers its text in the control band,
|
|
158
144
|
// leaving invisible slack below it — tuck the annotations up by that slack
|
|
159
145
|
// so the perceived gap matches a chip row's. Horizontal mode only (stacked
|
|
@@ -170,29 +156,11 @@ export function DetailRow(props: DetailRowProps) {
|
|
|
170
156
|
</Text>
|
|
171
157
|
</View>
|
|
172
158
|
);
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
const annotations =
|
|
178
|
-
<>
|
|
179
|
-
{error != null ? (
|
|
180
|
-
<Text size="xs" color="danger" accessibilityRole="alert" aria-live="polite" style={styles.annotation}>
|
|
181
|
-
{error}
|
|
182
|
-
</Text>
|
|
183
|
-
) : null}
|
|
184
|
-
{warning != null ? (
|
|
185
|
-
<Text size="xs" color="warning" accessibilityRole="alert" aria-live="polite" style={styles.annotation}>
|
|
186
|
-
{warning}
|
|
187
|
-
</Text>
|
|
188
|
-
) : null}
|
|
189
|
-
{description != null ? (
|
|
190
|
-
<Text size="xs" color="muted" style={styles.annotation}>
|
|
191
|
-
{description}
|
|
192
|
-
</Text>
|
|
193
|
-
) : null}
|
|
194
|
-
</>
|
|
195
|
-
) : null;
|
|
159
|
+
// THE field-annotation anatomy — severity, tones, a11y and the control's own
|
|
160
|
+
// text inset, defined once in `field_annotations` and rendered identically by
|
|
161
|
+
// a task's `TaskSubRow`. A record's field and a task's field are the same
|
|
162
|
+
// thing on two surfaces; they may not say a fault two different ways.
|
|
163
|
+
const annotations = <FieldAnnotations description={description} warning={warning} error={error} />;
|
|
196
164
|
if (table?.stacked) {
|
|
197
165
|
// Stacked mode wears the FORM grammar: the label renders exactly like a
|
|
198
166
|
// `FormField` label (medium, default ink), so a narrow record surface
|
|
@@ -234,12 +202,12 @@ export function DetailRow(props: DetailRowProps) {
|
|
|
234
202
|
{form ? (
|
|
235
203
|
<View style={[styles.value, annotated && styles.valueStack]}>
|
|
236
204
|
<View style={[styles.controlLine, { minHeight }]}>{children}</View>
|
|
237
|
-
{flatTuck !== 0
|
|
205
|
+
{flatTuck !== 0 ? <View style={{ marginTop: flatTuck }}>{annotations}</View> : annotations}
|
|
238
206
|
</View>
|
|
239
207
|
) : (
|
|
240
208
|
<View style={annotated ? styles.valueStack : undefined}>
|
|
241
209
|
<View style={[styles.controlLine, { minHeight }]}>{children}</View>
|
|
242
|
-
{flatTuck !== 0
|
|
210
|
+
{flatTuck !== 0 ? <View style={{ marginTop: flatTuck }}>{annotations}</View> : annotations}
|
|
243
211
|
</View>
|
|
244
212
|
)}
|
|
245
213
|
{table?.trailingWidth != null ? (
|
|
@@ -279,9 +247,9 @@ const styles = StyleSheet.create({
|
|
|
279
247
|
// The first line of the value cell — the control centers in it exactly like
|
|
280
248
|
// the label and trailing cells do.
|
|
281
249
|
controlLine: { justifyContent: "center" },
|
|
250
|
+
// The 2px between the control line and the annotation block beneath it — the
|
|
251
|
+
// same rhythm `FieldAnnotations` puts between its own lines.
|
|
282
252
|
valueStack: { gap: 2 },
|
|
283
|
-
// Aligns annotation text with the inline chip's text (its 8px inset).
|
|
284
|
-
annotation: { paddingLeft: 8 },
|
|
285
253
|
// Form mode (labelWidth set): the value column FILLS the row so every inline
|
|
286
254
|
// editor spans the SAME width — and none jumps wider when it swaps to the
|
|
287
255
|
// (flex:1) edit control. A stretch column, so an InlineEditView / InlineStatic
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { StyleSheet, View } from "react-native";
|
|
2
|
+
import { CONTROL_TEXT_INSET } from "./control_surface";
|
|
3
|
+
import { Text } from "./text";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* What a FIELD says about itself under its own value — ONE anatomy, shared by every field
|
|
7
|
+
* surface in the kit (`DetailRow` on a record, `TaskSubRow` on a task). A record's field and a
|
|
8
|
+
* task's field carry the same three tones, in the same order, with the same a11y, because they
|
|
9
|
+
* ARE the same thing on two surfaces: a name, a value the reader sets, and whatever has to be
|
|
10
|
+
* said about it. Where the two disagreed, an app with a field-level fault had to demote it into
|
|
11
|
+
* prose about the whole ROW — which says something different.
|
|
12
|
+
*
|
|
13
|
+
* Always VISIBLE. A field annotation is never a hidden ⓘ gloss: guidance nobody opens is
|
|
14
|
+
* guidance nobody reads, and a fault behind a disclosure is a fault the reader ships.
|
|
15
|
+
*/
|
|
16
|
+
export interface FieldAnnotationProps {
|
|
17
|
+
/** PERSISTENT guidance — what to enter, or the rule that applies ("Printed on every
|
|
18
|
+
* document"). Muted, never announced: it is true before the reader touches the field and
|
|
19
|
+
* stays true after. An invalid STATE is `error`, not a description written in the past
|
|
20
|
+
* tense. Same name, same meaning as `FormField.description`. */
|
|
21
|
+
description?: string;
|
|
22
|
+
/** A consequence the reader should WEIGH before acting — amber, one tone between the muted
|
|
23
|
+
* `description` and the danger `error`, announced on appearance. It does not block: the
|
|
24
|
+
* value is accepted and something downstream is worse for it. Same name, same meaning as
|
|
25
|
+
* `FormField.warning`. */
|
|
26
|
+
warning?: string;
|
|
27
|
+
/** Field-level FAILURE — danger ink, `FormField`'s alert semantics, announced on appearance.
|
|
28
|
+
* For CONSUMER-validated state: the `Inline*` editors already render their own transient
|
|
29
|
+
* save errors, so don't wire both to one failure. */
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Does this field have anything to say? Hosts ask because an annotated field is a TALLER row,
|
|
34
|
+
* and the label beside it still has to centre on the CONTROL line rather than on the block. */
|
|
35
|
+
export function hasFieldAnnotation(props: FieldAnnotationProps): boolean {
|
|
36
|
+
return props.description != null || props.warning != null || props.error != null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The stack itself — rendered directly beneath the value, `null` when the field has nothing to
|
|
41
|
+
* say.
|
|
42
|
+
*
|
|
43
|
+
* SEVERITY DESCENDS FROM THE CONTROL: error first (adjacent to the value that failed), then
|
|
44
|
+
* warning, then the guidance. All three can be present at once and none suppresses another —
|
|
45
|
+
* they answer different questions ("this is broken" / "this will cost you" / "this is what the
|
|
46
|
+
* field is for"), and hiding the rule that explains the failure is exactly the wrong economy.
|
|
47
|
+
*/
|
|
48
|
+
export function FieldAnnotations(props: FieldAnnotationProps) {
|
|
49
|
+
if (!hasFieldAnnotation(props)) return null;
|
|
50
|
+
return (
|
|
51
|
+
<View style={styles.stack}>
|
|
52
|
+
{props.error != null ? (
|
|
53
|
+
<Text size="xs" color="danger" accessibilityRole="alert" aria-live="polite">
|
|
54
|
+
{props.error}
|
|
55
|
+
</Text>
|
|
56
|
+
) : null}
|
|
57
|
+
{props.warning != null ? (
|
|
58
|
+
<Text size="xs" color="warning" accessibilityRole="alert" aria-live="polite">
|
|
59
|
+
{props.warning}
|
|
60
|
+
</Text>
|
|
61
|
+
) : null}
|
|
62
|
+
{props.description != null ? (
|
|
63
|
+
<Text size="xs" color="muted">
|
|
64
|
+
{props.description}
|
|
65
|
+
</Text>
|
|
66
|
+
) : null}
|
|
67
|
+
</View>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const styles = StyleSheet.create({
|
|
72
|
+
// The control's own text inset, so an annotation lines up with the value's WORDS rather than
|
|
73
|
+
// with the editor's invisible box — and the 2px that separates the lines from each other,
|
|
74
|
+
// which is the same rhythm the host puts between the control and this block.
|
|
75
|
+
stack: { gap: 2, paddingLeft: CONTROL_TEXT_INSET },
|
|
76
|
+
});
|
package/src/sources.tsx
CHANGED
|
@@ -75,7 +75,7 @@ function SourceChip({ source, openable }: { source: SourceRef; openable?: boolea
|
|
|
75
75
|
<View style={[styles.iconDisc, { backgroundColor: tint(k.color, 0.14) }]}>
|
|
76
76
|
<Icon name={k.icon} size={13} color={solid(k.color)} />
|
|
77
77
|
</View>
|
|
78
|
-
<Text size="xs" weight="medium" numberOfLines={1}>
|
|
78
|
+
<Text size="xs" weight="medium" numberOfLines={1} style={styles.label}>
|
|
79
79
|
{source.label}
|
|
80
80
|
</Text>
|
|
81
81
|
{source.detail ? (
|
|
@@ -93,6 +93,14 @@ const styles = StyleSheet.create({
|
|
|
93
93
|
chip: {
|
|
94
94
|
flexDirection: "row",
|
|
95
95
|
alignItems: "center",
|
|
96
|
+
// A source label is a document TITLE and can be long — "Thành phần hồ sơ
|
|
97
|
+
// mua NOXH — Khoản 7 — Sĩ quan, hạ sĩ quan, quân nhân chuyên nghiệp". The
|
|
98
|
+
// chip must never exceed its row: `numberOfLines` alone cannot truncate a
|
|
99
|
+
// flex child that is free to grow, so the chip is capped and the label is
|
|
100
|
+
// the part that gives way (`flexShrink`), keeping the icon, the locator and
|
|
101
|
+
// the open glyph intact — they are what make the chip readable once the
|
|
102
|
+
// title is clipped.
|
|
103
|
+
maxWidth: "100%",
|
|
96
104
|
gap: 8,
|
|
97
105
|
paddingLeft: 5,
|
|
98
106
|
paddingRight: 10,
|
|
@@ -102,8 +110,10 @@ const styles = StyleSheet.create({
|
|
|
102
110
|
borderColor: colors.border,
|
|
103
111
|
backgroundColor: colors.white,
|
|
104
112
|
},
|
|
113
|
+
label: { flexShrink: 1 },
|
|
105
114
|
iconDisc: {
|
|
106
115
|
width: 24,
|
|
116
|
+
flexShrink: 0,
|
|
107
117
|
height: 24,
|
|
108
118
|
borderRadius: 7,
|
|
109
119
|
alignItems: "center",
|
package/src/task.tsx
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { createContext, useContext, type ReactNode } from "react";
|
|
2
2
|
import { StyleSheet, View } from "react-native";
|
|
3
3
|
import { colors } from "./colors";
|
|
4
|
+
import { CONTROL_TEXT_INSET } from "./control_surface";
|
|
4
5
|
import { DETAIL_LABEL_WIDTH } from "./detail_row";
|
|
6
|
+
import { FieldAnnotationProps, FieldAnnotations } from "./field_annotations";
|
|
5
7
|
import { INLINE_CONTROL_HEIGHT } from "./inline_edit";
|
|
6
8
|
import { Text } from "./text";
|
|
7
9
|
import { TASK_ROW_BAND, TASK_ROW_GAP, taskGutter, taskTitleSlack } from "./task_metrics";
|
|
@@ -51,6 +53,14 @@ import { TASK_ROW_BAND, TASK_ROW_GAP, taskGutter, taskTitleSlack } from "./task_
|
|
|
51
53
|
* `TaskDetail` is the exception that proves the indent — a free-form block has no label to hang
|
|
52
54
|
* on it, so it keeps a rule instead.
|
|
53
55
|
*
|
|
56
|
+
* **The sub-row LABEL column is the list's** (`labelWidth`, defaulting to the record surface's
|
|
57
|
+
* `DETAIL_LABEL_WIDTH`), inherited to every depth like the gutters. A name longer than it wraps
|
|
58
|
+
* INSIDE it and its value stays put. The column each sub-row sized to its OWN words looked
|
|
59
|
+
* shared on a list of two-word English names and was not one: a short label started its value at
|
|
60
|
+
* one x, a long one at another, and a vocabulary with longer words than English staggered nearly
|
|
61
|
+
* every row. A column that is not shared is not a column — and this is the label's, never the
|
|
62
|
+
* value's, which is the one this family deleted.
|
|
63
|
+
*
|
|
54
64
|
* **A task list is not a table.** Anything the reader scans DOWN a column — six rows compared on
|
|
55
65
|
* the same four values — is a `Table` or a `DataGrid`, which are built for it. A `TaskList` sized
|
|
56
66
|
* for that is a table with a worse header.
|
|
@@ -70,10 +80,14 @@ import { TASK_ROW_BAND, TASK_ROW_GAP, taskGutter, taskTitleSlack } from "./task_
|
|
|
70
80
|
*
|
|
71
81
|
* **A value and a sentence are different things.** A `TaskSubRow` holds a NAMED value the reader
|
|
72
82
|
* sets; `TaskCaption` holds a sentence ABOUT the row ("Missing 2 of 6") on its own line under the
|
|
73
|
-
* title. Prose given a label reads as a field nobody can edit.
|
|
83
|
+
* title. Prose given a label reads as a field nobody can edit. Which is why a sub-row annotates
|
|
84
|
+
* like a record's field does — `description` · `warning` · `error`, the one anatomy in
|
|
85
|
+
* `field_annotations` — so a fault in ONE field says so on that field instead of being demoted
|
|
86
|
+
* into a sentence about the whole row.
|
|
74
87
|
*
|
|
75
88
|
* There is deliberately no `note` slot. A task's free text is its title, a caption, or detail;
|
|
76
|
-
* a fourth place to write invited writing it twice.
|
|
89
|
+
* a fourth place to write about the TASK invited writing it twice. A sentence about one FIELD is
|
|
90
|
+
* that sub-row's own annotation, not a caption.
|
|
77
91
|
*/
|
|
78
92
|
|
|
79
93
|
interface TaskListContextValue {
|
|
@@ -81,6 +95,8 @@ interface TaskListContextValue {
|
|
|
81
95
|
controlWidth: number;
|
|
82
96
|
/** The RIGHT gutter's control — the row's ⋯. `0` on a list whose rows carry no actions. */
|
|
83
97
|
actionWidth: number;
|
|
98
|
+
/** The label column every `TaskSubRow` on the list renders its name in, at every depth. */
|
|
99
|
+
labelWidth: number;
|
|
84
100
|
/** 0 at the root. Any value above 0 means "I am rendered inside a task". */
|
|
85
101
|
depth: number;
|
|
86
102
|
density: TaskDensity;
|
|
@@ -101,29 +117,11 @@ const TASK_ACTION_WIDTH = 28;
|
|
|
101
117
|
const TaskListContext = createContext<TaskListContextValue>({
|
|
102
118
|
controlWidth: 20,
|
|
103
119
|
actionWidth: TASK_ACTION_WIDTH,
|
|
120
|
+
labelWidth: DETAIL_LABEL_WIDTH,
|
|
104
121
|
depth: 0,
|
|
105
122
|
density: "comfortable",
|
|
106
123
|
});
|
|
107
124
|
|
|
108
|
-
/**
|
|
109
|
-
* A sub-row's label column. It is sized to its OWN content — no measurement, no width from the
|
|
110
|
-
* list — between a floor and a ceiling, which is what makes the labels of ONE task line up with
|
|
111
|
-
* each other: a field name is two or three words, every one of them lands under the floor, so
|
|
112
|
-
* every value in that task starts on the same x. The floor IS the kit's label column
|
|
113
|
-
* (`DETAIL_LABEL_WIDTH`, `DetailTable`'s default) — ~18 characters, where real field names live
|
|
114
|
-
* ("Completed on", "Portal account", "Courier reference") — taken from that one constant rather
|
|
115
|
-
* than matched to it, so a task's fields and a record's cannot drift apart on a surface carrying
|
|
116
|
-
* both.
|
|
117
|
-
*
|
|
118
|
-
* The ceiling is the honest half of the deal. A label longer than the floor takes the width it
|
|
119
|
-
* needs and its value sits outboard of its siblings' — until the ceiling, where it wraps instead,
|
|
120
|
-
* so one verbose name cannot push the whole column off the surface. A TRUE shared column (every
|
|
121
|
-
* label at the widest sibling's width, whatever that is) cannot be had in flexbox without
|
|
122
|
-
* measuring text, and a measured column costs a layout pass, a re-render and a rule this file
|
|
123
|
-
* could not state — for a case that mostly does not arise.
|
|
124
|
-
*/
|
|
125
|
-
const SUB_LABEL_MIN = DETAIL_LABEL_WIDTH;
|
|
126
|
-
const SUB_LABEL_MAX = 168;
|
|
127
125
|
/**
|
|
128
126
|
* The width a value claims before it would rather WRAP. It is set by what a real value needs —
|
|
129
127
|
* an email address, a member's name, a three-word status all run to about 185px — so below it,
|
|
@@ -133,17 +131,14 @@ const SUB_LABEL_MAX = 168;
|
|
|
133
131
|
*/
|
|
134
132
|
const SUB_VALUE_MIN = 200;
|
|
135
133
|
/**
|
|
136
|
-
* How far a `variant="cell"` control insets its own text
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* `variant="cell"` control has to add it, or
|
|
141
|
-
* around it.
|
|
142
|
-
*
|
|
143
|
-
* It was 8 — the padding alone, forgetting the border — which put every caption, detail block
|
|
144
|
-
* and nested list 1px left of the title it belonged to.
|
|
134
|
+
* How far a `variant="cell"` control insets its own text — the kit's `CONTROL_TEXT_INSET` under
|
|
135
|
+
* this family's name, because a task surface reaches for it constantly. Everything that hangs
|
|
136
|
+
* beneath a row — the caption, a sub-row's label, the detail block — adds the same, so it lines
|
|
137
|
+
* up with the WORDS rather than with the editor's invisible box. A STRING title gets it from
|
|
138
|
+
* `TaskTitle`; only a custom title NODE that is not a `variant="cell"` control has to add it, or
|
|
139
|
+
* it sits out of line with the editable titles around it.
|
|
145
140
|
*/
|
|
146
|
-
export const TASK_TEXT_INSET =
|
|
141
|
+
export const TASK_TEXT_INSET = CONTROL_TEXT_INSET;
|
|
147
142
|
|
|
148
143
|
export interface TaskListProps {
|
|
149
144
|
children: ReactNode;
|
|
@@ -163,6 +158,20 @@ export interface TaskListProps {
|
|
|
163
158
|
* nested list inherits it.
|
|
164
159
|
*/
|
|
165
160
|
actionWidth?: number;
|
|
161
|
+
/**
|
|
162
|
+
* The width of the label column EVERY `TaskSubRow` on this list renders its name in, at every
|
|
163
|
+
* depth — the list's, never the row's, exactly as `DetailTable labelWidth` is the record's.
|
|
164
|
+
* Defaults to `DETAIL_LABEL_WIDTH` (130, ~18 characters), taken from that one constant rather
|
|
165
|
+
* than matched to it, so a task's fields and a record's cannot drift apart on a surface
|
|
166
|
+
* carrying both.
|
|
167
|
+
*
|
|
168
|
+
* A name longer than the column WRAPS inside it and its value stays put. That is the whole
|
|
169
|
+
* point: a column each row sized to its OWN words is not a column — it started a short label's
|
|
170
|
+
* value at one x and a long one's at another, on the same list, and a surface whose field
|
|
171
|
+
* names run long (any language with longer words than English) had a different value x on
|
|
172
|
+
* nearly every row. Raise it for a vocabulary that genuinely needs more, on the LIST, once.
|
|
173
|
+
*/
|
|
174
|
+
labelWidth?: number;
|
|
166
175
|
/**
|
|
167
176
|
* `comfortable` (the default) gives every row a 44px first-line BAND — the tap target a
|
|
168
177
|
* finger needs, and what the leading control centres in; `dense` drops to 32px for a
|
|
@@ -183,9 +192,10 @@ export function TaskList(props: TaskListProps) {
|
|
|
183
192
|
const isNested = parent.depth > 0;
|
|
184
193
|
const controlWidth = props.controlWidth ?? parent.controlWidth;
|
|
185
194
|
const actionWidth = props.actionWidth ?? parent.actionWidth;
|
|
195
|
+
const labelWidth = props.labelWidth ?? parent.labelWidth;
|
|
186
196
|
const density = props.density ?? parent.density;
|
|
187
197
|
return (
|
|
188
|
-
<TaskListContext.Provider value={{ controlWidth, actionWidth, depth: parent.depth + 1, density }}>
|
|
198
|
+
<TaskListContext.Provider value={{ controlWidth, actionWidth, labelWidth, depth: parent.depth + 1, density }}>
|
|
189
199
|
<View
|
|
190
200
|
style={[
|
|
191
201
|
styles.list,
|
|
@@ -344,32 +354,40 @@ export function TaskActions(props: { children: ReactNode }) {
|
|
|
344
354
|
);
|
|
345
355
|
}
|
|
346
356
|
|
|
357
|
+
interface TaskSubRowProps extends FieldAnnotationProps {
|
|
358
|
+
/** The field's NAME, rendered muted in the LIST's label column. Longer than the column and it
|
|
359
|
+
* WRAPS inside it — it is never clipped, and it never moves its own value. */
|
|
360
|
+
label: ReactNode;
|
|
361
|
+
/** The value's control — a form-variant `Inline*` editor, a `Text`. It FILLS the cell. */
|
|
362
|
+
children: ReactNode;
|
|
363
|
+
}
|
|
364
|
+
|
|
347
365
|
/**
|
|
348
366
|
* ONE of the task's own FIELDS, hung under the row: its NAME, then its VALUE, adjacent, indented
|
|
349
367
|
* one step so it reads as the task's.
|
|
350
368
|
*
|
|
351
|
-
* The
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
369
|
+
* **The label column belongs to the LIST** (`TaskList labelWidth`, defaulting to the kit's
|
|
370
|
+
* `DETAIL_LABEL_WIDTH`), so every sub-row on the surface starts its value on ONE x, at every
|
|
371
|
+
* depth — the same contract `DetailTable labelWidth` gives a record's fields, because a task's
|
|
372
|
+
* field and a record's field are the same thing on two surfaces. A label longer than the column
|
|
373
|
+
* wraps inside it and its value does not move. The column each row sized to its OWN words was
|
|
374
|
+
* the bug: it aligned the labels of a task whose names happened to be short and staggered every
|
|
375
|
+
* other one, and a surface whose field names run long had a different value x on nearly every
|
|
376
|
+
* row. A column that is not shared is not a column.
|
|
356
377
|
*
|
|
357
378
|
* The value takes the row's slack from a readable minimum: on a narrow surface it drops onto its
|
|
358
379
|
* own line under the label rather than compressing to a few characters, and on a wide one it
|
|
359
380
|
* FILLS the content box, ending on the same x as the title above it and the ⋯ gutter beside it.
|
|
360
381
|
*
|
|
382
|
+
* It annotates exactly like a `DetailRow` — `description` · `warning` · `error`, one shared
|
|
383
|
+
* anatomy (`field_annotations`). A field-level fault says so ON THE FIELD; demoting it into a
|
|
384
|
+
* `TaskCaption`, which is a sentence about the ROW, says something different.
|
|
385
|
+
*
|
|
361
386
|
* **Use `TaskDetail` instead when the block is not a label/value field** — a chart, a whole
|
|
362
387
|
* table, a form with its own submit. (Subtasks are neither: they are a nested `TaskList`.)
|
|
363
388
|
*/
|
|
364
|
-
export function TaskSubRow(props: {
|
|
365
|
-
|
|
366
|
-
label: ReactNode;
|
|
367
|
-
/** The value's control — a form-variant `Inline*` editor, a `Text`. It FILLS the cell. */
|
|
368
|
-
children: ReactNode;
|
|
369
|
-
/** Persistent guidance under the value ("The password is never stored"), on the control's
|
|
370
|
-
* own text inset. Not a state: a fault or a warning about the ROW is a `TaskCaption`. */
|
|
371
|
-
description?: string;
|
|
372
|
-
}) {
|
|
389
|
+
export function TaskSubRow(props: TaskSubRowProps) {
|
|
390
|
+
const { labelWidth } = useContext(TaskListContext);
|
|
373
391
|
// The sub-row's own first-line band. The row's band, unless the density's band cannot contain
|
|
374
392
|
// a form control (a `dense` 32 row against a 40px inline editor) — the label's slack and the
|
|
375
393
|
// control's centre are then BOTH taken from 40, so they still meet on one y.
|
|
@@ -379,18 +397,17 @@ export function TaskSubRow(props: {
|
|
|
379
397
|
<View
|
|
380
398
|
style={[
|
|
381
399
|
styles.subLabel,
|
|
382
|
-
//
|
|
383
|
-
// line on the control beside it, the same law
|
|
384
|
-
|
|
400
|
+
// The LIST's column, and symmetric slack rather than centring — a field name long
|
|
401
|
+
// enough to WRAP keeps its first line on the control beside it, the same law
|
|
402
|
+
// `TaskTitle` follows against the ring.
|
|
403
|
+
{ width: labelWidth, paddingVertical: taskTitleSlack(band) },
|
|
385
404
|
]}
|
|
386
405
|
>
|
|
387
406
|
<Text size="sm" color="muted">{props.label}</Text>
|
|
388
407
|
</View>
|
|
389
408
|
<View style={styles.subValue}>
|
|
390
409
|
<View style={[styles.subControl, { minHeight: band }]}>{props.children}</View>
|
|
391
|
-
{props.description
|
|
392
|
-
<Text size="xs" color="muted" style={styles.subDescription}>{props.description}</Text>
|
|
393
|
-
) : null}
|
|
410
|
+
<FieldAnnotations description={props.description} warning={props.warning} error={props.error} />
|
|
394
411
|
</View>
|
|
395
412
|
</View>
|
|
396
413
|
);
|
|
@@ -460,16 +477,18 @@ const styles = StyleSheet.create({
|
|
|
460
477
|
// rhythm the row uses — and the same wrap: too narrow to seat both and the value drops to its
|
|
461
478
|
// own line under the label.
|
|
462
479
|
subRow: { flexBasis: "100%", flexDirection: "row", flexWrap: "wrap", columnGap: TASK_ROW_GAP },
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
// narrower than the
|
|
466
|
-
//
|
|
467
|
-
//
|
|
480
|
+
// A FIXED column, arriving as the list's `labelWidth` — never sized by this row's own words,
|
|
481
|
+
// which is what puts every value on one x. `flexShrink` is the safety valve alone: a
|
|
482
|
+
// container narrower than the whole column has no line to shrink against, so the label gives
|
|
483
|
+
// way rather than overflowing — and it gives way by the same amount on every row of that
|
|
484
|
+
// list. The inset puts its WORDS on the TITLE's x, not one step in: the control gutter
|
|
485
|
+
// already says which rows are tasks and which are their fields, so an indent on top of it
|
|
486
|
+
// only breaks the single left edge every line on the surface otherwise shares. It is INSIDE
|
|
487
|
+
// the column, so the value starts the same distance from the row's edge as a `DetailRow`'s
|
|
488
|
+
// does on a record.
|
|
468
489
|
subLabel: {
|
|
469
490
|
flexGrow: 0,
|
|
470
491
|
flexShrink: 1,
|
|
471
|
-
minWidth: SUB_LABEL_MIN,
|
|
472
|
-
maxWidth: SUB_LABEL_MAX,
|
|
473
492
|
paddingLeft: TASK_TEXT_INSET,
|
|
474
493
|
},
|
|
475
494
|
// FILLS the rest of the row, exactly as `DetailRow`'s value column does — a sub-row is the
|
|
@@ -477,13 +496,12 @@ const styles = StyleSheet.create({
|
|
|
477
496
|
// left the row reading as a fragment floating under a full-width one. A control too wide for
|
|
478
497
|
// its content is that CONTROL's business (and `DetailRow` would have the same complaint), not
|
|
479
498
|
// a reason for this row to end early.
|
|
480
|
-
|
|
499
|
+
// The 2px `gap` is the air between the control line and any annotation block beneath it — the
|
|
500
|
+
// same rhythm `DetailRow` uses, and inert on the rows that say nothing.
|
|
501
|
+
subValue: { flexGrow: 1, flexShrink: 1, flexBasis: SUB_VALUE_MIN, minWidth: 0, gap: 2 },
|
|
481
502
|
// The control CENTRES in the sub-row's band — the label's first line is pinned to the same
|
|
482
503
|
// centre — and stretches to the cell's full width.
|
|
483
504
|
subControl: { justifyContent: "center" },
|
|
484
|
-
// The control's own text inset, so the guidance lines up with the value it describes rather
|
|
485
|
-
// than with the editor's invisible box.
|
|
486
|
-
subDescription: { paddingLeft: TASK_TEXT_INSET, marginTop: 2 },
|
|
487
505
|
// A full line like the rest, except that its inset arrives as a MARGIN (the rule has to sit on
|
|
488
506
|
// the title's text edge, so it cannot be padding). A 100% basis plus that margin is 9px wider
|
|
489
507
|
// than the line, so `flexShrink` gives the 9 back and the block ends on the content edge with
|