diff --git a/AGENTS.md b/AGENTS.md index 9a16809..2359e81 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,8 @@ This file is the canonical agent guide for every tool (`CLAUDE.md` is just 1. **Zero runtime dependencies.** Nothing under `dependencies`, ever. `Intl.*` is allowed (built into every runtime). React appears only as an optional peer for - `./react`. Mechanically gated: `packages/lingo/scripts/check-zero-deps.mjs` + `./react` and `./react-native`. Mechanically gated: + `packages/lingo/scripts/check-zero-deps.mjs` (part of `bun run check` and CI). 2. **Size budgets are enforced** (`bun run size`, min+gzip via esbuild). **`packages/lingo/scripts/size.mjs` is the single source of truth for budget @@ -127,6 +128,7 @@ Inside `packages/lingo/` the same scripts run directly (`bun run test`, etc.). - `dom/` — headless input controller (entry `./dom`) - `element/` — custom-element wrapper over the DOM controller (entry `./element`) - `react/` — React hook adapter (entry `./react`) +- `react-native/` — DOM-free React Native TextInput hook (entry `./react-native`) - `ai/` — Standard Schema fields for LLM structured output (entry `./ai`) - `mcp/` — Model Context Protocol tool helpers (entry `./mcp`) diff --git a/CONTEXT.md b/CONTEXT.md index bac9210..681ef88 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -112,8 +112,8 @@ its value. Fields never rewrite text while typing (D6). ## Infrastructure nouns **Entry:** a published subpath — `.`, `./core`, `./date`, `./dom`, `./element`, -`./describe`, `./catalog`, `./schema`, `./ai`, `./mcp`, `./react`, `./complete`, -`./locales/*`. *Avoid: subpackage, plugin.* +`./describe`, `./catalog`, `./schema`, `./ai`, `./mcp`, `./react`, +`./react-native`, `./complete`, `./locales/*`. *Avoid: subpackage, plugin.* **Catalog:** the read-only query surface (`./catalog`) over built-in unit/kind/currency data — list kinds/units, resolve refs, related units, diff --git a/apps/site/package.json b/apps/site/package.json index 0274c17..66105bf 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -6,6 +6,7 @@ "dev": "NEXT_DIST_DIR=.next-dev next dev", "build": "bun run typecheck && next build", "start": "next start", + "test": "vitest run", "typecheck": "tsgo --project tsconfig.json --noEmit", "kill": "bun ./scripts/kill-port.mjs", "clean": "bun ./scripts/clean-next.mjs", @@ -31,12 +32,13 @@ }, "devDependencies": { "@tailwindcss/postcss": "^4", - "@typescript/native-preview": "7.0.0-dev.20260704.1", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "@typescript/native-preview": "7.0.0-dev.20260704.1", "eslint": "^9", "eslint-config-next": "16.2.10", + "jsdom": "^29.1.1", "react-doctor": "^0.6.2", "react-grab": "^0.1.48", "react-scan": "^0.5.7", diff --git a/apps/site/public/llms-small.txt b/apps/site/public/llms-small.txt index 6576144..3833bb1 100644 --- a/apps/site/public/llms-small.txt +++ b/apps/site/public/llms-small.txt @@ -4,7 +4,7 @@ Agent fetch order (online): `https://lingo.pascal.app/llms.txt` (index) → `https://lingo.pascal.app/docs/
.md` (per-topic) or `https://lingo.pascal.app/llms-full.txt` (complete narrative). Offline: this file (`node_modules/@pascal-app/lingo/llms.txt`) is the compressed self-contained reference. Keep user measurements as strings in tool schemas; call lingo to convert, validate, surface spans, and handle ambiguity. -Entries: `@pascal-app/lingo` (core+units+fuzzy), `@pascal-app/lingo/date`, `@pascal-app/lingo/dom`, `@pascal-app/lingo/element` (``), `@pascal-app/lingo/react`, `@pascal-app/lingo/ai` (LLM tool fields), `@pascal-app/lingo/mcp` (MCP tool helper), `@pascal-app/lingo/describe` (rich value/result descriptions), `@pascal-app/lingo/catalog` (query units/kinds/currencies + ISO country codes), `@pascal-app/lingo/complete` (ranked autocomplete completions), `@pascal-app/lingo/schema` (JSON Schema + OpenAPI + enum reference), `@pascal-app/lingo/locales/{en,en-gb,es,fr,pt,zh,ja}` (tree-shakeable language packs), `@pascal-app/lingo/core`. +Entries: `@pascal-app/lingo` (core+units+fuzzy), `@pascal-app/lingo/date`, `@pascal-app/lingo/dom`, `@pascal-app/lingo/element` (``), `@pascal-app/lingo/react`, `@pascal-app/lingo/react-native`, `@pascal-app/lingo/ai` (LLM tool fields), `@pascal-app/lingo/mcp` (MCP tool helper), `@pascal-app/lingo/describe` (rich value/result descriptions), `@pascal-app/lingo/catalog` (query units/kinds/currencies + ISO country codes), `@pascal-app/lingo/complete` (ranked autocomplete completions), `@pascal-app/lingo/schema` (JSON Schema + OpenAPI + enum reference), `@pascal-app/lingo/locales/{en,en-gb,es,fr,pt,zh,ja}` (tree-shakeable language packs), `@pascal-app/lingo/core`. ## Core (`@pascal-app/lingo`) @@ -41,7 +41,7 @@ const items = completions("10 kg to 16", { kind: "mass", limit: 8 }) // [{ text, result, confidence, source }] — source: parse|alternative|unit-ambiguity|unit-prefix|implied-unit|range-implied|cross-kind|date ``` -Distinct from success `alternatives`, failure `candidate`, and issue `suggestions`. Wire into `lingoInput({ complete, onComplete })` for autocomplete dropdowns. Pass `date: (text) => parseDate(text, { now })` to opt into date completions without bundling `@pascal-app/lingo/date` into `@pascal-app/lingo/complete`. +Distinct from success `alternatives`, failure `candidate`, and issue `suggestions`. Wire into `lingoInput({ complete, onComplete })` or `useLingoInput({ complete })` for autocomplete dropdowns. The React hook exposes `completions`, `highlightedIndex`, `setHighlightedIndex()`, and `selectCompletion()` while keeping popup UI caller-owned. Pass `date: (text) => parseDate(text, { now })` to opt into date completions without bundling `@pascal-app/lingo/date` into `@pascal-app/lingo/complete`. ## Locales (`@pascal-app/lingo/locales/*`) @@ -110,7 +110,102 @@ field.set("6ft") field.commit() // hidden input submits canonical value ``` -`field.value` · `.quantity` · `.result` · `.state` ('idle'|'incomplete'|'valid'|'invalid') · `.set()` · `.commit()` · `.update()` · `.destroy()`. Never rewrites while typing; canonicalizes on blur/Enter/submit. With `complete`/`onComplete`, the input gets the headless WAI-ARIA combobox attributes (`role="combobox"`, `aria-autocomplete="list"`, `aria-expanded`); pass `listboxId` to set `aria-controls` for your rendered listbox. React: `useLingoInput(opts)` from `@pascal-app/lingo/react`. +`field.value` · `.quantity` · `.result` · `.state` ('idle'|'incomplete'|'valid'|'invalid') · `.set()` · `.commit()` · `.update()` · `.destroy()`. Never rewrites while typing; canonicalizes on blur/Enter/submit. With `complete`/`onComplete`, the input gets the headless WAI-ARIA combobox attributes (`role="combobox"`, `aria-autocomplete="list"`, `aria-expanded`); pass `listboxId` to set `aria-controls` for your rendered listbox. + +## React (`@pascal-app/lingo/react`) + +```tsx +import { completions } from "@pascal-app/lingo/complete" +import { useLingoInput } from "@pascal-app/lingo/react" + +function HeightField() { + const field = useLingoInput({ + kind: "length", + unit: "m", + name: "height_m", + listboxId: "height-options", + complete: (text) => completions(text, { kind: "length" }), + }) + return ( + <> + +
    + {field.completions.map((item, index) => ( +
  • { + e.preventDefault() + field.selectCompletion(index) + }} + > + {item.text} +
  • + ))} +
+ + ) +} +``` + +- Attach `field.ref` to `` / `