@lotics/ui 47.14.0 → 47.15.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 CHANGED
@@ -18,7 +18,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
18
18
  | [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, choosing a CHOICE control by option count, find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (`InlineFiles` for a record ROW, the `FilesEditor` COMPOUND for a whole section, plus the three-way file INTAKE), stage gates, and the commit-on-blur vs action-press ordering law. |
19
19
  | [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split — it turns on WHO supplied the values; findings, provenance, confidence (`Finding` is filed here because AI produces most of them, NOT because it is AI-only — a rules-derived contradiction is as in scope as an extraction's); **after the run** — a stored record that fills up from several writers; the whole run in a dialog, **stopping**; **review surfaces compose from atoms** — `DiffValue`, `DiffMark`, `useChangeSet`. |
20
20
  | [docs/composition.md](./docs/composition.md) | The design-language contract — **the form comes before the treatment** (name what the subject IS before reaching for parts), canvas + content column, heading altitude, register vs inset rows, the button ladder, master-detail `Drawer`, the register's rhythm, craft and TRIAGE bands, where the accent goes, theming as an app-level ESCAPE HATCH, color discipline, typography, **one fact one surface** (a chooser over the thing it chooses is a SELECTOR, not a second card grid; a control that only swaps one number for another is replaced by showing both), **a width floor answers can this be READ and never can this be operated** (the operating question passes a row whose every string is already cut), **a status column earns its slot by carrying a REASON rather than a state** (the floor asks for the fact to be visible, not for a badge), **an identity mark's GLYPH is derived from the name's distinguishing part** (an organization's first word is its legal form, so untreated every row marks one letter), **a row subject's supporting line takes a REFERENCE and never a sentence** (an explanation truncates to the clause the reader already knew), **a list stays fast by keeping IDENTITY rather than memoizing** (key by the record not the index; `memo` is the last resort and does nothing against a freshly-built prop), **a surface keyed by a whole MONTH takes a `MonthStepper`, never a range field** (its `max` bound is the contract), **a subject-by-date sheet of STATES is a `StateMatrix`** (a glyph per cell, the legend derived from the same list; not a `Matrix`, where a blank means zero), **a report's comparator is DERIVED from the selected range with `previousPeriod`, never a second control** (show it on the figure — `KPIStripItem.trend`/`caption`, `Metric previousValue`, a second `LineChart` series; a grain `SegmentedControl` beside the date field re-adds the mode the field removed), and where a commit sits. |
21
- | [docs/reviewing.md](./docs/reviewing.md) | **Reviewing a screen you built — and the answer when one "looks bad"** — the other docs say what good looks like; this one says how to find out whether you achieved it. Run it BEFORE the deploy: a typecheck, a lint, a test and `app check` are all blind to how a screen reads. GATES that outrank every treatment question, then the probes, each naming what to COLLECT and the SIGNATURE in the numbers — including the truncation sweep (a row whose halves both measure fine and whose text is cut) the summary-vs-facet check (a strip that keeps describing the unfiltered set) and the derivable-column test (a status badge its neighbour already answers) the first-line drift a column shows when its cell is sometimes one line and sometimes two, the cross-FRAME check (what the screen destroys when a row arrives — the one defect that exists in no single frame), and the semantic-section check (a danger or empty section drawn as an ordinary one, and the primitive nobody reached for). Plus the extraction snippets, what measurement cannot see, and how to act on a finding. |
21
+ | [docs/reviewing.md](./docs/reviewing.md) | **Reviewing a screen you built — and the answer when one "looks bad"** — the other docs say what good looks like; this one says how to find out whether you achieved it. Run it BEFORE the deploy: a typecheck, a lint, a test and `app check` are all blind to how a screen reads. GATES that outrank every treatment question, then the probes, each naming what to COLLECT and the SIGNATURE in the numbers — including the truncation sweep (a row whose halves both measure fine and whose text is cut) the summary-vs-facet check (a strip that keeps describing the unfiltered set) and the derivable-column test (a status badge its neighbour already answers) the opinions-the-reader-cannot-operate pair (bands nobody asked for, pills crowding a search) the first-line drift a column shows when its cell is sometimes one line and sometimes two, the cross-FRAME check (what the screen destroys when a row arrives — the one defect that exists in no single frame), and the semantic-section check (a danger or empty section drawn as an ordinary one, and the primitive nobody reached for). Plus the extraction snippets, what measurement cannot see, and how to act on a finding. |
22
22
  | [docs/testing.md](./docs/testing.md) | Driving the kit in a browser — the three anatomies where the a11y tree says one thing and a driver must do another: a `PressDoor` row whose named button always intercepts pointer events (by design), portalled overlays that render at the top of the DOM, and custom pointer drag that `dragTo` cannot move. |
23
23
  | [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget) and the ACTIVITY shape — a communications feed where the row's label is the GIST and the body varies by medium, one anatomy rather than a row type per kind. |
24
24
 
@@ -159,7 +159,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
159
159
  reading rather than a neutral one, and `toggleStripWidth(n)` for the column that seats it.
160
160
  Selected wears `chipSurfaceStyle`'s doubled dark edge — a brand ground on a selection is the
161
161
  decision the kit already reversed once.
162
- → [catalog.md §"Choice controls"](./docs/catalog.md).
162
+ → [catalog.md §"Pickers & selection controls"](./docs/catalog.md).
163
163
  - **Pure primitives only** — no i18n, analytics, or domain types in `src/` (pass `labels`,
164
164
  callbacks); Lotics-coupled UI belongs in `@lotics/ui-internal`.
165
165
  - **A button never contains a control** — a row that presses open AND carries its own controls
package/docs/catalog.md CHANGED
@@ -696,8 +696,9 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
696
696
  `vi`→`vi-VN`); date/time controls read it via **`useLocaleTag`** (`resolveLocaleTag` is its
697
697
  pure core) so the segmented field's part ORDER (dd/MM vs MM/dd), the picker calendar's
698
698
  weekday/month names, and `formatDate` display all follow the provider with no per-instance
699
- `locale` prop — an explicit `locale` still overrides. **Limitation:** the calendar/gantt
700
- views and the comment labels do not read the provider pass their `labels` props directly.
699
+ `locale` prop — an explicit `locale` still overrides. `CalendarView` and `GanttView` are
700
+ wired the same way, chrome and tick names both. **Limitation:** `CommentList` carries its
701
+ own English defaults — pass its `labels` prop directly.
701
702
  - **`colors`** — the palette + `withAlpha`, `solid`, `tint`, `ramp`, `ColorName`,
702
703
  `isColorName`, `asColorName` (coerce a stored option/status token to a `ColorName`,
703
704
  neutral fallback).
package/docs/reviewing.md CHANGED
@@ -669,6 +669,47 @@ reports two cells. And a `Text` wrapping another `Text` keeps its own words in a
669
669
  so a walk over leaf ELEMENTS measures only the nested span and reports the cell starting a
670
670
  hundred pixels in; measure text nodes with a `Range`.
671
671
 
672
+ ### 8k. Opinions the reader cannot operate — authored bands, and pills beside a search
673
+
674
+ Two defects of one kind: the screen holds a view the reader can neither inspect nor put down. Both
675
+ render perfectly, and neither shows up in any measurement of treatment.
676
+
677
+ **Measure, bands.** Count the group bands a register renders, then look in the filter band for a
678
+ control that produced them. Bands with no `group by` control are AUTHORED — the screen decided the
679
+ cut. Then ask the harder half: is the value those bands group on also a COLUMN? A register that
680
+ bands by something it never shows as a column leaves that fact unsortable and unfilterable, and it
681
+ disappears the moment the reader wants a different cut.
682
+
683
+ ```js
684
+ // bands rendered vs. a control that could have produced them
685
+ const bands = document.querySelectorAll('[data-table-group], [role="rowgroup"] > [role="presentation"]').length
686
+ const controls = [...document.querySelectorAll('button, select')].map(e => e.textContent.trim())
687
+ ```
688
+
689
+ **Measure, pills.** In one filter band, count `SearchInput`s and count pill rows (`ChipGroup`). Both
690
+ above zero is the finding: a row of pills next to a search box is two controls competing to be read
691
+ first, and the pills win on ink while the search wins on habit.
692
+
693
+ ```js
694
+ const band = document.querySelector('[data-filter-band]')
695
+ band && { search: band.querySelectorAll('input[type="search"], input[type="text"]').length,
696
+ pills: band.querySelectorAll('[role="radiogroup"], [data-chip-group]').length }
697
+ ```
698
+
699
+ **The source-side signature is cheaper than either.** A file rendering band headings whose label
700
+ comes from the row's own data, with no state driving it — a `groupBy`/`lens` param absent from the
701
+ url or component state. And an import of both `search_input` and `chip_group` in one screen.
702
+
703
+ **What the fix is not.** Deleting the bands is not the fix, and neither is keeping them and adding a
704
+ chip. Ship the value as a column first, then offer grouping as a control defaulting to none, and
705
+ drop that column while its own grouping is on — the band states it once, the column would repeat it
706
+ on every row beneath it.
707
+
708
+ → [composition.md](./composition.md) §"A register that TRIAGES — `TableGroup`" (flat is the
709
+ default) and §"One view-control vocabulary" (a `ChipGroup` lens only where the band has no search).
710
+ Both rules predate this probe; it exists because reading them is not the same as noticing you broke
711
+ them.
712
+
672
713
  ### 9. The absence pass — what catches "bland"
673
714
  Every probe above reads an element that EXISTS. None fires on a screen where nothing is wrong and
674
715
  nothing is right, which is the screen reported as bland. Defect-removal converges on *inoffensive*;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/ui",
3
- "version": "47.14.0",
3
+ "version": "47.15.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  "./accordion": "./src/accordion.tsx",
@@ -1,7 +1,7 @@
1
1
  import { useMemo, useState, type ReactNode } from "react";
2
2
  import { View, StyleSheet, type LayoutChangeEvent } from "react-native";
3
3
  import { colors } from "../colors";
4
- import { useLoticsLocale } from "../locale";
4
+ import { useLoticsLocale, useLocaleTag } from "../locale";
5
5
  import { CalendarProvider, useCalendar, type CalendarContextValue } from "./context";
6
6
  import { CalendarToolbar, CALENDAR_TOOLBAR_HEIGHT } from "./calendar_toolbar";
7
7
  import { CalendarMonth, MIN_MONTH_HEIGHT } from "./month_view";
@@ -33,6 +33,8 @@ export interface CalendarViewProps<T = unknown> {
33
33
  /** Which modes the built-in toolbar offers. Default: all four. */
34
34
  views?: CalendarViewMode[];
35
35
  weekStartsOn?: Weekday;
36
+ /** BCP-47 tag for the weekday / month / time names. Defaults to the active
37
+ * `LoticsLocaleProvider` pack's `bcp47`; pass one only to override it. */
36
38
  locale?: string;
37
39
  /** Per-instance overrides; the rest resolve from `LoticsLocale.calendarView`. */
38
40
  labels?: Partial<CalendarViewLabels>;
@@ -88,13 +90,17 @@ export interface CalendarViewProps<T = unknown> {
88
90
  */
89
91
  export function CalendarView<T = unknown>(props: CalendarViewProps<T>) {
90
92
  const {
91
- events, weekStartsOn = 1, locale, views = ALL_VIEWS,
93
+ events, weekStartsOn = 1, views = ALL_VIEWS,
92
94
  dayStartHour = 0, dayEndHour = 24, compactBelow = DEFAULT_COMPACT_BELOW, now,
93
95
  onEventPress, onSlotPress, renderEvent, children,
94
96
  } = props;
95
97
 
96
98
  const pack = useLoticsLocale();
97
99
  const labels: CalendarViewLabels = { ...pack.calendarView, ...props.labels };
100
+ // Every Intl rendering the grid does (weekday and month names, the times down
101
+ // the day axis) reads this, so the active pack alone localizes the calendar —
102
+ // an app that already supplies `vi` must not also thread a `locale` prop.
103
+ const locale = useLocaleTag(props.locale);
98
104
 
99
105
  // One reading of the clock for the whole tree, so the badge, the now line and
100
106
  // the scroll target cannot disagree across a midnight boundary mid-render.
@@ -5,7 +5,7 @@ import { colors, solid } from "../colors";
5
5
  import { proportionalRadius } from "../control_surface";
6
6
  import { FocusRingPressable } from "../focus_ring_pressable";
7
7
  import { SegmentedControl, type SegmentOption } from "../segmented_control";
8
- import { useLoticsLocale } from "../locale";
8
+ import { useLoticsLocale, useLocaleTag } from "../locale";
9
9
  import { dayDiff } from "../calendar/dates";
10
10
  import { axisRange, barGeometry, buildRows, buildTicks, pxPerDay } from "./scale";
11
11
  import type { GanttLabels, GanttScale, GanttTask } from "./types";
@@ -23,6 +23,8 @@ export interface GanttViewProps<T = unknown> {
23
23
  defaultScale?: GanttScale;
24
24
  /** What counts as today for the marker and the opening scroll position. */
25
25
  today?: Date;
26
+ /** BCP-47 tag for the axis tick names. Defaults to the active
27
+ * `LoticsLocaleProvider` pack's `bcp47`; pass one only to override it. */
26
28
  locale?: string;
27
29
  /** Optional toolbar caption shown left of the zoom switch. */
28
30
  title?: string;
@@ -48,9 +50,11 @@ export interface GanttViewProps<T = unknown> {
48
50
  * Renders at its natural height — wrap in a `ScrollView` for very long lists.
49
51
  */
50
52
  export function GanttView<T = unknown>(props: GanttViewProps<T>) {
51
- const { tasks, defaultScale = "week", today = new Date(), locale, title, onTaskPress } = props;
53
+ const { tasks, defaultScale = "week", today = new Date(), title, onTaskPress } = props;
52
54
  const pack = useLoticsLocale();
53
55
  const L: GanttLabels = { ...pack.gantt, ...props.labels };
56
+ // The axis tick names come from Intl, so the active pack alone localizes them.
57
+ const locale = useLocaleTag(props.locale);
54
58
  const [scale, setScale] = useState<GanttScale>(defaultScale);
55
59
 
56
60
  const rows = useMemo(() => buildRows(tasks), [tasks]);
package/src/vite.d.mts CHANGED
@@ -6,6 +6,13 @@
6
6
  */
7
7
  export declare const loticsOptimizeDeps: readonly string[];
8
8
 
9
+ /**
10
+ * The specifiers `optimizeDeps.exclude` must carry so the kit stays ONE module
11
+ * graph — pre-bundling a @lotics/ui subpath inlines a private copy of every
12
+ * context-owning kit module it reaches. `readonly` for the same reason.
13
+ */
14
+ export declare const loticsOptimizeExclude: readonly string[];
15
+
9
16
  /**
10
17
  * The Vite `resolve` block for a Lotics custom-code app (runtime in `vite.mjs`).
11
18
  * Reads `LOTICS_UI_SRC` at call time, so the dev-link is decided per command.
package/src/vite.mjs CHANGED
@@ -3,22 +3,24 @@
3
3
  *
4
4
  * ZERO-IMPORT LEAF. This module is loaded inside Vite's Node config context
5
5
  * (a scaffolded app's `vite.config.ts` imports it), so it must pull in NOTHING —
6
- * no react-native-web, no kit components, no other module. It exports only a
7
- * plain array of strings. It ships as `.mjs` (not `.ts`) on purpose: a Vite
6
+ * no react-native-web, no kit components, no other module. Everything it exports
7
+ * is plain data or computed from `process.env` / `import.meta.resolve` at call
8
+ * time. It ships as `.mjs` (not `.ts`) on purpose: a Vite
8
9
  * config bundle externalizes its bare imports to their on-disk paths and hands
9
10
  * them to Node's `import()`, and Node refuses to strip types for a `.ts` file
10
11
  * under `node_modules` — a source `.ts` export would fail the config load.
11
12
  *
12
- * `loticsOptimizeDeps` is the set of module specifiers a scaffolded app's
13
- * `vite.config.ts` `optimizeDeps.include` MUST carry for `lotics app dev` to
14
- * render. Vite's dev dep-optimizer pre-bundles these to synthesize the
15
- * CJS-interop (`default` / named) exports the RN-ecosystem + markdown subtree
16
- * ship un-prebundled otherwise one of them blanks the dev iframe with "does
17
- * not provide an export named 'default'/'parse'". Dev-ONLY: the production
18
- * rollup build resolves the interop without it (typecheck/lint/build stay green
19
- * while the dev iframe goes blank), which is exactly why this list ships WITH
20
- * the kit so it can never drift from what @lotics/ui's transitive deps
21
- * require across a major bump.
13
+ * `loticsOptimizeDeps` is the set of THIRD-PARTY module specifiers a scaffolded
14
+ * app's `vite.config.ts` `optimizeDeps.include` MUST carry for `lotics app dev`
15
+ * to render @lotics/ui itself is never one of them (see
16
+ * {@link loticsOptimizeExclude}). Vite's dev dep-optimizer pre-bundles these to
17
+ * synthesize the CJS-interop (`default` / named) exports the RN-ecosystem +
18
+ * markdown subtree ship un-prebundled otherwise one of them blanks the dev
19
+ * iframe with "does not provide an export named 'default'/'parse'". Dev-ONLY:
20
+ * the production rollup build resolves the interop without it (typecheck/lint/
21
+ * build stay green while the dev iframe goes blank), which is exactly why this
22
+ * list ships WITH the kit — so it can never drift from what @lotics/ui's
23
+ * transitive deps require across a major bump.
22
24
  *
23
25
  * Why each family is here:
24
26
  * - `react-native-svg` — @lotics/ui's svg charts (PieChart/LineChart/Sparkline/
@@ -34,10 +36,13 @@
34
36
  * - `react` / `react-dom` / `react-dom/client` — pinned into the pre-bundle so
35
37
  * every kit subpath shares one React instance (else "Invalid hook call").
36
38
  *
37
- * Usage — consumed by a scaffolded app's `vite.config.ts`:
39
+ * Usage — consumed by a scaffolded app's `vite.config.ts`, always as a pair with
40
+ * {@link loticsOptimizeExclude}:
38
41
  *
39
- * import { loticsOptimizeDeps } from "@lotics/ui/vite";
40
- * export default defineConfig({ optimizeDeps: { include: loticsOptimizeDeps } });
42
+ * import { loticsOptimizeDeps, loticsOptimizeExclude } from "@lotics/ui/vite";
43
+ * export default defineConfig({
44
+ * optimizeDeps: { include: loticsOptimizeDeps, exclude: loticsOptimizeExclude },
45
+ * });
41
46
  *
42
47
  * To add app-specific pre-bundle entries, spread the list:
43
48
  *
@@ -72,6 +77,33 @@ export const loticsOptimizeDeps = [
72
77
  "@formulajs/formulajs",
73
78
  ];
74
79
 
80
+ /**
81
+ * The specifiers a scaffolded app's `optimizeDeps.exclude` MUST carry — the
82
+ * mirror of `loticsOptimizeDeps`, and load-bearing for the same dev-only reason.
83
+ *
84
+ * THE INVARIANT: the kit is ONE module graph. @lotics/ui ships source under
85
+ * `node_modules`, so Vite's dep optimizer auto-DISCOVERS every subpath an app
86
+ * imports (`@lotics/ui/calendar`, `@lotics/ui/table`, …) and pre-bundles each as
87
+ * its own entry. A kit module that is not itself an entry — `./locale`,
88
+ * `./tooltip`, `./portal`, `./popover` — is then INLINED into every chunk that
89
+ * reaches it, while the app's own `@lotics/ui/locale` import is served from
90
+ * source. Around 30 kit modules own a React context, and a duplicated module is
91
+ * a duplicated `createContext`: the provider fills one instance and the
92
+ * component reads the other, so `LoticsLocaleProvider` silently stops reaching
93
+ * `CalendarView` (chrome falls back to English) and a mounted `TooltipProvider`
94
+ * stops reaching a `Tooltip`. Excluding the kit keeps every subpath on the
95
+ * source graph, where one module means one context.
96
+ *
97
+ * Dev-ONLY, and silent: the production rollup build resolves the whole kit as
98
+ * one graph already, so typecheck/lint/build stay green while `lotics app dev`
99
+ * renders the wrong strings — which is why the list ships WITH the kit rather
100
+ * than living in an app that cannot know how the kit's contexts are laid out.
101
+ *
102
+ * @lotics/app-sdk needs no entry here: it imports @lotics/ui nowhere (only in
103
+ * doc comments), so its own optimized chunk inlines no kit module.
104
+ */
105
+ export const loticsOptimizeExclude = ["@lotics/ui"];
106
+
75
107
  /**
76
108
  * The complete Vite `resolve` block a Lotics custom-code app needs — the second
77
109
  * half of the same contract `loticsOptimizeDeps` carries, and shipped here for