@lotics/ui 47.2.1 → 47.4.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/docs/catalog.md +8 -2
- package/docs/composition.md +3 -1
- package/docs/reviewing.md +18 -0
- package/package.json +2 -2
- package/src/chip_group.tsx +41 -7
- package/src/vite.mjs +33 -0
package/docs/catalog.md
CHANGED
|
@@ -1125,8 +1125,14 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1125
1125
|
choice. → `data_entry.md` §"Choosing a CHOICE control" — and check the states really ARE
|
|
1126
1126
|
exclusive: independent capabilities that happen to be exclusive today bake today's
|
|
1127
1127
|
combinations into the type.
|
|
1128
|
-
- **`chip_group`** — `ChipGroup`: pill single-select over a small visible set.
|
|
1129
|
-
|
|
1128
|
+
- **`chip_group`** — `ChipGroup`: pill single-select over a small visible set. The row wraps by
|
|
1129
|
+
default; `overflow="scroll"` keeps it to ONE row for a register FILTER, where six options
|
|
1130
|
+
carrying counts wrap to three rows — 136px on a phone, more than the search band and the view
|
|
1131
|
+
switcher combined. Deliberately NOT a screen-width branch: width is not the question, the job
|
|
1132
|
+
is, and both jobs occur at every width. Never `scroll` on a form field or a view control —
|
|
1133
|
+
chips are worth more than a `Picker` there only because every option is visible, and a swipe
|
|
1134
|
+
is exactly what takes that away. → `data_entry.md` §"Choosing a CHOICE control" for which
|
|
1135
|
+
control the count and the job call for.
|
|
1130
1136
|
- **`checkbox`** — `Checkbox`: the bare square check control. Bare means UNLABELLED — reach for
|
|
1131
1137
|
`CheckboxInput` unless something else already names it (a table's select-all, a row's
|
|
1132
1138
|
leading slot). A square means CHOSEN; a ring means DONE, and spending the wrong one teaches a
|
package/docs/composition.md
CHANGED
|
@@ -1015,7 +1015,9 @@ by what KIND of thing they are.
|
|
|
1015
1015
|
stays one consistent band. Never a bare `TextInputField` + search icon.
|
|
1016
1016
|
- **`ChipGroup`** is THE one-of-N lens (≤ ~10 options the user flips between; include "All";
|
|
1017
1017
|
counts in the label). Filters ONE list to a SUBSET — including by process stage / lifecycle
|
|
1018
|
-
state.
|
|
1018
|
+
state. As a FILTER above a register it takes `overflow="scroll"` and costs one row on a phone
|
|
1019
|
+
instead of three; a VIEW CONTROL ("how are they arranged") and a form field keep the default
|
|
1020
|
+
wrap. The line is the toolbar's own — which rows vs how they are arranged — not a width.
|
|
1019
1021
|
- **`FilterChip`** is THE secondary-dimension filter — a compact chip that opens a composed
|
|
1020
1022
|
popover editor (`OptionList` multi / `Slider range` / `Counter` / date range). Single-select
|
|
1021
1023
|
closes on pick via the `{({ close }) => …}` render prop.
|
package/docs/reviewing.md
CHANGED
|
@@ -10,6 +10,24 @@ Everything here is about a screen **that already renders**. Building one starts
|
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
## The kit's peer ranges are a resolution contract, not a floor
|
|
14
|
+
|
|
15
|
+
`lucide-react-native` sat at `>=0.460.0` — unbounded — while `icon.tsx` deep-imports
|
|
16
|
+
`lucide-react-native/dist/esm/icons/<name>` to keep the ~1650-icon barrel out of the
|
|
17
|
+
Metro bundle. lucide later shipped an `exports` map listing only `.` and `./icons`, so
|
|
18
|
+
that path stopped being publicly exported, and every resolver that honours `exports`
|
|
19
|
+
(Vite's dep optimizer among them) began refusing it. Nothing in this repo changed; the
|
|
20
|
+
range simply accepted the version that changed the rules.
|
|
21
|
+
|
|
22
|
+
It surfaced late and looked intermittent because a warm `.vite` cache never re-resolves
|
|
23
|
+
— only a fresh install hits it, which is why it kept coming back and kept getting
|
|
24
|
+
patched as a symptom (clear the cache, add a local `exclude`).
|
|
25
|
+
|
|
26
|
+
**A peer range with no upper bound on a package we deep-import is a standing invitation
|
|
27
|
+
for this.** When the kit reaches past a package's public entry points, bound the range,
|
|
28
|
+
and say in the code why the deep path is being used at all.
|
|
29
|
+
|
|
30
|
+
|
|
13
31
|
## The method
|
|
14
32
|
|
|
15
33
|
**"Looks off" is where a finding starts.** Trust the impression and use measurement to say
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "47.
|
|
3
|
+
"version": "47.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./accordion": "./src/accordion.tsx",
|
|
@@ -355,7 +355,7 @@
|
|
|
355
355
|
"@react-native-picker/picker": ">=2.0.0",
|
|
356
356
|
"expo-image": ">=3.0.0",
|
|
357
357
|
"lucide-react": ">=0.460.0",
|
|
358
|
-
"lucide-react-native": "
|
|
358
|
+
"lucide-react-native": "^0.562.0",
|
|
359
359
|
"react": "^19.2.0",
|
|
360
360
|
"react-dom": "^19.2.0",
|
|
361
361
|
"react-native": ">=0.85.0",
|
package/src/chip_group.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { StyleSheet, View, type ViewStyle } from "react-native";
|
|
1
|
+
import { ScrollView, StyleSheet, View, type ViewStyle } from "react-native";
|
|
2
2
|
import { colors } from "./colors";
|
|
3
3
|
import { Icon, type IconName } from "./icon";
|
|
4
4
|
import { solid, type ColorName } from "./colors";
|
|
@@ -6,8 +6,9 @@ import { Text } from "./text";
|
|
|
6
6
|
import { PressableHighlight } from "./pressable_highlight";
|
|
7
7
|
import { chipSurfaceStyle } from "./control_surface";
|
|
8
8
|
|
|
9
|
-
// One-of-N chips: every option visible, one tap to switch
|
|
10
|
-
//
|
|
9
|
+
// One-of-N chips: every option visible, one tap to switch. The row WRAPS by
|
|
10
|
+
// default; a hot filter with many options passes `overflow="scroll"` to keep
|
|
11
|
+
// itself to one row. Quiet zinc styling — bordered white at rest, and the active one
|
|
11
12
|
// keeps that same ground and takes a doubled zinc-900 EDGE instead, so a
|
|
12
13
|
// selection never spends the brand and never speaks the row's language (the
|
|
13
14
|
// reasoning is on `chipSurfaceStyle`). Colour stays reserved for status +
|
|
@@ -76,12 +77,34 @@ export interface ChipGroupProps<T extends string = string> {
|
|
|
76
77
|
* yet) is valid. */
|
|
77
78
|
value: T;
|
|
78
79
|
onValueChange: (value: T) => void;
|
|
80
|
+
/**
|
|
81
|
+
* What the row does when it runs out of width.
|
|
82
|
+
*
|
|
83
|
+
* `"wrap"` (the default) keeps EVERY option on screen, which is the whole
|
|
84
|
+
* reason a form field or a composer reaches for chips instead of a `Picker` —
|
|
85
|
+
* a required single-select that hides half its values behind a swipe is a
|
|
86
|
+
* worse `Picker`, not a better one. Those sets are 2-3 short options and fit
|
|
87
|
+
* a phone anyway.
|
|
88
|
+
*
|
|
89
|
+
* `"scroll"` is for the other job: a HOT FILTER above a register, where the
|
|
90
|
+
* set is long enough and the labels carry counts. Six status options wrap to
|
|
91
|
+
* THREE ROWS at 390px — 136px, more than the search band and the view
|
|
92
|
+
* switcher combined, and the largest single block above the first record.
|
|
93
|
+
* One row costs 40px; the leading options stay a tap away and the tail is a
|
|
94
|
+
* swipe.
|
|
95
|
+
*
|
|
96
|
+
* Not derived from screen width, because width is not the question — the JOB
|
|
97
|
+
* is, and both jobs occur at every width. A `ScrollView` whose content fits
|
|
98
|
+
* does not scroll, so this is a no-op on a desktop either way.
|
|
99
|
+
*/
|
|
100
|
+
overflow?: "wrap" | "scroll";
|
|
79
101
|
}
|
|
80
102
|
|
|
81
103
|
export function ChipGroup<T extends string = string>(props: ChipGroupProps<T>) {
|
|
82
|
-
const { accessibilityLabel, options, value, onValueChange } = props;
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
const { accessibilityLabel, options, value, onValueChange, overflow = "wrap" } = props;
|
|
105
|
+
|
|
106
|
+
const inner = (
|
|
107
|
+
<>
|
|
85
108
|
{options.map((option) => {
|
|
86
109
|
const active = option.value === value;
|
|
87
110
|
return (
|
|
@@ -140,11 +163,22 @@ export function ChipGroup<T extends string = string>(props: ChipGroupProps<T>) {
|
|
|
140
163
|
</PressableHighlight>
|
|
141
164
|
);
|
|
142
165
|
})}
|
|
143
|
-
|
|
166
|
+
</>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return overflow === "scroll" ? (
|
|
170
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.scrollRow}>
|
|
171
|
+
{inner}
|
|
172
|
+
</ScrollView>
|
|
173
|
+
) : (
|
|
174
|
+
<View style={styles.wrapRow}>{inner}</View>
|
|
144
175
|
);
|
|
145
176
|
}
|
|
146
177
|
|
|
147
178
|
const styles = StyleSheet.create({
|
|
179
|
+
wrapRow: { flexDirection: "row", alignItems: "center", flexWrap: "wrap", gap: 8 },
|
|
180
|
+
// paddingRight so the last chip does not sit flush against the edge mid-scroll
|
|
181
|
+
scrollRow: { flexDirection: "row", alignItems: "center", gap: 8, paddingRight: 8 },
|
|
148
182
|
dot: { width: 6, height: 6, borderRadius: 3 },
|
|
149
183
|
chip: {
|
|
150
184
|
flexDirection: "row",
|
package/src/vite.mjs
CHANGED
|
@@ -102,11 +102,44 @@ export const loticsOptimizeDeps = [
|
|
|
102
102
|
* const base = loticsResolve();
|
|
103
103
|
* resolve: { ...base, alias: [...base.alias, { find: "x", replacement: "y" }] }
|
|
104
104
|
*/
|
|
105
|
+
/**
|
|
106
|
+
* Absolute path to lucide's ESM icon directory, or null when it isn't installed.
|
|
107
|
+
*
|
|
108
|
+
* `icon.tsx` deep-imports `lucide-react-native/dist/esm/icons/<name>` to keep the
|
|
109
|
+
* ~1650-icon barrel out of the Metro bundle. lucide 0.562 ships an `exports` map
|
|
110
|
+
* listing only `.` and `./icons`, so that specifier is not publicly exported —
|
|
111
|
+
* and Vite's dep optimizer, which honours `exports`, refuses it outright:
|
|
112
|
+
* `Missing "./dist/esm/icons/activity" specifier`. The rollup build resolves it
|
|
113
|
+
* anyway, so the failure is dev-only, and it stayed hidden because a warm
|
|
114
|
+
* `.vite` cache never re-resolves. Any app installing fresh hits it immediately.
|
|
115
|
+
*
|
|
116
|
+
* Resolving the package's own ESM entry and walking to its sibling `icons/`
|
|
117
|
+
* gives the optimizer a real path, so no consumer has to touch its vite config
|
|
118
|
+
* and no bundle changes. `import.meta.resolve` (not `createRequire`) because we
|
|
119
|
+
* need the ESM condition — the require condition lands in `dist/cjs`.
|
|
120
|
+
*/
|
|
121
|
+
function lucideIconsDir() {
|
|
122
|
+
try {
|
|
123
|
+
// `import.meta.resolve`, and string-slicing rather than `node:path` /
|
|
124
|
+
// `node:url`, because this module must stay a zero-import leaf — see the
|
|
125
|
+
// header. The ESM condition matters: the require condition lands in
|
|
126
|
+
// `dist/cjs`, whose icons are CJS and would defeat the point.
|
|
127
|
+
const url = import.meta.resolve("lucide-react-native");
|
|
128
|
+
if (!url.startsWith("file://")) return null;
|
|
129
|
+
const file = decodeURIComponent(url.slice("file://".length));
|
|
130
|
+
return `${file.slice(0, file.lastIndexOf("/"))}/icons`;
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
105
136
|
export function loticsResolve() {
|
|
106
137
|
const uiSrc = process.env.LOTICS_UI_SRC;
|
|
138
|
+
const icons = lucideIconsDir();
|
|
107
139
|
return {
|
|
108
140
|
alias: [
|
|
109
141
|
...(uiSrc ? [{ find: /^@lotics\/ui\/(.+)$/, replacement: `${uiSrc}/$1` }] : []),
|
|
142
|
+
...(icons ? [{ find: /^lucide-react-native\/dist\/esm\/icons\/(.+)$/, replacement: `${icons}/$1` }] : []),
|
|
110
143
|
{ find: "react-native", replacement: "react-native-web" },
|
|
111
144
|
],
|
|
112
145
|
extensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".mjs", ".mts"],
|