@lotics/ui 27.10.0 → 27.12.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 +1 -1
- package/docs/catalog.md +20 -1
- package/docs/composition.md +6 -8
- package/docs/templates.md +4 -4
- package/package.json +1 -1
- package/src/control_surface.ts +8 -1
- package/src/dialog.tsx +10 -0
- package/src/react_native.d.ts +8 -5
- package/src/screen_router.tsx +23 -3
- package/src/vite.d.mts +10 -0
- package/src/vite.mjs +42 -0
package/AGENTS.md
CHANGED
|
@@ -14,7 +14,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
14
14
|
|
|
15
15
|
| Doc | Read it for |
|
|
16
16
|
|---|---|
|
|
17
|
-
| [docs/catalog.md](./docs/catalog.md) | **The complete inventory** — Reach-by-role (each data role → the ONE canonical component) + every `@lotics/ui/<module>` entry point (incl. `@lotics/ui/vite`'s `loticsOptimizeDeps`
|
|
17
|
+
| [docs/catalog.md](./docs/catalog.md) | **The complete inventory** — Reach-by-role (each data role → the ONE canonical component) + every `@lotics/ui/<module>` entry point (incl. `@lotics/ui/vite`'s `loticsOptimizeDeps` + `loticsResolve()` — the pre-bundle list and the whole `resolve` block a custom-code app's `vite.config.ts` imports rather than hand-carries, dev-link included). Read before building any screen; reuse first. |
|
|
18
18
|
| [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, browser-autofill suppression (search controls only), find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (the `FilesEditor` COMPOUND — root owns selection/gallery/confirm, you compose the bar, a HOST verb reads `useFilesEditorSelection` — plus the three-way file INTAKE: CTA + `FileDropTarget` + `usePasteFiles`), stage gates, the commit-on-blur vs action-press ordering law (the kit gates the press — `pending_commits`). |
|
|
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 (modify → review-before-apply; create → save-direct + the `ResultHeader` receipt), findings, provenance, confidence; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md)., the whole run in a dialog (`AgentRunScope`/`AgentRunPane`/`AgentRunActions` — a parked question REPLACES the feed, actions in the footer) |
|
|
20
20
|
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude (incl. eyebrow vs group lead — a label is one or the other), banded cards, register vs inset rows, master-detail `Drawer` on a LIST screen vs a child collection's row EXPANDING inside a record, view controls, RECORD EXTENT (one page, sections scrolled to and never routed to), color discipline, typography, whitespace, and how to TEST an overlay component (a `Popover`-backed surface never mounts under jsdom). |
|
package/docs/catalog.md
CHANGED
|
@@ -325,6 +325,18 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
325
325
|
`import { loticsOptimizeDeps } from "@lotics/ui/vite"` → `optimizeDeps: { include: loticsOptimizeDeps }`.
|
|
326
326
|
To add app-specific entries, spread: `include: [...loticsOptimizeDeps, "my-dep"]`. A Node-loadable
|
|
327
327
|
`.mjs` leaf (a Vite config can't import a `.ts` from `node_modules`), zero-import by contract.
|
|
328
|
+
- **`vite`** — `loticsResolve()`: the whole `resolve` block, for the same reason and on the same
|
|
329
|
+
terms — `resolve: loticsResolve()`. Every entry is dictated by the kit's internals, so the kit
|
|
330
|
+
owns them: the `react-native` → `react-native-web` alias (**load-bearing** — without it the RN
|
|
331
|
+
primitives have no web implementation and the app does not build), `.web.tsx`-first `extensions`
|
|
332
|
+
(per-target variants win; `.mjs`/`.mts` kept for parity with Vite's default resolver), `dedupe`
|
|
333
|
+
for React + RN-Web (the kit is consumed across many subpaths, and a second React copy in one
|
|
334
|
+
chunk nulls the hooks dispatcher — "Invalid hook call"), and the `LOTICS_UI_SRC` **dev-link**,
|
|
335
|
+
read at call time so it lasts exactly one command. To add your own alias, spread:
|
|
336
|
+
`const base = loticsResolve(); resolve: { ...base, alias: [...base.alias, entry] }`.
|
|
337
|
+
`lotics app codegen` writes the matching `paths` into the app's `.lotics/tsconfig.link.json`,
|
|
338
|
+
so `tsc`, vitest, eslint and the editor resolve the same kit Vite does — the dev-link is not a
|
|
339
|
+
Vite-only trick that leaves every other tool checking the published package.
|
|
328
340
|
|
|
329
341
|
### Text & formatting
|
|
330
342
|
|
|
@@ -370,7 +382,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
370
382
|
- **`control_surface`** — `CONTROL_HEIGHT` (40), `CONTROL_RADIUS` (10), `CONTROL_TEXT_INSET`
|
|
371
383
|
(9 — how far a control insets its OWN text: 1px border + 8px padding; anything that must line
|
|
372
384
|
up with a control's WORDS rather than its box carries it, and `TASK_TEXT_INSET` IS it),
|
|
373
|
-
`FOCUS_RING
|
|
385
|
+
`FOCUS_RING` (a 2px OUTSET ring, so a container that CLIPS — any `ScrollView` — has to
|
|
386
|
+
keep padding on an edge a focusable child sits against, or the ring shears off on the
|
|
387
|
+
one interaction it signals; the padding still comes off the 8-grid, the ring is only
|
|
388
|
+
the floor), `HOVER_BORDER`, `CONTROL_TRANSITION`, `chipSurfaceStyle` — the shared
|
|
374
389
|
control-surface tokens.
|
|
375
390
|
- **`fonts.css`** — the Inter sheet (400/500/600, served by absolute URL so it resolves on
|
|
376
391
|
every origin an app runs from); the app entry imports it ONCE or every `Text` falls back
|
|
@@ -1166,6 +1181,10 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1166
1181
|
stack (a Drawer drilling into a LINKED record swaps its header to a BACK `IconButton`
|
|
1167
1182
|
while `canGoBack`). Key the router by record id so stepping between records resets the
|
|
1168
1183
|
stack. Worked example: [`tpl_item_list`](../examples/tpl_item_list.tsx) drawer.
|
|
1184
|
+
**`useScreenActive()` says whether the surrounding `Screen` is the one on top** —
|
|
1185
|
+
because "stays mounted" means its effects keep running, so anything a screen claims
|
|
1186
|
+
GLOBALLY (a keyboard shortcut) must stand down while it is buried, or the covered
|
|
1187
|
+
screen answers input aimed at the one in front of it. `true` outside any `Screen`.
|
|
1169
1188
|
- **`route_matching`** — pure `:param` route-pattern utilities — `routeMatches` /
|
|
1170
1189
|
`parseRouteParams` / `findBestPattern` (exact beats parameterized) / `shouldRouteMatch`;
|
|
1171
1190
|
the matching core under `ScreenRouter`/`Dialog`.
|
package/docs/composition.md
CHANGED
|
@@ -493,8 +493,8 @@ All view controls are 40px tall (`CONTROL_HEIGHT`), `sm` labels, in ONE wrapping
|
|
|
493
493
|
"success"/green button. Decision UIs put positive/negative color on the STATUS (dot) and verdict
|
|
494
494
|
(colored `Text`), not the buttons.
|
|
495
495
|
- **Create buttons carry NO `+` icon** — "Add fee", not "+ Add fee". A create is `primary` and
|
|
496
|
-
|
|
497
|
-
|
|
496
|
+
rides its section's HEADING row at the right edge — see § The add-placement law for why, and
|
|
497
|
+
for the one-verb-one-weight rule that keeps it primary in the `EmptyState` too. (The
|
|
498
498
|
icon-only `+` `IconButton` — a composer's attach — is a different thing and keeps its glyph.)
|
|
499
499
|
|
|
500
500
|
## The action-gating law — disabled + the reason as a co-located Callout
|
|
@@ -594,15 +594,13 @@ gate's SCOPE, once** — never prose beside the button, never revealed only on p
|
|
|
594
594
|
|---|---|
|
|
595
595
|
| a VALUE (copy it, open what it points at) | ON the value — `InlineButton` in the editor's `actions` |
|
|
596
596
|
| a ROW (remove it, move it) | ON the row — an `ActionMenu`, or the row's own controls |
|
|
597
|
-
| the LIST (one more of them) |
|
|
597
|
+
| the LIST (one more of them) | its section's HEADING row, right edge, `primary` — see [the add-placement law](#the-add-placement-law--a-sections-add-rides-its-heading-row-right-edge) |
|
|
598
598
|
| the RECORD (issue, print, hand off) | its own action row at the section's END |
|
|
599
599
|
| the record's EXISTENCE | the `DangerZone`, last on the page |
|
|
600
600
|
|
|
601
|
-
The
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
creates appears at the BOTTOM, nowhere near the button that made it. Put it under the last row,
|
|
605
|
-
on the list's own left edge, and the button is where its result will be.
|
|
601
|
+
The ADD is the one verb this law does NOT place next to its effect, and the add-placement law
|
|
602
|
+
says why: a position that never moves beats a position that is adjacent, because "below the
|
|
603
|
+
last row" is a different place on every list and no place at all on an empty one.
|
|
606
604
|
**An add is `primary`, in the `EmptyState` too.** It is the act its section offers, so it
|
|
607
605
|
carries that section's weight; a lone `secondary` button reads as though the real action were
|
|
608
606
|
somewhere else. The rule that matters is that ONE VERB KEEPS ONE WEIGHT — the same add must
|
package/docs/templates.md
CHANGED
|
@@ -92,11 +92,11 @@ Laws for any screen where a record's data is edited and a primary action produce
|
|
|
92
92
|
7. **Header = identity + provenance.** Id, the key identifier, created-at. No metrics or badges
|
|
93
93
|
duplicating data a section already owns.
|
|
94
94
|
8. **Action placement — a verb goes where its effect lands.** Value verbs on the value
|
|
95
|
-
(`InlineButton`), row verbs on the row, an ADD
|
|
96
|
-
|
|
95
|
+
(`InlineButton`), row verbs on the row, an ADD on its section's HEADING row at the right edge
|
|
96
|
+
(primary — the one spot that does not move with the row count, and primary in the
|
|
97
|
+
`EmptyState` too: one verb, one weight),
|
|
97
98
|
a committing act in its own row at the section's end,
|
|
98
|
-
deletion in the `DangerZone`.
|
|
99
|
-
in the heading sits above what it extends and competes with the title. A committing act rides
|
|
99
|
+
deletion in the `DangerZone`. A committing act rides
|
|
100
100
|
an empty-label `DetailRow` so it lands on the CONTROL column — never right-floated, which
|
|
101
101
|
aligns to nothing (only overlay footers right-align).
|
|
102
102
|
9. **Destructive placement.** Solid `danger`, bottom-left after the entity's fields, ONE
|
package/package.json
CHANGED
package/src/control_surface.ts
CHANGED
|
@@ -23,6 +23,13 @@ export const MIN_CONTROL_WIDTH = 160;
|
|
|
23
23
|
* (avatars, dots, switches, sliders, progress, icon buttons) stay full. */
|
|
24
24
|
export const CONTROL_RADIUS = 10;
|
|
25
25
|
|
|
26
|
+
/** How far {@link FOCUS_RING} extends BEYOND the control's border box. Module-local
|
|
27
|
+
* on purpose: it is a FLOOR for anyone who clips (a container reserving less than
|
|
28
|
+
* this on an edge a focusable child sits against shears that child's ring), never a
|
|
29
|
+
* spacing value — padding comes off the 8-grid in `spacing.ts`, which clears this
|
|
30
|
+
* several times over without coupling a layout number to a decoration one. */
|
|
31
|
+
const FOCUS_RING_WIDTH = 2;
|
|
32
|
+
|
|
26
33
|
/** THE focus ring — a 2px zinc-900 box-shadow flush against the box (offset 0),
|
|
27
34
|
* layout-neutral (never reflows). The kit no longer ships a global `:focus-visible`
|
|
28
35
|
* CSS rule, so every interactive control paints THIS inline when keyboard-focused
|
|
@@ -30,7 +37,7 @@ export const CONTROL_RADIUS = 10;
|
|
|
30
37
|
* trigger (Picker / Combobox / InlineSelect / InlineDatePicker) wears it on its open
|
|
31
38
|
* state so an open trigger reads identically to a keyboard-focused one — one token,
|
|
32
39
|
* one look. Apps building their own controls reuse it. */
|
|
33
|
-
export const FOCUS_RING = `0 0 0
|
|
40
|
+
export const FOCUS_RING = `0 0 0 ${FOCUS_RING_WIDTH}px ${colors.zinc[900]}`;
|
|
34
41
|
|
|
35
42
|
/**
|
|
36
43
|
* How far a control surface insets its OWN text: 1px of border (transparent on an
|
package/src/dialog.tsx
CHANGED
|
@@ -313,6 +313,16 @@ const styles = StyleSheet.create({
|
|
|
313
313
|
minHeight: 56,
|
|
314
314
|
},
|
|
315
315
|
scrollAreaContent: {
|
|
316
|
+
// A ScrollView clips, so the FIRST child sat flush against the boundary and lost
|
|
317
|
+
// the outer half of its focus ring — an input at the top of a dialog showed a
|
|
318
|
+
// sheared ring on the one interaction the ring exists to signal. 8 clears that
|
|
319
|
+
// several times over, but the ring is only the FLOOR here, not the reason: this
|
|
320
|
+
// is separation from the header, so it comes off the 8-grid like every other gap.
|
|
321
|
+
// `DialogHeader` already pays 8 below itself, making the visible gap a clean 16.
|
|
322
|
+
//
|
|
323
|
+
// Asymmetric with the bottom on purpose — 24 there is scroll RUNOUT, so the last
|
|
324
|
+
// row isn't jammed against the edge at the end of a scroll. Different job.
|
|
325
|
+
paddingTop: 8,
|
|
316
326
|
paddingBottom: 24,
|
|
317
327
|
},
|
|
318
328
|
footer: {
|
package/src/react_native.d.ts
CHANGED
|
@@ -4,11 +4,14 @@ import "react-native";
|
|
|
4
4
|
// Pressable's `hovered` callback state, plus web-only ViewStyle / TextStyle
|
|
5
5
|
// properties (cursor, outline, boxShadow, etc.) used by its primitives.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// TypeScript does not auto-load a `.d.ts` from inside a dependency — the
|
|
8
|
+
// augmentation has to be visible in the consumer's own `include`. In-repo
|
|
9
|
+
// consumers (frontend) keep a copy in `src/`; a custom-code app does NOT, because
|
|
10
|
+
// `lotics app codegen` copies THIS file into its `.lotics/` on every run. That
|
|
11
|
+
// direction matters: a scaffolded snapshot silently falls behind whatever the kit
|
|
12
|
+
// adds here — the starter's copy was already missing `TextStyle`'s `boxShadow`,
|
|
13
|
+
// `transitionDuration` and `transitionProperty`, so an app author writing one got
|
|
14
|
+
// a type error the kit itself does not have.
|
|
12
15
|
declare module "react-native" {
|
|
13
16
|
interface PressableStateCallbackType {
|
|
14
17
|
hovered: boolean;
|
package/src/screen_router.tsx
CHANGED
|
@@ -159,6 +159,24 @@ export function useNavigationStack(
|
|
|
159
159
|
// Screen
|
|
160
160
|
// ============================================================================
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Whether the surrounding {@link Screen} is the one on top.
|
|
164
|
+
*
|
|
165
|
+
* A screen stacked BEHIND another stays mounted (`display: none`) so its scroll
|
|
166
|
+
* position survives, which means its effects keep running — including any
|
|
167
|
+
* keyboard shortcut it registered. Anything that claims a global key must read
|
|
168
|
+
* this and stand down while hidden, or the buried screen answers a keystroke the
|
|
169
|
+
* user aimed at the screen in front of it.
|
|
170
|
+
*
|
|
171
|
+
* Defaults to `true`, so a subtree outside any `Screen` behaves as if visible.
|
|
172
|
+
*/
|
|
173
|
+
const ScreenActiveContext = createContext(true);
|
|
174
|
+
|
|
175
|
+
/** @see ScreenActiveContext */
|
|
176
|
+
export function useScreenActive(): boolean {
|
|
177
|
+
return useContext(ScreenActiveContext);
|
|
178
|
+
}
|
|
179
|
+
|
|
162
180
|
export interface ScreenProps {
|
|
163
181
|
/** Route pattern to match. Use empty string "" for the default/initial screen. */
|
|
164
182
|
route: string;
|
|
@@ -208,9 +226,11 @@ export function Screen(props: ScreenProps) {
|
|
|
208
226
|
const params = parseRouteParams(route, matchingStackPath);
|
|
209
227
|
|
|
210
228
|
return (
|
|
211
|
-
<
|
|
212
|
-
{
|
|
213
|
-
|
|
229
|
+
<ScreenActiveContext.Provider value={isActive}>
|
|
230
|
+
<View style={isActive ? screenStyles.active : screenStyles.hidden}>
|
|
231
|
+
{typeof children === "function" ? children(params) : children}
|
|
232
|
+
</View>
|
|
233
|
+
</ScreenActiveContext.Provider>
|
|
214
234
|
);
|
|
215
235
|
}
|
|
216
236
|
|
package/src/vite.d.mts
CHANGED
|
@@ -5,3 +5,13 @@
|
|
|
5
5
|
* extend: `include: [...loticsOptimizeDeps, "my-extra-dep"]`.
|
|
6
6
|
*/
|
|
7
7
|
export declare const loticsOptimizeDeps: readonly string[];
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The Vite `resolve` block for a Lotics custom-code app (runtime in `vite.mjs`).
|
|
11
|
+
* Reads `LOTICS_UI_SRC` at call time, so the dev-link is decided per command.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loticsResolve(): {
|
|
14
|
+
alias: ({ find: RegExp; replacement: string } | { find: string; replacement: string })[];
|
|
15
|
+
extensions: string[];
|
|
16
|
+
dedupe: string[];
|
|
17
|
+
};
|
package/src/vite.mjs
CHANGED
|
@@ -71,3 +71,45 @@ export const loticsOptimizeDeps = [
|
|
|
71
71
|
"fast-formula-parser",
|
|
72
72
|
"@formulajs/formulajs",
|
|
73
73
|
];
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The complete Vite `resolve` block a Lotics custom-code app needs — the second
|
|
77
|
+
* half of the same contract `loticsOptimizeDeps` carries, and shipped here for
|
|
78
|
+
* the same reason: it tracks the installed @lotics/ui and cannot drift.
|
|
79
|
+
*
|
|
80
|
+
* Everything in it exists because the kit ships RN-Web SOURCE:
|
|
81
|
+
* - the `react-native` → `react-native-web` alias, so the kit's RN primitives
|
|
82
|
+
* (View/Text/Pressable/StyleSheet) render on the web at all. This entry is
|
|
83
|
+
* LOAD-BEARING: the deleted `lotics ui link` edited it by regex and twice
|
|
84
|
+
* deleted it along with the array's closing bracket (GAP-133/142), which is
|
|
85
|
+
* why the dev-link below is computed here rather than written into an app.
|
|
86
|
+
* - `.web.tsx`-first `extensions`, so per-target variants (avatar.web.tsx) win,
|
|
87
|
+
* plus `.mjs`/`.mts` to keep parity with Vite's default resolver — an override
|
|
88
|
+
* otherwise drops them and a package shipping only `.mjs` (lucide-react/dynamic,
|
|
89
|
+
* reached by DynamicIcon/AppIcon) fails to resolve under `lotics app dev` while
|
|
90
|
+
* the prod build resolves it, so the gap is dev-only and silent.
|
|
91
|
+
* - `dedupe`, because the kit is consumed across many subpath entries and Vite
|
|
92
|
+
* can otherwise pre-bundle one into its own chunk with a second React copy —
|
|
93
|
+
* a hook called from there hits a null dispatcher ("Invalid hook call").
|
|
94
|
+
* - the `LOTICS_UI_SRC` dev-link, when set: kit edits go live under
|
|
95
|
+
* `lotics app dev` (HMR) and bundle under `lotics app deploy`, with no publish
|
|
96
|
+
* round-trip. `lotics app codegen` writes the matching `paths` into
|
|
97
|
+
* `.lotics/tsconfig.link.json` from the same variable, so `tsc`, vitest, eslint
|
|
98
|
+
* and the editor resolve the same copy Vite does — the dev-link is not a
|
|
99
|
+
* Vite-only trick that lies to every other tool.
|
|
100
|
+
*
|
|
101
|
+
* An app that needs its own alias spreads the result:
|
|
102
|
+
* const base = loticsResolve();
|
|
103
|
+
* resolve: { ...base, alias: [...base.alias, { find: "x", replacement: "y" }] }
|
|
104
|
+
*/
|
|
105
|
+
export function loticsResolve() {
|
|
106
|
+
const uiSrc = process.env.LOTICS_UI_SRC;
|
|
107
|
+
return {
|
|
108
|
+
alias: [
|
|
109
|
+
...(uiSrc ? [{ find: /^@lotics\/ui\/(.+)$/, replacement: `${uiSrc}/$1` }] : []),
|
|
110
|
+
{ find: "react-native", replacement: "react-native-web" },
|
|
111
|
+
],
|
|
112
|
+
extensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".mjs", ".mts"],
|
|
113
|
+
dedupe: ["react", "react-dom", "react-native-web"],
|
|
114
|
+
};
|
|
115
|
+
}
|