@lotics/ui 44.14.0 → 45.3.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 +9 -2
- package/MIGRATION.md +61 -5
- package/docs/ai_patterns.md +2 -3
- package/docs/catalog.md +40 -17
- package/docs/composition.md +143 -42
- package/docs/data_entry.md +15 -8
- package/docs/reviewing.md +24 -12
- package/docs/templates.md +1 -1
- package/package.json +10 -1
- package/src/accordion.tsx +2 -2
- package/src/avatar_group.tsx +1 -1
- package/src/avatar_size.ts +2 -3
- package/src/card_select_item.tsx +7 -6
- package/src/color_tokens.ts +22 -6
- package/src/combobox.tsx +11 -1
- package/src/control_surface.ts +22 -16
- package/src/date_stamp.tsx +1 -3
- package/src/detail_row.tsx +4 -5
- package/src/dialog.tsx +13 -6
- package/src/drawer.tsx +18 -4
- package/src/filter_chip.tsx +1 -2
- package/src/finding.tsx +24 -7
- package/src/form_markdown_editor.tsx +15 -0
- package/src/heading_altitude.ts +65 -0
- package/src/inline_markdown.tsx +49 -76
- package/src/inline_text_input.tsx +4 -5
- package/src/ledger.tsx +3 -3
- package/src/locale.tsx +48 -0
- package/src/markdown.web.tsx +3 -6
- package/src/markdown_editor.css +144 -0
- package/src/markdown_editor.tsx +29 -0
- package/src/markdown_editor.web.tsx +89 -0
- package/src/markdown_editor_props.ts +29 -0
- package/src/markdown_toolbar.web.tsx +231 -0
- package/src/matrix.tsx +1 -1
- package/src/modal.tsx +10 -1
- package/src/popover.tsx +151 -142
- package/src/pressable_row.tsx +24 -15
- package/src/reference_field.tsx +11 -9
- package/src/section_heading.tsx +57 -71
- package/src/stepper.tsx +2 -3
- package/src/summary.tsx +3 -3
- package/src/table.tsx +4 -1
- package/src/text_utils.ts +3 -4
- package/src/timeline.tsx +3 -4
package/src/reference_field.tsx
CHANGED
|
@@ -7,7 +7,7 @@ import { formatDate } from "./format_date";
|
|
|
7
7
|
import { InlineEditView } from "./inline_edit";
|
|
8
8
|
import { InlineStatic } from "./inline_static";
|
|
9
9
|
import { Popover, PopoverContent, PopoverFooter, PopoverHeader } from "./popover";
|
|
10
|
-
import {
|
|
10
|
+
import { SectionHeadingTitle } from "./section_heading";
|
|
11
11
|
import { Text } from "./text";
|
|
12
12
|
import { TextInputField } from "./text_input_field";
|
|
13
13
|
import { TextLink } from "./text_link";
|
|
@@ -256,19 +256,21 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
256
256
|
</View>
|
|
257
257
|
}
|
|
258
258
|
/>
|
|
259
|
-
{/* THE PEEK'S FORMAT — the kit's own grammar, not a bespoke one.
|
|
260
|
-
is
|
|
261
|
-
|
|
262
|
-
|
|
259
|
+
{/* THE PEEK'S FORMAT — the kit's own grammar, not a bespoke one. The
|
|
260
|
+
identity is a plain `SectionHeadingTitle` with the code as its
|
|
261
|
+
description rather than a hand-picked font weight — a popover is
|
|
262
|
+
dialog-scale, so the ramp puts it on the `####` rung by itself, and
|
|
263
|
+
this call site no longer names the surface it is standing
|
|
264
|
+
on; the facts are `DetailRow`s, which
|
|
263
265
|
is what label-beside-value IS everywhere else on this page; and the
|
|
264
266
|
verbs sit in the popover's own pinned footer rather than floating after
|
|
265
267
|
the last fact.
|
|
266
268
|
|
|
267
269
|
WIDTH COMES FROM THE FIELD (`inheritTriggerWidth`), not a constant. A
|
|
268
270
|
fixed 320 was "every peek in an app is the same object", and the object
|
|
269
|
-
it was actually the same as was nothing on screen:
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
it was actually the same as was nothing on screen: a wide field opened a
|
|
272
|
+
narrow panel left-aligned under it, stopping short of the edge the
|
|
273
|
+
reader's eye had just travelled to. A
|
|
272
274
|
popover narrower than the control that opened it reads as belonging to
|
|
273
275
|
something else. `InlineSelect` — the same shape, an inline field opening
|
|
274
276
|
a `triggerRef` popover — already inherits its field's width, so the two
|
|
@@ -294,7 +296,7 @@ export function ReferenceField(props: ReferenceFieldProps) {
|
|
|
294
296
|
is never what scrolls out of view. */}
|
|
295
297
|
<PopoverContent style={{ maxHeight: 420 }}>
|
|
296
298
|
<PopoverHeader>
|
|
297
|
-
<
|
|
299
|
+
<SectionHeadingTitle description={code}>{name}</SectionHeadingTitle>
|
|
298
300
|
</PopoverHeader>
|
|
299
301
|
<View style={{ gap: 12 }}>
|
|
300
302
|
{/* ONE geometry for both modes — the table's own 40px band, which is
|
package/src/section_heading.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { View, type StyleProp, type ViewStyle } from "react-native";
|
|
2
|
-
import { Text, type HeadingLevel } from "./text";
|
|
2
|
+
import { Text, type HeadingLevel, type TextSize } from "./text";
|
|
3
3
|
import { Icon, type IconName } from "./icon";
|
|
4
4
|
import { InfoPopover } from "./info_popover";
|
|
5
5
|
import { useLoticsLocale } from "./locale";
|
|
6
|
+
import { useHeadingAltitude, type HeadingAltitude } from "./heading_altitude";
|
|
6
7
|
|
|
7
8
|
// The card-less section header — the bare-canvas sibling of `CardHeader`, built
|
|
8
9
|
// the same compound way. A title (+ optional leading icon / description) on one
|
|
@@ -117,9 +118,15 @@ export interface SubsectionHeadingTitleProps {
|
|
|
117
118
|
info?: string;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
|
-
/** The subsection title — ALWAYS `###` (lg semibold), the fixed step under
|
|
121
|
-
* xl section title and above the md/sm body. Grows to push siblings (a
|
|
122
|
-
* `SectionHeadingMeta`, a `Badge`, an action) to the right edge.
|
|
121
|
+
/** The subsection title — ALWAYS `###` (lg semibold), the fixed step under a
|
|
122
|
+
* PAGE's xl section title and above the md/sm body. Grows to push siblings (a
|
|
123
|
+
* `SectionHeadingMeta`, a `Badge`, an action) to the right edge.
|
|
124
|
+
*
|
|
125
|
+
* It has one altitude, not two: inside a PANEL the section rung is already
|
|
126
|
+
* the ramp's floor (`####` md), so a subsection there would out-rank its own
|
|
127
|
+
* section. A named group inside a panel's section is `Text size="sm"
|
|
128
|
+
* weight="semibold"` — a panel is one surface, and a second heading level in it
|
|
129
|
+
* is a document the reader is not reading. */
|
|
123
130
|
export function SubsectionHeadingTitle(props: SubsectionHeadingTitleProps) {
|
|
124
131
|
const { children, description, level = 3, info } = props;
|
|
125
132
|
const words = useLoticsLocale();
|
|
@@ -144,64 +151,6 @@ export function SubsectionHeadingTitle(props: SubsectionHeadingTitleProps) {
|
|
|
144
151
|
);
|
|
145
152
|
}
|
|
146
153
|
|
|
147
|
-
export interface DialogSectionHeadingTitleProps {
|
|
148
|
-
children: React.ReactNode;
|
|
149
|
-
/** Optional muted line under the title, as on `SectionHeadingTitle`. */
|
|
150
|
-
description?: string;
|
|
151
|
-
/** Leading glyph, as on `SectionHeadingTitle`. */
|
|
152
|
-
icon?: IconName;
|
|
153
|
-
/** Heading rank. Defaults to 4 — one level under `SubsectionHeadingTitle`'s 3. */
|
|
154
|
-
level?: HeadingLevel;
|
|
155
|
-
/** An ⓘ popover after the title, as on the two rungs above. */
|
|
156
|
-
info?: string;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* The section title for a surface hosted in a DIALOG — ALWAYS `####` (md semibold), the fourth
|
|
161
|
-
* rung of the same fixed ladder.
|
|
162
|
-
*
|
|
163
|
-
* It exists because `DialogHeaderTitle` is `lg`, which is the rung `SubsectionHeadingTitle`
|
|
164
|
-
* occupies: a dialog holding a multi-section surface has its own title tying with, or being
|
|
165
|
-
* outranked by, its content. The ladder stays ABSOLUTE — this is one more fixed size, never a
|
|
166
|
-
* knob on an existing rung. Do not reach for a `size`/`compact` prop on the titles above; the same
|
|
167
|
-
* rank rendering at two sizes is exactly what the fixed ramp prevents.
|
|
168
|
-
*
|
|
169
|
-
* Compose it with the SUBSECTION layout — `Subsection` › `SubsectionHeading` › this, inside a
|
|
170
|
-
* `SubsectionStack`. Only the type size steps down; the 12px body gap and the 32px between-group
|
|
171
|
-
* beat are already right for a dialog, so duplicating them under a `Dialog*` name would add three
|
|
172
|
-
* exports that differ from their siblings in nothing.
|
|
173
|
-
*
|
|
174
|
-
* <SubsectionStack divided>
|
|
175
|
-
* <Subsection>
|
|
176
|
-
* <SubsectionHeading>
|
|
177
|
-
* <DialogSectionHeadingTitle info="…">Usage</DialogSectionHeadingTitle>
|
|
178
|
-
* <SectionHeadingMeta>Aug 2 – Sep 2</SectionHeadingMeta>
|
|
179
|
-
* </SubsectionHeading>
|
|
180
|
-
* {body}
|
|
181
|
-
* </Subsection>
|
|
182
|
-
* </SubsectionStack>
|
|
183
|
-
*/
|
|
184
|
-
export function DialogSectionHeadingTitle(props: DialogSectionHeadingTitleProps) {
|
|
185
|
-
const { children, description, icon, level = 4, info } = props;
|
|
186
|
-
const words = useLoticsLocale();
|
|
187
|
-
return (
|
|
188
|
-
<View style={{ flex: 1, minWidth: HEADING_TITLE_MIN_WIDTH, gap: 2 }}>
|
|
189
|
-
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
190
|
-
{icon ? <Icon name={icon} size={16} /> : null}
|
|
191
|
-
<Text level={level} size="md" weight="semibold">
|
|
192
|
-
{children}
|
|
193
|
-
</Text>
|
|
194
|
-
{info ? <InfoPopover text={info} accessibilityLabel={words.sectionHeading.info} /> : null}
|
|
195
|
-
</View>
|
|
196
|
-
{description ? (
|
|
197
|
-
<Text color="zinc-500" size="sm">
|
|
198
|
-
{description}
|
|
199
|
-
</Text>
|
|
200
|
-
) : null}
|
|
201
|
-
</View>
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
154
|
export interface SectionHeadingProps {
|
|
206
155
|
children: React.ReactNode;
|
|
207
156
|
style?: StyleProp<ViewStyle>;
|
|
@@ -219,7 +168,15 @@ export interface SectionHeadingTitleProps {
|
|
|
219
168
|
children: React.ReactNode;
|
|
220
169
|
description?: string;
|
|
221
170
|
icon?: IconName;
|
|
222
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* Heading rank, when the surface's own outline needs one the altitude does not
|
|
173
|
+
* give it. Leave it unset: the default is the rank of whichever rung the
|
|
174
|
+
* altitude selected (`2` on a page, `4` in a panel), so an outline is
|
|
175
|
+
* correct without anyone thinking about it.
|
|
176
|
+
*
|
|
177
|
+
* It does NOT move the type size — the ramp is absolute, and a rank rendering
|
|
178
|
+
* at two sizes is exactly what a fixed ladder exists to prevent.
|
|
179
|
+
*/
|
|
223
180
|
level?: HeadingLevel;
|
|
224
181
|
/** Title weight. Defaults to `semibold` — a section title must separate from
|
|
225
182
|
* the body rows at a glance (medium blends in; it remains as an opt-down for
|
|
@@ -230,21 +187,50 @@ export interface SectionHeadingTitleProps {
|
|
|
230
187
|
info?: string;
|
|
231
188
|
}
|
|
232
189
|
|
|
233
|
-
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
190
|
+
/**
|
|
191
|
+
* THE SECTION RUNG, per altitude — the two fixed points of one ladder, written
|
|
192
|
+
* down once.
|
|
193
|
+
*
|
|
194
|
+
* On a PAGE the section title is `##` (xl), under the `#` xxl identity band.
|
|
195
|
+
* Inside a PANEL it is `####` (md), under the container's `lg` chrome title —
|
|
196
|
+
* the ladder does not gain a knob, it gains a second fixed point, chosen by
|
|
197
|
+
* where the heading IS rather than by what the caller remembered to pass.
|
|
198
|
+
*
|
|
199
|
+
* The glyph steps with the type: a 20px icon beside 16px text is a mark that
|
|
200
|
+
* outweighs the words it introduces.
|
|
201
|
+
*/
|
|
202
|
+
const SECTION_RUNG = {
|
|
203
|
+
page: { size: "xl", level: 2, icon: 20 },
|
|
204
|
+
panel: { size: "md", level: 4, icon: 16 },
|
|
205
|
+
} as const satisfies Record<HeadingAltitude, { size: TextSize; level: HeadingLevel; icon: number }>;
|
|
206
|
+
|
|
207
|
+
/** The title block — the heading ramp's SECTION rung, on the fixed markdown
|
|
208
|
+
* ladder `#` xxl (`RecordSummary`/`PageHeader`) → `##` xl (section) → `###` lg
|
|
209
|
+
* (`SubsectionHeadingTitle`) over the sm body. No size knob: which rung it
|
|
210
|
+
* renders is decided by the SURFACE, not the call site — `##` xl on a page,
|
|
211
|
+
* `####` md inside a panel's content region, where the container's own title
|
|
212
|
+
* is already `lg`. A caller never says which; the container publishes it (see
|
|
213
|
+
* `heading_altitude.ts`), so the same heading is correct on a page and in a
|
|
214
|
+
* drawer without being written twice.
|
|
215
|
+
*
|
|
216
|
+
* Inside a panel this rung is the ramp's FLOOR — a pane is one surface,
|
|
217
|
+
* not a document. A named group inside a section there is `Text size="sm"
|
|
218
|
+
* weight="semibold"`, not a `SubsectionHeadingTitle`, which is `lg` at every
|
|
219
|
+
* altitude and would out-rank the section holding it.
|
|
220
|
+
*
|
|
221
|
+
* Optional muted `description` line; grows to push any sibling actions to the
|
|
222
|
+
* right edge. */
|
|
238
223
|
export function SectionHeadingTitle(props: SectionHeadingTitleProps) {
|
|
239
|
-
const { children, description, icon, level
|
|
224
|
+
const { children, description, icon, level, weight = "semibold", info } = props;
|
|
240
225
|
// Hoisted — a hook inside the `info ?` ternary breaks hook ordering the
|
|
241
226
|
// render `info` appears or disappears.
|
|
242
227
|
const words = useLoticsLocale();
|
|
228
|
+
const rung = SECTION_RUNG[useHeadingAltitude()];
|
|
243
229
|
return (
|
|
244
230
|
<View style={{ flex: 1, minWidth: HEADING_TITLE_MIN_WIDTH, gap: 2 }}>
|
|
245
231
|
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
246
|
-
{icon ? <Icon name={icon} size={
|
|
247
|
-
<Text level={level} weight={weight} size=
|
|
232
|
+
{icon ? <Icon name={icon} size={rung.icon} /> : null}
|
|
233
|
+
<Text level={level ?? rung.level} weight={weight} size={rung.size}>
|
|
248
234
|
{children}
|
|
249
235
|
</Text>
|
|
250
236
|
{info ? <InfoPopover text={info} accessibilityLabel={words.sectionHeading.info} /> : null}
|
package/src/stepper.tsx
CHANGED
|
@@ -288,9 +288,8 @@ export function Step(props: StepProps) {
|
|
|
288
288
|
on the first content row, so there is a `markerTopOffset` band of
|
|
289
289
|
empty column above it. A single below-the-marker segment cannot
|
|
290
290
|
reach into the NEXT step's band, so the line broke at every joint
|
|
291
|
-
—
|
|
292
|
-
next one's offset above it
|
|
293
|
-
and 3px, at every stage, which reads as dashes rather than a spine.
|
|
291
|
+
— a gap under each marker (this segment's old top margin) plus the
|
|
292
|
+
next one's offset above it, which reads as dashes rather than a spine.
|
|
294
293
|
The upper segment fills that band, so the line is continuous from
|
|
295
294
|
the first marker to the last. It also replaces the column's old
|
|
296
295
|
`paddingTop`, which is why the marker lands in the same place. */}
|
package/src/summary.tsx
CHANGED
|
@@ -231,9 +231,9 @@ const styles = StyleSheet.create({
|
|
|
231
231
|
// 12, and it is half of what a page must put BETWEEN this band and its
|
|
232
232
|
// neighbours. The band's parts are one group — a figure, the shape of what it
|
|
233
233
|
// counts, and that shape's key — so they have to sit closer to each other than
|
|
234
|
-
// the band sits to the register below it.
|
|
235
|
-
//
|
|
236
|
-
// grouped, which is what "no rhythm"
|
|
234
|
+
// the band sits to the register below it. At near-parity with the page gap
|
|
235
|
+
// every block on the screen is an equal peer, nothing
|
|
236
|
+
// grouped, which is what "no rhythm" means.
|
|
237
237
|
root: { gap: 12 },
|
|
238
238
|
header: {
|
|
239
239
|
flexDirection: "row",
|
package/src/table.tsx
CHANGED
|
@@ -612,7 +612,10 @@ const styles = StyleSheet.create({
|
|
|
612
612
|
// A hairline under the column header anchors the columns; the rows below it are
|
|
613
613
|
// Divider-separated.
|
|
614
614
|
headerBandFilled: {
|
|
615
|
-
|
|
615
|
+
// The literal neutral, not `accent_wash`: this band is a lid on the columns,
|
|
616
|
+
// never a "you are here", and reading it through the brand token made a
|
|
617
|
+
// themed app tint a piece of pure furniture.
|
|
618
|
+
backgroundColor: colors.zinc[100],
|
|
616
619
|
// The band bleeds to the row wash's width so its edges line up with a hovered
|
|
617
620
|
// row beneath it; without this the lid is narrower than the rows it caps.
|
|
618
621
|
marginHorizontal: -ROW_WASH_BLEED,
|
package/src/text_utils.ts
CHANGED
|
@@ -103,10 +103,9 @@ export const inputTextStyleWeb = {
|
|
|
103
103
|
// THE INK IS PART OF THE CONTRACT, because leaving it out does not mean
|
|
104
104
|
// "inherit" — a native <input> ignores inherited colour and falls to the UA
|
|
105
105
|
// default, PURE BLACK, while every resting value beside it renders `zinc-900`.
|
|
106
|
-
// So a field's own text changed colour the instant it was focused
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
// and five widths all measured identical — but at 14px a darkness change
|
|
106
|
+
// So a field's own text changed colour the instant it was focused — the
|
|
107
|
+
// resting ink against the UA's black. Nothing MOVED, which is why every
|
|
108
|
+
// geometry check came back identical, but at body size a darkness change
|
|
110
109
|
// re-weights the antialiasing of every stroke, and the eye reads that as the
|
|
111
110
|
// text shifting on focus. An EMPTY field hid it completely, since a placeholder
|
|
112
111
|
// sets its own colour and therefore never changed.
|
package/src/timeline.tsx
CHANGED
|
@@ -17,10 +17,9 @@ export interface TimelineItem {
|
|
|
17
17
|
/**
|
|
18
18
|
* The row's identity line — CLAMPED TO TWO LINES, because it is a scan target
|
|
19
19
|
* before it is a sentence. A feed is the one place user-authored prose lands in
|
|
20
|
-
* a row primitive, and unclamped it eats the section:
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* below the first line it was meant to sit beside.
|
|
20
|
+
* a row primitive, and unclamped it eats the section: one long summary can
|
|
21
|
+
* draw a label many lines deep, and the disc — which centres on the label row —
|
|
22
|
+
* ends up far below the first line it was meant to sit beside.
|
|
24
23
|
*
|
|
25
24
|
* The clamp holds while the row is COLLAPSED and lifts when it opens, so a
|
|
26
25
|
* long label is readable in full without being copied anywhere. **Never repeat
|