@lotics/ui 19.1.0 → 20.0.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 +35 -0
- package/docs/catalog.md +12 -9
- package/docs/composition.md +16 -10
- package/package.json +1 -2
- package/src/accordion.tsx +4 -1
- package/src/breakdown.tsx +2 -1
- package/src/data_grid.tsx +2 -1
- package/src/progress_bar.tsx +19 -6
- package/src/section_heading.tsx +58 -0
- package/src/select.tsx +5 -3
- package/src/spreadsheet_view.tsx +3 -1
- package/src/section.tsx +0 -101
package/MIGRATION.md
CHANGED
|
@@ -4,6 +4,41 @@ Breaking changes, newest first — normally per major, plus the rare minor that
|
|
|
4
4
|
anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
|
|
5
5
|
this file exists only to move an app from one release to the next.
|
|
6
6
|
|
|
7
|
+
## v20 from 19.x
|
|
8
|
+
|
|
9
|
+
**`@lotics/ui/section` is DELETED.** Two modules exported a `Section` — the layout grammar's
|
|
10
|
+
card-less region (`section_heading`) and a standalone titled block with its own `title` string,
|
|
11
|
+
`collapsible` flag and imperative `SectionHandlers.expand/collapse`. The second one predated both
|
|
12
|
+
the fixed heading ramp and the `Accordion` family, so a collapsible section had a heading that
|
|
13
|
+
could not take the ramp's rungs and a disclosure that could not be driven from state.
|
|
14
|
+
|
|
15
|
+
A collapsible section is now the `Accordion` family with a heading rung composed inside it — one
|
|
16
|
+
heading family and one disclosure across static and collapsible. Replace the imperative handle
|
|
17
|
+
with the controlled `expanded`/`onToggle` pair (or `defaultExpanded` when nothing outside needs to
|
|
18
|
+
drive it).
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
// BEFORE
|
|
22
|
+
const ref = useRef<SectionHandlers>(null);
|
|
23
|
+
<Section ref={ref} icon="eye-off" title="Hidden fields" collapsible initialCollapsed>
|
|
24
|
+
{body}
|
|
25
|
+
</Section>
|
|
26
|
+
// …elsewhere: ref.current?.expand()
|
|
27
|
+
|
|
28
|
+
// AFTER
|
|
29
|
+
<Accordion expanded={open} onToggle={setOpen}>
|
|
30
|
+
<AccordionHeader>
|
|
31
|
+
<DialogSectionHeadingTitle icon="eye-off">Hidden fields</DialogSectionHeadingTitle>
|
|
32
|
+
</AccordionHeader>
|
|
33
|
+
<AccordionContent>{body}</AccordionContent>
|
|
34
|
+
</Accordion>
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Pick the heading rung by where the surface sits, exactly as for a static section:
|
|
38
|
+
`SectionHeadingTitle` (`##`), `SubsectionHeadingTitle` (`###`), `DialogSectionHeadingTitle`
|
|
39
|
+
(`####`). The last one gained `icon` and `description` in this release, so every rung of the ramp
|
|
40
|
+
now carries the same slots.
|
|
41
|
+
|
|
7
42
|
## v19 from 18.x
|
|
8
43
|
|
|
9
44
|
**`TaskFields` is DELETED, with `fieldsWidth`.** A task row was carrying a
|
package/docs/catalog.md
CHANGED
|
@@ -150,7 +150,10 @@ Two columnar shapes, and the choice is about data size:
|
|
|
150
150
|
`TrendChip` (delta), `Sparkline`, `BarChart` / `LineChart` / `PieChart` (the canonical SVG
|
|
151
151
|
set — no recharts), `RingGauge`, `ProgressBar` (its `compact` prop = ONE row, track + a
|
|
152
152
|
plain sm tabular count beside it — the cell/heading/peek-trigger meter; a caption floating
|
|
153
|
-
above a tiny bar reads misaligned
|
|
153
|
+
above a tiny bar reads misaligned. **The track clamps at 100%, the caption does not** — over
|
|
154
|
+
its max it reads `2,100 / 2,000 · 105%`, because a meter that says "100%" when you are over
|
|
155
|
+
tells the reader they are exactly at the limit. Numbers format in the reader's locale, so
|
|
156
|
+
never hand-format the value you pass in) / `StackedProgressBar` / `StepProgress`, `Breakdown` (a
|
|
154
157
|
stacked bar + ranked share rows, pressable to drill; `maxRows` folds the long tail behind a
|
|
155
158
|
"Show N more" toggle — `labels` to localize — so several facet cards align to one height in
|
|
156
159
|
a row), `Funnel` (a CONVERSION funnel — ordered stages as bars that NARROW; the step
|
|
@@ -172,6 +175,8 @@ to drill).
|
|
|
172
175
|
gap-spaced region + `SectionHeading`/`SectionHeadingTitle`/`SectionHeadingMeta`; no body
|
|
173
176
|
component — children are the body), `Subsection` (+ `SubsectionHeading`/
|
|
174
177
|
`SubsectionHeadingTitle` — the named group INSIDE a section, `###` lg-semibold title),
|
|
178
|
+
`DialogSectionHeadingTitle` (`####` md-semibold — the rung for a surface inside a DIALOG, whose
|
|
179
|
+
`DialogHeaderTitle` already occupies lg; composes with the Subsection layout),
|
|
175
180
|
`SectionStack` (the flat page's content column — owns the fixed 56px beat + hairline between
|
|
176
181
|
top-level blocks), `SubsectionStack` (a section's `Subsection` groups — fixed 32px beat,
|
|
177
182
|
space-only while the groups are SHORT, `divided` once they run long), `SectionCard`, `PageHeader` /
|
|
@@ -378,19 +383,17 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
378
383
|
`warning`/`error` render as an ARIA `alert`) — wrapping form fields in an alert is wrong.
|
|
379
384
|
The law: **content + fields → `Inset`; a message → `Callout`.**
|
|
380
385
|
- **`section_heading`** — `Section` · `SectionHeading` · `SectionHeadingTitle` ·
|
|
381
|
-
`SectionHeadingMeta` · `Subsection` · `SubsectionHeading` · `SubsectionHeadingTitle`
|
|
386
|
+
`SectionHeadingMeta` · `Subsection` · `SubsectionHeading` · `SubsectionHeadingTitle` ·
|
|
387
|
+
`DialogSectionHeadingTitle` — the
|
|
382
388
|
card-less twin of the Card family, compound, owns no margin; spacing via the Section gap
|
|
383
389
|
(16, fixed), no body component. `SectionHeadingTitle` is ALWAYS `##` (xl semibold;
|
|
384
390
|
`weight="medium"` opt-down only) + `info` for an ⓘ provenance popover after the title,
|
|
385
391
|
same as `CardHeaderTitle.info`. `SubsectionHeadingTitle` is the `###` lg-semibold level-3
|
|
386
392
|
title of a named group inside a section (same `info` ⓘ affordance as the section title) —
|
|
387
|
-
heading-row siblings ride its right edge
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
`SectionHandlers.expand/collapse`). **Warning:** two modules export a `Section` — the
|
|
392
|
-
layout grammar's card-less region is the one in `section_heading`; import from
|
|
393
|
-
`@lotics/ui/section` only when you specifically want this collapsible titled block.
|
|
393
|
+
heading-row siblings ride its right edge. `DialogSectionHeadingTitle` is the `####`
|
|
394
|
+
md-semibold rung with the SAME `icon`/`description`/`info` slots as the section title, so a
|
|
395
|
+
dialog surface loses only the type size, never an affordance. The heading ramp is FIXED:
|
|
396
|
+
`#` xxl / `##` xl / `###` lg / `####` md, no size props.
|
|
394
397
|
- **`section_stack`** — `SectionStack` · `SubsectionStack` — stacks that own the
|
|
395
398
|
between-block law, skipping null children: `SectionStack` = a fixed 56px beat + a hairline
|
|
396
399
|
`Divider` between top-level blocks; `SubsectionStack` = a fixed 32px beat, space-only while
|
package/docs/composition.md
CHANGED
|
@@ -53,12 +53,14 @@ restyle a heading level per-page.
|
|
|
53
53
|
slots carry each section's count and CTA); a `Text size="sm" weight="semibold"` stand-in is only
|
|
54
54
|
for small sub-groups. Do NOT wrap each section in its own `CardHeader`, do NOT hand-roll
|
|
55
55
|
sm-semibold lead lines on a flat page, and NEVER use a bare eyebrow as a section title.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
`
|
|
60
|
-
|
|
61
|
-
the
|
|
56
|
+
- **Dialog section title** — the FOURTH rung, for a surface hosted in a dialog:
|
|
57
|
+
`DialogSectionHeadingTitle` (`####` — md semibold, with the same `icon`/`description`/`info`
|
|
58
|
+
slots as `SectionHeadingTitle`, so only the type size steps down). It exists because
|
|
59
|
+
`DialogHeaderTitle` is **lg** — the rung `SubsectionHeadingTitle` occupies — so a dialog holding
|
|
60
|
+
a multi-section surface otherwise ties with, or is outranked by, its own content. Compose it
|
|
61
|
+
with the SUBSECTION layout (`Subsection` › `SubsectionHeading` › this, in a `SubsectionStack`):
|
|
62
|
+
only the type size steps down, the 12px body gap and 32px beat are already right. The ladder
|
|
63
|
+
stays ABSOLUTE — this is one more fixed size, never a `size`/`compact` knob on a rung above.
|
|
62
64
|
- **Subsection title** — the level BELOW a section on a long record surface: `Subsection` ›
|
|
63
65
|
`SubsectionHeading` › `SubsectionHeadingTitle` (`###` — lg semibold, with the same `info` ⓘ
|
|
64
66
|
affordance as `SectionHeadingTitle`; siblings — a `Badge`, a
|
|
@@ -233,10 +235,14 @@ below the pressed element — the composable `Accordion` family (`AccordionHeade
|
|
|
233
235
|
/ `AccordionMeta` / `AccordionContent`); a header-only accordion renders a plain row of identical
|
|
234
236
|
rhythm, so lists mix expandable + static rows. The body is **flush** with the header's left edge
|
|
235
237
|
(aligns with the title — like a `Section` body, no indent, no tinted well). `AccordionHeader` is a
|
|
236
|
-
layout slot: `AccordionTitle`/`AccordionMeta` is the compact list-row heading, but
|
|
237
|
-
**
|
|
238
|
-
`
|
|
239
|
-
collapsible
|
|
238
|
+
layout slot: `AccordionTitle`/`AccordionMeta` is the compact list-row heading, but a **collapsible
|
|
239
|
+
SECTION** is this same family with the heading rung composed inside it — `SectionHeadingTitle` /
|
|
240
|
+
`SubsectionHeadingTitle` / `DialogSectionHeadingTitle` (+ `SectionHeadingMeta`) in the
|
|
241
|
+
`AccordionHeader`, the body in `AccordionContent`. There is no separate collapsible-section
|
|
242
|
+
component: one heading family and one disclosure across static + collapsible. The disclosure
|
|
243
|
+
header IS the section heading — name it for what it hides, never a generic bucket ("Advanced",
|
|
244
|
+
"More", "Options") wrapped around one named section: that stacks two titles of the same rank and
|
|
245
|
+
leaves the reader no reason to open the chevron.
|
|
240
246
|
|
|
241
247
|
## No dead rows — registers & tables
|
|
242
248
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "20.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./vite": {
|
|
@@ -195,7 +195,6 @@
|
|
|
195
195
|
"./dots_indicator": "./src/dots_indicator.tsx",
|
|
196
196
|
"./list": "./src/list.tsx",
|
|
197
197
|
"./list_item": "./src/list_item.tsx",
|
|
198
|
-
"./section": "./src/section.tsx",
|
|
199
198
|
"./section_heading": "./src/section_heading.tsx",
|
|
200
199
|
"./section_stack": "./src/section_stack.tsx",
|
|
201
200
|
"./separator": "./src/separator.tsx",
|
package/src/accordion.tsx
CHANGED
|
@@ -103,7 +103,10 @@ export function AccordionHeader(props: AccordionHeaderProps) {
|
|
|
103
103
|
<PressableHighlight
|
|
104
104
|
focusRing onPress={toggle}
|
|
105
105
|
accessibilityRole="button"
|
|
106
|
-
accessibilityState
|
|
106
|
+
// The W3C prop, NOT `accessibilityState` — this react-native-web build drops the
|
|
107
|
+
// latter silently, so a disclosure shipped a `role="button"` that never announced
|
|
108
|
+
// whether its content was showing.
|
|
109
|
+
aria-expanded={expanded}
|
|
107
110
|
accessibilityLabel={accessibilityLabel}
|
|
108
111
|
style={[styles.headerRow, styles.pressable, style]}
|
|
109
112
|
>
|
package/src/breakdown.tsx
CHANGED
|
@@ -113,7 +113,8 @@ export function Breakdown(props: BreakdownProps) {
|
|
|
113
113
|
{collapsible ? (
|
|
114
114
|
<PressableHighlight
|
|
115
115
|
focusRing accessibilityRole="button"
|
|
116
|
-
accessibilityState
|
|
116
|
+
// The W3C prop — `accessibilityState` is dropped by this react-native-web build.
|
|
117
|
+
aria-expanded={expanded}
|
|
117
118
|
accessibilityLabel={expanded ? labels.less : labels.more(hidden)}
|
|
118
119
|
onPress={() => setExpanded((e) => !e)}
|
|
119
120
|
style={[styles.row, styles.pressable, styles.toggle]}
|
package/src/data_grid.tsx
CHANGED
|
@@ -75,7 +75,8 @@ export function DataGrid<T>(props: DataGridProps<T>) {
|
|
|
75
75
|
<FocusRingPressable
|
|
76
76
|
onPress={() => onToggleCollapse?.(g.key)}
|
|
77
77
|
accessibilityRole="button"
|
|
78
|
-
accessibilityState
|
|
78
|
+
// The W3C prop — `accessibilityState` is dropped by this react-native-web build.
|
|
79
|
+
aria-expanded={!isCollapsed}
|
|
79
80
|
disabled={!onToggleCollapse}
|
|
80
81
|
style={({ hovered }: { hovered?: boolean }) => [styles.section, hovered && onToggleCollapse ? styles.sectionHover : null]}
|
|
81
82
|
>
|
package/src/progress_bar.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { View, StyleSheet, type ViewStyle } from "react-native";
|
|
2
2
|
import { Text } from "./text";
|
|
3
3
|
import { colors } from "./colors";
|
|
4
|
+
import { useLocaleTag } from "./locale";
|
|
4
5
|
|
|
5
6
|
export type ProgressBarFormat = "percentage" | "fraction" | "none";
|
|
6
7
|
|
|
@@ -12,7 +13,8 @@ export interface ProgressBarProps {
|
|
|
12
13
|
* it. */
|
|
13
14
|
title?: string;
|
|
14
15
|
/** Caption above-right of the bar: `percentage` → "50%", `fraction` →
|
|
15
|
-
* "1
|
|
16
|
+
* "1,250 / 2,500 · 50%" (separators follow the reader's locale). Reports the
|
|
17
|
+
* TRUE ratio — over `max` it reads "105%" while the track stays clamped. */
|
|
16
18
|
format?: ProgressBarFormat;
|
|
17
19
|
color?: string;
|
|
18
20
|
completeColor?: string;
|
|
@@ -40,12 +42,23 @@ export function ProgressBar(props: ProgressBarProps) {
|
|
|
40
42
|
compact = false,
|
|
41
43
|
} = props;
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
// The pack is locale-aware (see `RemainderMeter`); a hardcoded tag renders 2.100 for an
|
|
46
|
+
// English reader, which reads as 2.1.
|
|
47
|
+
const localeTag = useLocaleTag();
|
|
48
|
+
/**
|
|
49
|
+
* The TRACK clamps — a fill wider than its own track is meaningless. The CAPTION does not: a
|
|
50
|
+
* meter reading `2.100 / 2.000 · 100%` asserts a number that is false, and a reader scanning
|
|
51
|
+
* percentages sees "exactly at the limit" when they are over it. Progress-toward-completion
|
|
52
|
+
* never exceeds its max, so this only diverges for the over-allowance case, where the true
|
|
53
|
+
* figure is the whole point.
|
|
54
|
+
*/
|
|
55
|
+
const ratio = max > 0 ? Math.max(0, (value / max) * 100) : 0;
|
|
56
|
+
const percentage = Math.min(100, ratio);
|
|
57
|
+
const isComplete = ratio >= 100;
|
|
45
58
|
|
|
46
59
|
if (compact) {
|
|
47
60
|
const label =
|
|
48
|
-
format === "percentage" ? `${Math.round(
|
|
61
|
+
format === "percentage" ? `${Math.round(ratio)}%` : `${value.toLocaleString(localeTag)}/${max.toLocaleString(localeTag)}`;
|
|
49
62
|
return (
|
|
50
63
|
<View style={styles.compactRow}>
|
|
51
64
|
<View style={[styles.track, styles.compactTrack]}>
|
|
@@ -62,9 +75,9 @@ export function ProgressBar(props: ProgressBarProps) {
|
|
|
62
75
|
|
|
63
76
|
const caption =
|
|
64
77
|
format === "fraction"
|
|
65
|
-
? `${value.toLocaleString(
|
|
78
|
+
? `${value.toLocaleString(localeTag)} / ${max.toLocaleString(localeTag)} · ${Math.round(ratio)}%`
|
|
66
79
|
: format === "percentage"
|
|
67
|
-
? `${Math.round(
|
|
80
|
+
? `${Math.round(ratio)}%`
|
|
68
81
|
: null;
|
|
69
82
|
|
|
70
83
|
return (
|
package/src/section_heading.tsx
CHANGED
|
@@ -110,6 +110,64 @@ export function SubsectionHeadingTitle(props: SubsectionHeadingTitleProps) {
|
|
|
110
110
|
);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
export interface DialogSectionHeadingTitleProps {
|
|
114
|
+
children: React.ReactNode;
|
|
115
|
+
/** Optional muted line under the title, as on `SectionHeadingTitle`. */
|
|
116
|
+
description?: string;
|
|
117
|
+
/** Leading glyph, as on `SectionHeadingTitle`. */
|
|
118
|
+
icon?: IconName;
|
|
119
|
+
/** Heading rank. Defaults to 4 — one level under `SubsectionHeadingTitle`'s 3. */
|
|
120
|
+
level?: HeadingLevel;
|
|
121
|
+
/** An ⓘ popover after the title, as on the two rungs above. */
|
|
122
|
+
info?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The section title for a surface hosted in a DIALOG — ALWAYS `####` (md semibold), the fourth
|
|
127
|
+
* rung of the same fixed ladder.
|
|
128
|
+
*
|
|
129
|
+
* It exists because `DialogHeaderTitle` is `lg`, which is the rung `SubsectionHeadingTitle`
|
|
130
|
+
* occupies: a dialog holding a multi-section surface has its own title tying with, or being
|
|
131
|
+
* outranked by, its content. The ladder stays ABSOLUTE — this is one more fixed size, never a
|
|
132
|
+
* knob on an existing rung. Do not reach for a `size`/`compact` prop on the titles above; the same
|
|
133
|
+
* rank rendering at two sizes is exactly what the fixed ramp prevents.
|
|
134
|
+
*
|
|
135
|
+
* Compose it with the SUBSECTION layout — `Subsection` › `SubsectionHeading` › this, inside a
|
|
136
|
+
* `SubsectionStack`. Only the type size steps down; the 12px body gap and the 32px between-group
|
|
137
|
+
* beat are already right for a dialog, so duplicating them under a `Dialog*` name would add three
|
|
138
|
+
* exports that differ from their siblings in nothing.
|
|
139
|
+
*
|
|
140
|
+
* <SubsectionStack divided>
|
|
141
|
+
* <Subsection>
|
|
142
|
+
* <SubsectionHeading>
|
|
143
|
+
* <DialogSectionHeadingTitle info="…">Usage</DialogSectionHeadingTitle>
|
|
144
|
+
* <SectionHeadingMeta>Aug 2 – Sep 2</SectionHeadingMeta>
|
|
145
|
+
* </SubsectionHeading>
|
|
146
|
+
* {body}
|
|
147
|
+
* </Subsection>
|
|
148
|
+
* </SubsectionStack>
|
|
149
|
+
*/
|
|
150
|
+
export function DialogSectionHeadingTitle(props: DialogSectionHeadingTitleProps) {
|
|
151
|
+
const { children, description, icon, level = 4, info } = props;
|
|
152
|
+
const words = useLoticsLocale();
|
|
153
|
+
return (
|
|
154
|
+
<View style={{ flex: 1, gap: 2 }}>
|
|
155
|
+
<View style={{ flexDirection: "row", alignItems: "center", gap: 6 }}>
|
|
156
|
+
{icon ? <Icon name={icon} size={16} /> : null}
|
|
157
|
+
<Text level={level} size="md" weight="semibold">
|
|
158
|
+
{children}
|
|
159
|
+
</Text>
|
|
160
|
+
{info ? <InfoPopover text={info} accessibilityLabel={words.sectionHeading.info} /> : null}
|
|
161
|
+
</View>
|
|
162
|
+
{description ? (
|
|
163
|
+
<Text color="zinc-500" size="sm">
|
|
164
|
+
{description}
|
|
165
|
+
</Text>
|
|
166
|
+
) : null}
|
|
167
|
+
</View>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
113
171
|
export interface SectionHeadingProps {
|
|
114
172
|
children: React.ReactNode;
|
|
115
173
|
style?: StyleProp<ViewStyle>;
|
package/src/select.tsx
CHANGED
|
@@ -228,11 +228,13 @@ function SelectTrigger<T extends string>({
|
|
|
228
228
|
testID={testID}
|
|
229
229
|
// Without a role this Pressable renders as an unfocusable <div> on web — the
|
|
230
230
|
// trigger drops out of the tab order and Enter/Space can't open it. `button`
|
|
231
|
-
// makes it tab-focusable and maps keyboard activation to onPress; `expanded`
|
|
232
|
-
// announces open/closed to assistive tech.
|
|
231
|
+
// makes it tab-focusable and maps keyboard activation to onPress; `aria-expanded`
|
|
232
|
+
// announces open/closed to assistive tech. The W3C props, NOT `accessibilityState` —
|
|
233
|
+
// this react-native-web build drops the latter silently.
|
|
233
234
|
accessibilityRole="button"
|
|
234
235
|
accessibilityLabel={accessibilityLabel}
|
|
235
|
-
|
|
236
|
+
aria-expanded={open}
|
|
237
|
+
aria-disabled={disabled}
|
|
236
238
|
style={(state) => [styles.pressable, CONTROL_TRANSITION, open && styles.opened, disabled && styles.disabled, style, state.hovered && !disabled && { borderColor: HOVER_BORDER }]}
|
|
237
239
|
onPress={!disabled ? onPress : undefined}
|
|
238
240
|
disabled={disabled}
|
package/src/spreadsheet_view.tsx
CHANGED
|
@@ -9,7 +9,9 @@ function SheetTab(props: { selected: boolean; onPress: () => void; name: string
|
|
|
9
9
|
return (
|
|
10
10
|
<FocusRingPressable
|
|
11
11
|
accessibilityRole="tab"
|
|
12
|
-
accessibilityState
|
|
12
|
+
// The W3C prop — `accessibilityState` is dropped by this react-native-web build, and a
|
|
13
|
+
// tab that never announces `aria-selected` leaves the tablist with no current item.
|
|
14
|
+
aria-selected={props.selected}
|
|
13
15
|
onPress={props.onPress}
|
|
14
16
|
style={[styles.tab, props.selected ? styles.tabActive : null]}
|
|
15
17
|
>
|
package/src/section.tsx
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { Pressable, View } from "react-native";
|
|
2
|
-
import { Text } from "./text";
|
|
3
|
-
import { useState, useImperativeHandle } from "react";
|
|
4
|
-
import { Icon, IconName } from "./icon";
|
|
5
|
-
import { Spacer } from "./spacer";
|
|
6
|
-
import { FOCUS_RING } from "./control_surface";
|
|
7
|
-
import { useFocusRing } from "./use_focus_ring";
|
|
8
|
-
|
|
9
|
-
export interface SectionHandlers {
|
|
10
|
-
expand: () => void;
|
|
11
|
-
collapse: () => void;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface SectionProps {
|
|
15
|
-
children: React.ReactNode;
|
|
16
|
-
title: string;
|
|
17
|
-
description?: string;
|
|
18
|
-
collapsible?: boolean;
|
|
19
|
-
icon?: IconName;
|
|
20
|
-
titleRight?: React.ReactNode;
|
|
21
|
-
initialCollapsed?: boolean;
|
|
22
|
-
ref?: React.Ref<SectionHandlers>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function Section(props: SectionProps) {
|
|
26
|
-
const {
|
|
27
|
-
children,
|
|
28
|
-
title,
|
|
29
|
-
icon,
|
|
30
|
-
description,
|
|
31
|
-
collapsible,
|
|
32
|
-
titleRight,
|
|
33
|
-
initialCollapsed = false,
|
|
34
|
-
ref,
|
|
35
|
-
} = props;
|
|
36
|
-
const [collapsed, setCollapsed] = useState(collapsible && initialCollapsed);
|
|
37
|
-
|
|
38
|
-
useImperativeHandle(ref, () => ({
|
|
39
|
-
expand: () => {
|
|
40
|
-
if (collapsible) {
|
|
41
|
-
setCollapsed(false);
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
collapse: () => {
|
|
45
|
-
if (collapsible) {
|
|
46
|
-
setCollapsed(true);
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
const { focusVisible, focusProps } = useFocusRing();
|
|
52
|
-
|
|
53
|
-
const titleComponent = collapsible ? (
|
|
54
|
-
<Pressable
|
|
55
|
-
{...focusProps}
|
|
56
|
-
style={[{ flexDirection: "row", alignItems: "center", gap: 8, flex: 1 }, focusVisible && { boxShadow: FOCUS_RING, borderRadius: 4 }]}
|
|
57
|
-
onPress={() => setCollapsed(!collapsed)}
|
|
58
|
-
>
|
|
59
|
-
<Icon name={collapsed ? "chevron-right" : "chevron-down"} size={20} />
|
|
60
|
-
{icon && <Icon name={icon} size={20} />}
|
|
61
|
-
<Text size="md" weight="medium" userSelect="none">
|
|
62
|
-
{title}
|
|
63
|
-
</Text>
|
|
64
|
-
</Pressable>
|
|
65
|
-
) : (
|
|
66
|
-
<View style={{ flex: 1 }}>
|
|
67
|
-
<Text size="md" weight="medium">
|
|
68
|
-
{title}
|
|
69
|
-
</Text>
|
|
70
|
-
</View>
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
return (
|
|
74
|
-
<View>
|
|
75
|
-
<View
|
|
76
|
-
style={{
|
|
77
|
-
flexDirection: "row",
|
|
78
|
-
alignItems: "center",
|
|
79
|
-
height: 40,
|
|
80
|
-
paddingBottom: 8,
|
|
81
|
-
}}
|
|
82
|
-
>
|
|
83
|
-
{titleComponent}
|
|
84
|
-
{titleRight}
|
|
85
|
-
</View>
|
|
86
|
-
{!collapsed && (
|
|
87
|
-
<>
|
|
88
|
-
{!!description && (
|
|
89
|
-
<>
|
|
90
|
-
<Text size="sm" color="zinc-500">
|
|
91
|
-
{description}
|
|
92
|
-
</Text>
|
|
93
|
-
<Spacer size={16} />
|
|
94
|
-
</>
|
|
95
|
-
)}
|
|
96
|
-
{children}
|
|
97
|
-
</>
|
|
98
|
-
)}
|
|
99
|
-
</View>
|
|
100
|
-
);
|
|
101
|
-
}
|