@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 +2 -2
- package/docs/catalog.md +3 -2
- package/docs/reviewing.md +41 -0
- package/package.json +1 -1
- package/src/calendar/calendar_view.tsx +8 -2
- package/src/gantt/gantt_view.tsx +6 -2
- package/src/vite.d.mts +7 -0
- package/src/vite.mjs +47 -15
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 §"
|
|
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.
|
|
700
|
-
|
|
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,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,
|
|
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.
|
package/src/gantt/gantt_view.tsx
CHANGED
|
@@ -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(),
|
|
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.
|
|
7
|
-
* plain
|
|
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
|
|
13
|
-
* `vite.config.ts` `optimizeDeps.include` MUST carry for `lotics app dev`
|
|
14
|
-
* render
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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({
|
|
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
|