@lotics/ui 47.2.0 → 47.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/MIGRATION.md CHANGED
@@ -1,8 +1,47 @@
1
1
  # @lotics/ui — migration notes
2
2
 
3
3
  Breaking changes, newest first — normally per major, plus the rare minor that broke a type
4
- anyway (recorded under its exact version). The current contract lives in `AGENTS.md` + `docs/`;
5
- this file exists only to move an app from one release to the next.
4
+ anyway (recorded under its exact version), **and a minor that changes what an existing call site
5
+ LOOKS LIKE with no code change**. That last kind needs an entry precisely because nothing forces
6
+ the author to read one: their screens shift on an `npm update` and they have nothing to attribute
7
+ it to, so they revert or file a bug. The current contract lives in `AGENTS.md` + `docs/`; this
8
+ file exists only to move an app from one release to the next.
9
+
10
+ ## 47.1.0
11
+
12
+ **No API breaks and no action required — but four things change what an existing screen looks
13
+ like.** All four are fixes to a component asserting something it could not know, so the new
14
+ rendering is the correct one; they are listed so nobody spends an afternoon on them.
15
+
16
+ - **`ProgressBar`'s compact caption is muted at every ratio.** It used to paint `success` at
17
+ ≥ 100%. A meter can mean "done" (a task, an upload) or "used up" (a credit limit, a quota), and
18
+ the same 100% is good in one and bad in the other — so every usage-against-a-cap meter drew a red
19
+ bar with a green number beside it. The verdict now lives only where the caller states it: the
20
+ FILL, via `completeColor`. A task meter that wants a green number renders its own caption
21
+ (`format="none"` plus your text).
22
+ - **`Breakdown`'s value is `medium` and `StackedBarChart`'s label is `regular`.** They draw the same
23
+ row — label left, right-aligned figure, proportion bar — and disagreed about weight, so a card
24
+ placing them side by side made one read as more important for no stated reason. One rule now: the
25
+ label names the series and stays regular, the figure carries the weight, and `semibold` stays
26
+ reserved for selection.
27
+ - **A disabled `MenuButton` now looks disabled.** It blocked the press and announced
28
+ `aria-disabled` while rendering at full ink, so a blocked item was pixel-identical to a live one.
29
+ If you were hiding items rather than disabling them to work around this, you can stop.
30
+ - **`ChargeLines`' narrow quantity slot is 60px (was 48).** `28,9` needed 33px in the 30 available
31
+ and clipped. Slightly less room for the price column in the narrow fork.
32
+
33
+ Two smaller layout corrections in the same release: `Finding`'s readings stretch instead of
34
+ shrink-wrapping past their column, and `RecordSummary`'s identity row wraps rather than letting the
35
+ title shrink while the metric keeps full width.
36
+
37
+ **New, all additive and defaulted to the previous behaviour:** `TableColumn.lead` (declare the
38
+ fixed chrome inside a flexible cell, so the fit's read floor is for the TEXT — pass it wherever you
39
+ put a mark in the subject cell), `MemberChip.showAvatar` (`false` where the member is one field
40
+ among columns rather than the row's subject), `InlineStatic.actions` and `InlineSlot.actions` (a
41
+ verb about a read-only value, on the value's own surface), `ChargeLines.formatQuantity`
42
+ (band-level, like `formatMoney` — without it the kit printed `28.9` in a locale that writes
43
+ `28,9`), `ThumbnailStack.decorative` (silence the marks where the row already names the thing),
44
+ and `FormTextInput` now forwards `optionalLabel`, which it previously accepted and dropped.
6
45
 
7
46
  ## 47.0.0
8
47
 
@@ -83,6 +83,15 @@ already has: errors arrive from a validate pass, and editing a field clears that
83
83
 
84
84
  ## A picker over a select FIELD keeps the field's colours — `optionPicker`
85
85
 
86
+ **First, the precondition nobody states: the field's colours have to MEAN something.** A platform
87
+ that auto-assigns option colours does it **by position** — the first option blue, then green,
88
+ amber, red, purple, teal — so a field whose options were created without colours carries a
89
+ sequence nobody chose. Propagate that faithfully and you ship the accident consistently: measured
90
+ on a real workspace, the same rule would have painted an in-transit order **red** and a customer on
91
+ delivery hold **green**, inches from the control that decides whether to keep delivering to them.
92
+ So: set the colours on the FIELD, semantically, then carry them everywhere. Carrying an
93
+ auto-assigned default is worse than plain text, because plain text does not assert anything.
94
+
86
95
  <InlineSelect {...optionPicker(fields.city?.options ?? [])} // a category
87
96
  value={cityKey} onSave={(v) => save({ city: v })} />
88
97
  <InlineSelect {...optionPicker(fields.status?.options ?? [], { badge: "dot" })} // a status
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
@@ -560,6 +578,23 @@ on a defect that does not exist. Walk the surface with real `Tab` presses and re
560
578
  `box-shadow` at each stop; `.focus()` is only good for asking what CAN take focus, never for how it
561
579
  looks when it does.
562
580
 
581
+ ### 9b. One VALUE, one treatment — the cross-shape check
582
+ Probe 1 asks "same shape → same treatment", which groups filter chips with filter chips and table
583
+ cells with table cells, finds each group internally consistent, and passes. The defect it cannot
584
+ see is **one datum reading two ways across shapes**: a stage drawn as a coloured badge in the
585
+ register and as plain text in the filter that selects it, on the same screen.
586
+
587
+ **Collect**, per select field: every surface its value renders on — register cell, filter chip and
588
+ its dropdown, drawer, record field at rest and editing, picker option, diff row, summary line,
589
+ chart legend — and the colour each one paints. **The signature** is one field whose renders
590
+ disagree: some coloured, some plain, or two different colours for one key.
591
+
592
+ Two causes, both silent. The picker's own option shape has no colour member, so the obvious
593
+ adapter drops it and compiles; and a hand-written key→colour map beside the field's own colours is
594
+ a second copy free to disagree — check it against the FIELD, not against itself.
595
+
596
+ → [data_entry.md](./data_entry.md) §"A picker over a select FIELD keeps the field's colours".
597
+
563
598
  ### 10. Microcopy
564
599
  - **No punctuation doing a word's job.** ` · ` is banned outright — it claims a relationship while
565
600
  refusing to name it, and it is the loudest templated-metadata tell there is. If a mark cannot be
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "47.2.0",
3
+ "version": "47.3.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": ">=0.460.0",
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",
@@ -124,7 +124,7 @@ export function FilterChip(props: FilterChipProps) {
124
124
  testID={testID}
125
125
  onPress={() => setOpen(!isOpen)}
126
126
  accessibilityRole="button"
127
- accessibilityState={{ expanded: isOpen }}
127
+ aria-expanded={isOpen}
128
128
  style={styles.rowHeader}
129
129
  >
130
130
  <Text size="sm" weight="medium" color="zinc-700">{label}</Text>
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"],