@lotics/ui 22.3.0 → 23.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.
Files changed (70) hide show
  1. package/AGENTS.md +9 -2
  2. package/MIGRATION.md +142 -2
  3. package/docs/ai_patterns.md +22 -22
  4. package/docs/catalog.md +131 -99
  5. package/docs/composition.md +134 -42
  6. package/docs/data_entry.md +67 -31
  7. package/docs/templates.md +86 -68
  8. package/examples/tpl_allocate.tsx +5 -5
  9. package/examples/tpl_attendance.tsx +2 -2
  10. package/examples/tpl_calendar.tsx +6 -6
  11. package/examples/tpl_dashboard.tsx +9 -9
  12. package/examples/tpl_dieline.tsx +2 -2
  13. package/examples/tpl_item_list.tsx +64 -37
  14. package/examples/tpl_lookup.tsx +5 -5
  15. package/examples/tpl_pick.tsx +6 -6
  16. package/examples/tpl_pivot.tsx +3 -3
  17. package/examples/tpl_record.tsx +919 -611
  18. package/examples/tpl_report.tsx +2 -2
  19. package/examples/tpl_rollup.tsx +5 -5
  20. package/examples/tpl_shifts.tsx +5 -5
  21. package/examples/tpl_statements.tsx +7 -7
  22. package/examples/tpl_stock.tsx +2 -2
  23. package/examples/tpl_task_board.tsx +42 -26
  24. package/examples/tpl_tower.tsx +6 -6
  25. package/package.json +4 -3
  26. package/src/agent_run.tsx +4 -4
  27. package/src/breakdown.tsx +1 -1
  28. package/src/calendar/calendar_view.tsx +1 -1
  29. package/src/change_review.tsx +9 -8
  30. package/src/chip_group.tsx +12 -2
  31. package/src/choice_list.tsx +2 -2
  32. package/src/confidence.tsx +2 -2
  33. package/src/data_grid.tsx +1 -1
  34. package/src/detail_row.tsx +50 -58
  35. package/src/file_dropzone.tsx +1 -1
  36. package/src/file_gallery_modal.tsx +3 -3
  37. package/src/file_row.tsx +29 -4
  38. package/src/file_rows.tsx +1 -1
  39. package/src/finding.tsx +4 -4
  40. package/src/form_field.tsx +1 -1
  41. package/src/format_date.ts +2 -2
  42. package/src/heatmap.tsx +1 -1
  43. package/src/inline_button.tsx +84 -0
  44. package/src/inline_date_picker.tsx +17 -10
  45. package/src/inline_edit.tsx +341 -60
  46. package/src/inline_member_select.tsx +8 -3
  47. package/src/inline_number_input.tsx +11 -4
  48. package/src/inline_select.tsx +26 -13
  49. package/src/inline_text_input.tsx +27 -6
  50. package/src/inline_time_picker.tsx +10 -4
  51. package/src/input_metrics.ts +17 -0
  52. package/src/ledger.tsx +2 -2
  53. package/src/locale.tsx +7 -3
  54. package/src/matrix.tsx +1 -1
  55. package/src/number_input.tsx +18 -7
  56. package/src/pipeline.tsx +1 -1
  57. package/src/popover.tsx +1 -1
  58. package/src/press_door.tsx +1 -1
  59. package/src/pressable_highlight.tsx +1 -1
  60. package/src/progress_bar.tsx +3 -3
  61. package/src/record_summary.tsx +2 -2
  62. package/src/result_header.tsx +2 -2
  63. package/src/sequence.tsx +170 -0
  64. package/src/share_or_download.ts +2 -2
  65. package/src/step_progress.tsx +7 -5
  66. package/src/stepper.tsx +1 -1
  67. package/src/task.tsx +6 -6
  68. package/src/text_input_field.tsx +21 -4
  69. package/src/text_utils.ts +7 -1
  70. package/src/linked_record_box.tsx +0 -157
@@ -30,7 +30,7 @@ export type ShareOrDownloadResult =
30
30
  | { delivered: "share"; count: number }
31
31
  | { delivered: "download"; count: number };
32
32
 
33
- /** "shared" = the sheet completed · "cancelled" = the user dismissed it ·
33
+ /** "shared" = the sheet completed, "cancelled" = the user dismissed it,
34
34
  * "unsupported" = Web Share (files) unavailable. A genuine share failure THROWS. */
35
35
  export type ShareFilesResult = "shared" | "cancelled" | "unsupported";
36
36
 
@@ -65,7 +65,7 @@ export async function prepareShareFiles(
65
65
  /**
66
66
  * Share already-fetched File objects via the OS sheet. Call SYNCHRONOUSLY from a
67
67
  * user gesture (no awaited fetch before it) so iOS keeps the transient activation.
68
- * "cancelled" = user dismissed · "unsupported" = no Web Share; a real failure THROWS.
68
+ * "cancelled" = user dismissed, "unsupported" = no Web Share; a real failure THROWS.
69
69
  */
70
70
  export async function shareFiles(fileObjs: File[], opts?: { title?: string; text?: string }): Promise<ShareFilesResult> {
71
71
  const nav = typeof navigator !== "undefined" ? navigator : undefined;
@@ -4,7 +4,7 @@ import { Text } from "./text";
4
4
  import { useTooltip } from "./tooltip";
5
5
 
6
6
  export interface StepProgressProps {
7
- /** The stages: pass the NAMES (enables the built-in "4/7 · In" caption
7
+ /** The stages: pass the NAMES (enables the built-in "In (4/7)" caption
8
8
  * and per-segment hover names) or a bare count (bar only). */
9
9
  steps: number | string[];
10
10
  /** 0-based index of the stage in progress; earlier segments render
@@ -17,7 +17,7 @@ export interface StepProgressProps {
17
17
  * uppercase eyebrow row above the bar, caption right. Omit at card
18
18
  * density where the context already names it. */
19
19
  title?: string;
20
- /** Caption override for when the current stage needs prose ("In production") instead of the derived "3/6 · SX". */
20
+ /** Caption override for when the current stage needs prose ("In production") instead of the derived "SX (3/6)". */
21
21
  label?: string;
22
22
  /** Tone for the caption — `danger` flags a stalled/overdue stage (a stuck
23
23
  * production order) in the FIXED caption spot, so the bar stays consistent
@@ -35,7 +35,7 @@ export interface StepProgressProps {
35
35
 
36
36
  /**
37
37
  * THE compact stage indicator — N equal segments filled through the current
38
- * stage, with a built-in "4/7 · In" caption when stages are named (hovering
38
+ * stage, with a built-in "In (4/7)" caption when stages are named (hovering
39
39
  * a segment names it). For *countable* stages an item walks through (a
40
40
  * production line, a checklist, a pipeline) shown at card/list density.
41
41
  *
@@ -55,8 +55,10 @@ export function StepProgress(props: StepProgressProps) {
55
55
  label ??
56
56
  (names
57
57
  ? isComplete
58
- ? `${count}/${count} · Complete`
59
- : `${Math.max(0, safe + 1)}/${count}${safe >= 0 ? ` · ${names[safe]}` : ""}`
58
+ ? `Complete (${count}/${count})`
59
+ : safe >= 0
60
+ ? `${names[safe]} (${safe + 1}/${count})`
61
+ : `0/${count}`
60
62
  : undefined);
61
63
 
62
64
  // `progressbar` IS right here — the segments are decoration over one quantity,
package/src/stepper.tsx CHANGED
@@ -68,7 +68,7 @@ export interface StepProps extends StepPositional {
68
68
  }
69
69
 
70
70
  /**
71
- * Progress through an ordered sequence — done · current · upcoming on a
71
+ * Progress through an ordered sequence — done, current, upcoming on a
72
72
  * connecting track (horizontal) or spine (vertical). The node encodes STATUS,
73
73
  * not identity: a filled dot once reached, a ring with a white centre at the
74
74
  * current step (pulsing when `live`), a faint ring for what's ahead, a check for
package/src/task.tsx CHANGED
@@ -14,7 +14,7 @@ import { TASK_ROW_BAND, TASK_ROW_GAP, taskGutter, taskTitleSlack } from "./task_
14
14
  * <TaskList>
15
15
  * <TaskItem>
16
16
  * <TaskStatus><CheckCircle …/></TaskStatus>
17
- * <TaskTitle><InlineTextInput variant="cell" struck={done} …/></TaskTitle>
17
+ * <TaskTitle><InlineTextInput variant="bare" struck={done} …/></TaskTitle>
18
18
  * <TaskActions><ActionMenu …/></TaskActions>
19
19
  * <TaskCaption>Waiting on the signed copy</TaskCaption>
20
20
  * <TaskSubRow label="Portal account"><InlineTextInput …/></TaskSubRow>
@@ -81,7 +81,7 @@ import { TASK_ROW_BAND, TASK_ROW_GAP, taskGutter, taskTitleSlack } from "./task_
81
81
  * **A value and a sentence are different things.** A `TaskSubRow` holds a NAMED value the reader
82
82
  * sets; `TaskCaption` holds a sentence ABOUT the row ("Missing 2 of 6") on its own line under the
83
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
84
+ * like a record's field does — `description`, `warning`, `error`, the one anatomy in
85
85
  * `field_annotations` — so a fault in ONE field says so on that field instead of being demoted
86
86
  * into a sentence about the whole row.
87
87
  *
@@ -131,11 +131,11 @@ const TaskListContext = createContext<TaskListContextValue>({
131
131
  */
132
132
  const SUB_VALUE_MIN = 200;
133
133
  /**
134
- * How far a `variant="cell"` control insets its own text — the kit's `CONTROL_TEXT_INSET` under
134
+ * How far an inline control insets its own text — the kit's `CONTROL_TEXT_INSET` under
135
135
  * this family's name, because a task surface reaches for it constantly. Everything that hangs
136
136
  * beneath a row — the caption, a sub-row's label, the detail block — adds the same, so it lines
137
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
138
+ * `TaskTitle`; only a custom title NODE that is not an inline control has to add it, or
139
139
  * it sits out of line with the editable titles around it.
140
140
  */
141
141
  export const TASK_TEXT_INSET = CONTROL_TEXT_INSET;
@@ -282,7 +282,7 @@ type TaskTitleProps =
282
282
  | { children: ReactNode; struck?: never };
283
283
 
284
284
  /**
285
- * The task's identity — an inline editor (`variant="cell"`), or the title text itself.
285
+ * The task's identity — an inline editor, or the title text itself.
286
286
  *
287
287
  * Pass a STRING and the title renders on the cell inset with the band's slack above and below
288
288
  * it, so it lines up with the editable titles around it AND a title that wraps keeps its first
@@ -379,7 +379,7 @@ interface TaskSubRowProps extends FieldAnnotationProps {
379
379
  * own line under the label rather than compressing to a few characters, and on a wide one it
380
380
  * FILLS the content box, ending on the same x as the title above it and the ⋯ gutter beside it.
381
381
  *
382
- * It annotates exactly like a `DetailRow` — `description` · `warning` · `error`, one shared
382
+ * It annotates exactly like a `DetailRow` — `description`, `warning`, `error`, one shared
383
383
  * anatomy (`field_annotations`). A field-level fault says so ON THE FIELD; demoting it into a
384
384
  * `TaskCaption`, which is a sentence about the ROW, says something different.
385
385
  *
@@ -13,7 +13,7 @@ import { Ref, useCallback, useState } from "react";
13
13
  import { Icon, IconName } from "./icon";
14
14
  import { IconButton } from "./icon_button";
15
15
  import { ShortcutBadge } from "./shortcut_badge";
16
- import { fontFamilyRegular, getInputLineHeight, getInputTextStyle } from "./text_utils";
16
+ import { fontFamilyRegular, getInputTextStyle, getMultilineInputHeight } from "./text_utils";
17
17
  import { useScreenSize } from "./use_screen_size";
18
18
  import { useAutoGrowHeight } from "./use_auto_grow_height";
19
19
  import { useFormField } from "./form_field";
@@ -34,6 +34,17 @@ interface TextInputFieldProps extends RNTextInputProps {
34
34
  /** Accessible name for the clear button. Defaults to the locale's
35
35
  * `textInputField.clear` ("Clear" / "Xóa"). */
36
36
  clearLabel?: string;
37
+ /** The HOST paints the surface — drop this input's own border, fill, focus ring
38
+ * AND horizontal padding, so the page draws one box and the text lands on the
39
+ * host's inset rather than 8px further in. Set by an inline editor whose field
40
+ * carries `actions`: the field's shell owns the surface (so the verbs never
41
+ * move between modes) and owns the ring with it.
42
+ *
43
+ * HEIGHT and VERTICAL padding stay — those are what keep the field the same
44
+ * size in both modes; the horizontal inset is what has to be surrendered,
45
+ * because the host already spent `CONTROL_TEXT_INSET` on it and two insets
46
+ * stacked is a visible jump to the right on focus. */
47
+ seamless?: boolean;
37
48
  // DOM-only ARIA attrs not declared on React Native's TextInputProps. They
38
49
  // are forwarded verbatim to the underlying web input.
39
50
  "aria-controls"?: string;
@@ -45,6 +56,7 @@ export function TextInputField(props: TextInputFieldProps) {
45
56
  const locale = useLoticsLocale();
46
57
  const {
47
58
  style,
59
+ seamless,
48
60
  icon,
49
61
  clearable,
50
62
  onClear,
@@ -72,7 +84,6 @@ export function TextInputField(props: TextInputFieldProps) {
72
84
  if (ariaAutocomplete !== undefined) webAriaAttrs["aria-autocomplete"] = ariaAutocomplete;
73
85
 
74
86
  const { small } = useScreenSize();
75
- const lineHeight = getInputLineHeight(small);
76
87
  const binding = useFormField();
77
88
  // Text-like input: ring on ANY focus (browsers treat typing-capable fields as
78
89
  // focus-visible even on pointer focus), so a clicked field shows its ring.
@@ -87,7 +98,7 @@ export function TextInputField(props: TextInputFieldProps) {
87
98
 
88
99
  const minHeight =
89
100
  numberOfLines && numberOfLines > 1
90
- ? numberOfLines * lineHeight + 16
101
+ ? getMultilineInputHeight(numberOfLines, small)
91
102
  : undefined;
92
103
 
93
104
  const autoGrowResult = useAutoGrowHeight({
@@ -167,7 +178,8 @@ export function TextInputField(props: TextInputFieldProps) {
167
178
  showShortcut && shortcutWidth > 0 && { paddingRight: SHORTCUT_INSET + shortcutWidth },
168
179
  hovered && editable && { borderColor: HOVER_BORDER },
169
180
  style,
170
- focusVisible && { boxShadow: FOCUS_RING },
181
+ focusVisible && !seamless && { boxShadow: FOCUS_RING },
182
+ seamless && styles.seamless,
171
183
  ]}
172
184
  multiline={(numberOfLines && numberOfLines > 1) || multiline}
173
185
  focusable={editable}
@@ -227,6 +239,11 @@ const styles = StyleSheet.create({
227
239
  fontFamily: fontFamilyRegular,
228
240
  letterSpacing: -0.4,
229
241
  },
242
+ // The HOST paints the surface AND owns the text inset. Height and vertical
243
+ // padding stay (they hold the field's size); the horizontal padding goes,
244
+ // because the host already spent CONTROL_TEXT_INSET and two insets stacked
245
+ // shift the text 8px right the moment the field focuses.
246
+ seamless: { borderColor: "transparent", backgroundColor: "transparent", paddingHorizontal: 0 },
230
247
  disabled: {
231
248
  color: colors.zinc["400"],
232
249
  outlineStyle: "none" as unknown as "solid",
package/src/text_utils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Platform, TextStyle } from "react-native";
2
2
  import { colors } from "./colors";
3
+ import { multilineInputHeight } from "./input_metrics";
3
4
 
4
5
  export type TextColor =
5
6
  | "default"
@@ -26,7 +27,7 @@ export function getTextColor(color?: TextColor): string {
26
27
  case "zinc-500":
27
28
  return colors.zinc["500"];
28
29
  // The disabled ink — light enough that a control reads inert at a glance
29
- // (muted/600 still reads as body ink on a zinc-50 chip).
30
+ // (muted/600 still reads as body ink on the control's resting surface).
30
31
  case "zinc-400":
31
32
  return colors.zinc["400"];
32
33
  case "inverted":
@@ -112,6 +113,11 @@ export function getInputTextStyle(): TextStyle {
112
113
  *
113
114
  * @param isSmallScreen - Whether the screen is small (mobile). Get this from useScreenSize().small
114
115
  */
116
+ /** {@link multilineInputHeight} at this platform's input line height. */
117
+ export function getMultilineInputHeight(numberOfLines: number, isSmallScreen: boolean): number {
118
+ return multilineInputHeight(numberOfLines, getInputLineHeight(isSmallScreen));
119
+ }
120
+
115
121
  export function getInputLineHeight(isSmallScreen: boolean): number {
116
122
  if (Platform.OS !== "web") {
117
123
  return INPUT_LINE_HEIGHT_DESKTOP;
@@ -1,157 +0,0 @@
1
- import { type ReactNode } from "react";
2
- import { View, StyleSheet } from "react-native";
3
- import { PressableRow } from "./pressable_row";
4
- import { PressDoor } from "./press_door";
5
- import { Text } from "./text";
6
- import { Icon, type IconName } from "./icon";
7
- import { colors } from "./colors";
8
- import { INLINE_CONTROL_HEIGHT } from "./inline_edit";
9
-
10
- /** One reference fact. `V` is the value the variant admits — text under a door, any node without one. */
11
- interface LinkedRecordFact<V = ReactNode> {
12
- label: string;
13
- value: V;
14
- }
15
-
16
- interface LinkedRecordBoxBaseProps {
17
- /** The linked record's kind glyph (building-2 for a company, file-text for a record…). */
18
- icon: IconName;
19
- /** The linked record's display name. */
20
- name: string;
21
- /** A secondary identity line — a code, tax id, country, or "<desk> record". */
22
- subtitle?: string;
23
- /** Width of the facts' label column (default 80). Widen for longer labels. */
24
- factLabelWidth?: number;
25
- /** The verbs, in a hairline-fenced footer at the bottom of the box, lifted above the door's
26
- * hit area. Convention: destructive LEFT, go-to RIGHT (a `<View style={{ flex: 1 }} />`
27
- * spacer between). The divider is drawn automatically when actions are present. Supplied by
28
- * the consumer so the labels stay localized and the verb set fits the link. */
29
- actions?: ReactNode;
30
- }
31
-
32
- /** The box that OPENS — the whole surface is the door into the linked record's own detail. */
33
- interface LinkedRecordBoxDoorProps extends LinkedRecordBoxBaseProps {
34
- /** Opens the linked record's detail (drawer/page) — the WHOLE box presses this. */
35
- onOpen: () => void;
36
- /** Accessible name for opening the record (e.g. "Acme Corp — details"). */
37
- doorLabel: string;
38
- /** Reference facts, stacked VERTICALLY (legible at any width, never squeezed). TEXT only
39
- * (`""` renders "—"): the box is one press target, so the only controls it may hold are the
40
- * `actions` verbs, which the footer fences off and lifts above the door. A fact the reader
41
- * must EDIT belongs on the linked record's own page — that is what the door is for. */
42
- facts?: LinkedRecordFact<string>[];
43
- }
44
-
45
- /** The box with nothing to open — the linked record has no detail surface of its own. */
46
- interface LinkedRecordBoxStaticProps extends LinkedRecordBoxBaseProps {
47
- /** Absent by construction. Typed `never` rather than left off so `doorLabel` can never arrive
48
- * without a handler — a named door that goes nowhere. */
49
- onOpen?: never;
50
- doorLabel?: never;
51
- /** Reference facts, stacked VERTICALLY. A string renders as text (`""` → "—"); a NODE renders
52
- * as given (no placeholder), so a fact can carry its own inline editor — safe here precisely
53
- * because nothing about this box presses. */
54
- facts?: LinkedRecordFact[];
55
- }
56
-
57
- type LinkedRecordBoxProps = LinkedRecordBoxDoorProps | LinkedRecordBoxStaticProps;
58
-
59
- /**
60
- * A LINKED RECORD shown for reference — a bordered box scoping ANOTHER record's data
61
- * (identity glyph · name · facts). Use it wherever one record points at another (a shipment's
62
- * customer, an invoice's party, a case's sibling); the picker / empty-state that REASSIGNS the
63
- * link is the consumer's, shown in place of the box.
64
- *
65
- * `onOpen` picks the variant:
66
- *
67
- * - **With `onOpen`** (+ `doorLabel`) the WHOLE box is a keyboard-accessible door into the
68
- * record's detail. Its facts are TEXT, and the `actions` footer is the one place a control
69
- * may live — everything else is press-through to the door.
70
- * - **Without it** the box is a STATIC reference card: no door, no tab stop, no pointer, nothing
71
- * announced as a button; the only interactive parts are `actions` and whatever a fact node
72
- * holds. This is the shape for a record with NO page of its own — there is nothing to open, so
73
- * the box IS where its values are read and edited, and an inline editor in a fact is honest.
74
- *
75
- * The a11y contract the door variant enforces (the error-prone part, hence a primitive): a
76
- * container with interactive descendants must NEVER be `role="button"` (invalid HTML). So the
77
- * box is a role-less `PressableRow`, a `PressDoor` sibling carries the tab stop / accessible
78
- * name / focus ring, and the interior verbs lift above it via `zIndex`.
79
- */
80
- export function LinkedRecordBox(props: LinkedRecordBoxProps) {
81
- const { icon, name, subtitle, factLabelWidth = 80, actions } = props;
82
- // A door box's facts are strings, and a string IS a ReactNode — so both variants render
83
- // through one path typed at the wider value.
84
- const facts: LinkedRecordFact[] = props.facts ?? [];
85
- const body = (
86
- <>
87
- <View style={styles.identity}>
88
- <View style={styles.glyph}>
89
- <Icon name={icon} size={17} color={colors.zinc[600]} />
90
- </View>
91
- <View style={{ flex: 1, minWidth: 0 }}>
92
- <Text size="sm" weight="medium" numberOfLines={1}>{name}</Text>
93
- {subtitle ? <Text size="xs" color="muted" numberOfLines={1}>{subtitle}</Text> : null}
94
- </View>
95
- </View>
96
- {facts.length > 0 ? (
97
- <View style={{ gap: 6 }}>
98
- {facts.map((f) => {
99
- // THE ALIGNMENT LAW, the same one `DetailRow` runs on: the fact TOP-aligns and each
100
- // cell centres within the first CONTROL LINE. A node value brings the kit's control
101
- // band (an inline editor is 40 tall, a chip less), so both cells claim it — without
102
- // it the label `Text`, stretched to the row, printed its words at the band's TOP,
103
- // a half-band above the value it names. A STRING value has no band: label and value
104
- // are the same 20px line, so they sit at the row's top and a long value that wraps
105
- // still tops out level with its label.
106
- const banded = typeof f.value !== "string";
107
- return (
108
- <View key={f.label} style={styles.fact}>
109
- <View style={[{ width: factLabelWidth }, banded && styles.bandedCell]}>
110
- <Text size="sm" color="muted">{f.label}</Text>
111
- </View>
112
- {banded ? (
113
- // A node prints as authored — the "—" placeholder is a TEXT affordance, and a
114
- // control renders its own empty state.
115
- <View style={[{ flex: 1, minWidth: 0 }, styles.bandedCell]}>{f.value}</View>
116
- ) : (
117
- <Text size="sm" style={{ flex: 1 }}>{f.value || "—"}</Text>
118
- )}
119
- </View>
120
- );
121
- })}
122
- </View>
123
- ) : null}
124
- {actions ? (
125
- <>
126
- {/* full-bleed hairline fencing the verb footer off from the facts */}
127
- <View style={styles.divider} />
128
- <View style={styles.actions}>{actions}</View>
129
- </>
130
- ) : null}
131
- </>
132
- );
133
-
134
- // Nothing to open → a plain View, so the box takes no tab stop, announces no role, and paints
135
- // no pointer/hover promise of a destination it doesn't have.
136
- if (!props.onOpen) return <View style={styles.box}>{body}</View>;
137
-
138
- return (
139
- <PressableRow onPress={props.onOpen} style={styles.box}>
140
- <PressDoor accessibilityLabel={props.doorLabel} onPress={props.onOpen} />
141
- {body}
142
- </PressableRow>
143
- );
144
- }
145
-
146
- const styles = StyleSheet.create({
147
- box: { flexDirection: "column", alignItems: "stretch", borderWidth: 1, borderColor: colors.zinc[200], borderRadius: 10, paddingHorizontal: 16, paddingTop: 14, paddingBottom: 10, gap: 12 },
148
- identity: { flexDirection: "row", alignItems: "center", gap: 10 },
149
- glyph: { width: 34, height: 34, borderRadius: 8, backgroundColor: colors.zinc[100], alignItems: "center", justifyContent: "center" },
150
- fact: { flexDirection: "row", gap: 10, alignItems: "flex-start" },
151
- // A cell on the control band: exactly `minHeight` tall for a one-line label / a short chip,
152
- // and taller content (a wrapped label, a stack of controls) tops out level instead of being
153
- // dragged toward the block's centre.
154
- bandedCell: { minHeight: INLINE_CONTROL_HEIGHT, justifyContent: "center" },
155
- divider: { height: 1, backgroundColor: colors.zinc[100], marginHorizontal: -16 },
156
- actions: { flexDirection: "row", alignItems: "center", flexWrap: "wrap", columnGap: 8, rowGap: 8, zIndex: 1 },
157
- });