@lotics/cli 0.191.0 → 0.191.1

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/dist/src/cli.js CHANGED
@@ -39864,15 +39864,15 @@ var require_deflate = __commonJS({
39864
39864
  if (!strm) {
39865
39865
  return Z_STREAM_ERROR;
39866
39866
  }
39867
- var wrap = 1;
39867
+ var wrap2 = 1;
39868
39868
  if (level === Z_DEFAULT_COMPRESSION) {
39869
39869
  level = 6;
39870
39870
  }
39871
39871
  if (windowBits < 0) {
39872
- wrap = 0;
39872
+ wrap2 = 0;
39873
39873
  windowBits = -windowBits;
39874
39874
  } else if (windowBits > 15) {
39875
- wrap = 2;
39875
+ wrap2 = 2;
39876
39876
  windowBits -= 16;
39877
39877
  }
39878
39878
  if (memLevel < 1 || memLevel > MAX_MEM_LEVEL || method !== Z_DEFLATED || windowBits < 8 || windowBits > 15 || level < 0 || level > 9 || strategy < 0 || strategy > Z_FIXED) {
@@ -39884,7 +39884,7 @@ var require_deflate = __commonJS({
39884
39884
  var s = new DeflateState();
39885
39885
  strm.state = s;
39886
39886
  s.strm = strm;
39887
- s.wrap = wrap;
39887
+ s.wrap = wrap2;
39888
39888
  s.gzhead = null;
39889
39889
  s.w_bits = windowBits;
39890
39890
  s.w_size = 1 << s.w_bits;
@@ -40177,7 +40177,7 @@ var require_deflate = __commonJS({
40177
40177
  var dictLength = dictionary.length;
40178
40178
  var s;
40179
40179
  var str, n;
40180
- var wrap;
40180
+ var wrap2;
40181
40181
  var avail;
40182
40182
  var next;
40183
40183
  var input;
@@ -40186,16 +40186,16 @@ var require_deflate = __commonJS({
40186
40186
  return Z_STREAM_ERROR;
40187
40187
  }
40188
40188
  s = strm.state;
40189
- wrap = s.wrap;
40190
- if (wrap === 2 || wrap === 1 && s.status !== INIT_STATE || s.lookahead) {
40189
+ wrap2 = s.wrap;
40190
+ if (wrap2 === 2 || wrap2 === 1 && s.status !== INIT_STATE || s.lookahead) {
40191
40191
  return Z_STREAM_ERROR;
40192
40192
  }
40193
- if (wrap === 1) {
40193
+ if (wrap2 === 1) {
40194
40194
  strm.adler = adler32(strm.adler, dictionary, dictLength, 0);
40195
40195
  }
40196
40196
  s.wrap = 0;
40197
40197
  if (dictLength >= s.w_size) {
40198
- if (wrap === 0) {
40198
+ if (wrap2 === 0) {
40199
40199
  zero(s.head);
40200
40200
  s.strstart = 0;
40201
40201
  s.block_start = 0;
@@ -40235,7 +40235,7 @@ var require_deflate = __commonJS({
40235
40235
  strm.next_in = next;
40236
40236
  strm.input = input;
40237
40237
  strm.avail_in = avail;
40238
- s.wrap = wrap;
40238
+ s.wrap = wrap2;
40239
40239
  return Z_OK;
40240
40240
  }
40241
40241
  exports2.deflateInit = deflateInit;
@@ -41255,17 +41255,17 @@ var require_inflate = __commonJS({
41255
41255
  return inflateResetKeep(strm);
41256
41256
  }
41257
41257
  function inflateReset2(strm, windowBits) {
41258
- var wrap;
41258
+ var wrap2;
41259
41259
  var state;
41260
41260
  if (!strm || !strm.state) {
41261
41261
  return Z_STREAM_ERROR;
41262
41262
  }
41263
41263
  state = strm.state;
41264
41264
  if (windowBits < 0) {
41265
- wrap = 0;
41265
+ wrap2 = 0;
41266
41266
  windowBits = -windowBits;
41267
41267
  } else {
41268
- wrap = (windowBits >> 4) + 1;
41268
+ wrap2 = (windowBits >> 4) + 1;
41269
41269
  if (windowBits < 48) {
41270
41270
  windowBits &= 15;
41271
41271
  }
@@ -41276,7 +41276,7 @@ var require_inflate = __commonJS({
41276
41276
  if (state.window !== null && state.wbits !== windowBits) {
41277
41277
  state.window = null;
41278
41278
  }
41279
- state.wrap = wrap;
41279
+ state.wrap = wrap2;
41280
41280
  state.wbits = windowBits;
41281
41281
  return inflateReset(strm);
41282
41282
  }
@@ -46001,7 +46001,7 @@ function resultSideEffects(result) {
46001
46001
  }
46002
46002
 
46003
46003
  // src/version.ts
46004
- var VERSION = "0.191.0";
46004
+ var VERSION = "0.191.1";
46005
46005
 
46006
46006
  // src/timezone.ts
46007
46007
  function machineTimezone() {
@@ -46110,6 +46110,167 @@ function matchDocArea(areas, query) {
46110
46110
  if (exact.length > 0) return exact;
46111
46111
  return areas.filter((a) => a.area.toLowerCase().includes(q));
46112
46112
  }
46113
+ var PAGE_BYTES = 16e3;
46114
+ var NAV_BYTES = PAGE_BYTES / 2;
46115
+ var CUT_MARK = "\n[cut \u2014 the whole document is at the path above]";
46116
+ var slugOf = (title) => title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
46117
+ function docSections(text) {
46118
+ const lines = text.split("\n");
46119
+ const found = [];
46120
+ let fenced = false;
46121
+ lines.forEach((line, index) => {
46122
+ if (line.startsWith("```")) {
46123
+ fenced = !fenced;
46124
+ return;
46125
+ }
46126
+ if (fenced) return;
46127
+ const m = /^(#{1,6})\s+(.+?)\s*$/.exec(line);
46128
+ if (m === null) return;
46129
+ const level = m[1].length;
46130
+ for (const open of found) {
46131
+ if (open.end === lines.length && open.level >= level) open.end = index;
46132
+ }
46133
+ found.push({ level, title: m[2], slug: slugOf(m[2]), line: index, end: lines.length });
46134
+ });
46135
+ return found;
46136
+ }
46137
+ function docRows(text) {
46138
+ const found = [];
46139
+ let fenced = false;
46140
+ let header = true;
46141
+ text.split("\n").forEach((line, index) => {
46142
+ if (line.startsWith("```")) {
46143
+ fenced = !fenced;
46144
+ return;
46145
+ }
46146
+ if (fenced) return;
46147
+ if (!line.startsWith("|")) {
46148
+ header = true;
46149
+ return;
46150
+ }
46151
+ const cells = line.split("|");
46152
+ if (cells.length < 3) return;
46153
+ const first3 = cells[1].trim();
46154
+ if (/^:?-{2,}:?$/.test(first3)) return;
46155
+ if (header) {
46156
+ header = false;
46157
+ return;
46158
+ }
46159
+ const key = first3.replace(/[`*[\]]/g, "").trim();
46160
+ if (key === "") return;
46161
+ found.push({ key, slug: slugOf(key), line: index });
46162
+ });
46163
+ return found;
46164
+ }
46165
+ var byteLength = (s) => Buffer.byteLength(s, "utf-8");
46166
+ var oneNewline = (s) => `${s.replace(/\n+$/, "")}
46167
+ `;
46168
+ var wrap = (names, width = 92) => names.reduce((out, name2) => {
46169
+ const last3 = out[out.length - 1];
46170
+ if (last3 !== void 0 && last3.length + name2.length + 1 <= width) {
46171
+ out[out.length - 1] = `${last3} ${name2}`;
46172
+ return out;
46173
+ }
46174
+ return [...out, ` ${name2}`];
46175
+ }, []);
46176
+ var fit = (lines, budget) => {
46177
+ const kept = [];
46178
+ let used = 0;
46179
+ for (const line of lines) {
46180
+ const cost = byteLength(line) + 1;
46181
+ if (used + cost > budget) break;
46182
+ kept.push(line);
46183
+ used += cost;
46184
+ }
46185
+ return { kept, dropped: lines.length - kept.length };
46186
+ };
46187
+ var childrenOf = (sections, parent) => {
46188
+ const within = parent === void 0 ? sections : sections.filter((s) => s.line > parent.line && s.line < parent.end);
46189
+ if (within.length === 0) return [];
46190
+ const top = Math.min(...within.map((s) => s.level));
46191
+ return within.filter((s) => s.level === top);
46192
+ };
46193
+ var linesOf = (lines, section) => section === void 0 ? lines : lines.slice(section.line, section.end);
46194
+ var tableTop = (lines, rowLine) => {
46195
+ for (let i2 = rowLine; i2 > 0; i2--) {
46196
+ if (!lines[i2].startsWith("|")) return i2 + 1;
46197
+ }
46198
+ return 0;
46199
+ };
46200
+ var tableHeader = (lines, rowLine) => {
46201
+ for (let i2 = rowLine; i2 > 0; i2--) {
46202
+ if (/^\|\s*:?-{2,}/.test(lines[i2])) return i2 > 0 ? [lines[i2 - 1], lines[i2]] : [];
46203
+ if (!lines[i2].startsWith("|")) return [];
46204
+ }
46205
+ return [];
46206
+ };
46207
+ function renderDocPage(text, args) {
46208
+ const lines = text.split("\n");
46209
+ const sections = docSections(text);
46210
+ const rows = docRows(text);
46211
+ const address = (slug) => ` lotics docs ${args.address}/${slug}`;
46212
+ const navFor = (parent2) => {
46213
+ const children2 = childrenOf(sections, parent2);
46214
+ if (children2.length > 0) {
46215
+ return children2.map(
46216
+ (s) => `${address(s.slug)} (${byteLength(linesOf(lines, s).join("\n"))} bytes)`
46217
+ );
46218
+ }
46219
+ const within = parent2 === void 0 ? rows : rows.filter((r) => r.line > parent2.line && r.line < parent2.end);
46220
+ if (within.length === 0) return [];
46221
+ return [` lotics docs ${args.address}/<name>, where <name> is one of:`, "", ...wrap(within.map((r) => r.slug))];
46222
+ };
46223
+ const titled = sections.length > 0 && sections[0].level === 1 ? sections[0] : void 0;
46224
+ const listed = (nav2) => {
46225
+ const shown = fit(nav2, NAV_BYTES);
46226
+ return [...shown.kept, ...shown.dropped > 0 ? [` \u2026 and ${shown.dropped} more`] : []].join(
46227
+ "\n"
46228
+ );
46229
+ };
46230
+ let current;
46231
+ if (args.section !== void 0 && args.section !== "") {
46232
+ const q = slugOf(args.section);
46233
+ const pick4 = (all) => {
46234
+ const exact = all.filter((s) => s.slug === q);
46235
+ return exact.length > 0 ? exact : all.filter((s) => s.slug.startsWith(q));
46236
+ };
46237
+ const hitSections = pick4(sections.filter((s) => s.level > 1));
46238
+ const hitRows = hitSections.length > 0 ? [] : pick4(rows);
46239
+ if (hitSections.length + hitRows.length !== 1) {
46240
+ const ambiguous = [...hitSections.map((s) => s.slug), ...hitRows.map((r) => r.slug)];
46241
+ const list2 = listed(ambiguous.length > 1 ? ambiguous.map(address) : navFor(titled));
46242
+ return {
46243
+ error: ambiguous.length > 1 ? `"${args.section}" names more than one part of ${args.address}:
46244
+ ${list2}` : `No "${args.section}" in ${args.address}. It holds:
46245
+ ${list2}`
46246
+ };
46247
+ }
46248
+ const row = hitRows[0];
46249
+ if (row !== void 0) {
46250
+ return { text: oneNewline([...tableHeader(lines, row.line), lines[row.line]].join("\n")) };
46251
+ }
46252
+ current = hitSections[0];
46253
+ }
46254
+ const whole = linesOf(lines, current).join("\n");
46255
+ if (byteLength(whole) <= PAGE_BYTES) return { text: oneNewline(whole) };
46256
+ const parent = current ?? titled;
46257
+ const nav = navFor(parent);
46258
+ const navText = nav.length === 0 ? "" : `
46259
+
46260
+ This is ${byteLength(whole)} bytes; one page is ${PAGE_BYTES}. It holds:
46261
+
46262
+ ${listed(nav)}`;
46263
+ const children = childrenOf(sections, parent);
46264
+ const rowsWithin = parent === void 0 ? rows : rows.filter((r) => r.line > parent.line && r.line < parent.end);
46265
+ const openingTo = children.length > 0 ? children[0].line : rowsWithin.length > 0 ? tableTop(lines, rowsWithin[0].line) : current?.end ?? lines.length;
46266
+ const head = fit(
46267
+ lines.slice(current?.line ?? 0, openingTo),
46268
+ PAGE_BYTES - byteLength(navText) - byteLength(CUT_MARK) - 1
46269
+ );
46270
+ return {
46271
+ text: oneNewline(`${head.kept.join("\n")}${head.dropped > 0 ? CUT_MARK : ""}${navText}`)
46272
+ };
46273
+ }
46113
46274
  function onlySelfReference(areas) {
46114
46275
  return areas.length > 0 && areas.every((a) => a.pkg === "@lotics/cli");
46115
46276
  }
@@ -46129,6 +46290,7 @@ ${a.pkg} ${a.version}`);
46129
46290
  lines.push(` ${name2.length >= 24 ? `${name2} ` : name2.padEnd(24)}${a.title}`);
46130
46291
  }
46131
46292
  lines.push("\n lotics docs <area> print one, e.g. `lotics docs ai`");
46293
+ lines.push(" lotics docs <area>/<section> one section \u2014 a long doc prints its section list");
46132
46294
  lines.push(" lotics docs <pkg>/<area> when two packages share a name");
46133
46295
  lines.push("\n Versions are the INSTALLED ones; a doc describes the version beside it.");
46134
46296
  if (onlySelfReference(areas)) {
@@ -46145,7 +46307,17 @@ function docsCommand(args) {
46145
46307
  console.log(renderDocIndex(areas));
46146
46308
  return;
46147
46309
  }
46148
- const hits = matchDocArea(areas, args.area);
46310
+ const asked = args.area.trim();
46311
+ let hits = matchDocArea(areas, asked);
46312
+ let section;
46313
+ if (hits.length === 0 && asked.includes("/")) {
46314
+ const cut = asked.lastIndexOf("/");
46315
+ const head = matchDocArea(areas, asked.slice(0, cut));
46316
+ if (head.length > 0) {
46317
+ hits = head;
46318
+ section = asked.slice(cut + 1);
46319
+ }
46320
+ }
46149
46321
  if (hits.length === 0) {
46150
46322
  console.error(
46151
46323
  `No reference area matching "${args.area}".
@@ -46156,14 +46328,22 @@ function docsCommand(args) {
46156
46328
  if (hits.length > 1) {
46157
46329
  console.error(
46158
46330
  `"${args.area}" matches more than one reference \u2014 name the package:
46159
- ` + hits.map((a) => ` \u2022 lotics docs ${qualified(a)} (${a.pkg} ${a.version})`).join("\n")
46331
+ ` + hits.map(
46332
+ (a) => ` \u2022 lotics docs ${qualified(a)}${section === void 0 ? "" : `/${section}`} (${a.pkg} ${a.version})`
46333
+ ).join("\n")
46160
46334
  );
46161
46335
  process.exit(1);
46162
46336
  }
46163
46337
  const hit = hits[0];
46338
+ const address = areas.filter((a) => a.area === hit.area).length > 1 ? qualified(hit) : hit.area;
46339
+ const page = renderDocPage(fs5.readFileSync(hit.absPath, "utf-8"), { address, section });
46340
+ if ("error" in page) {
46341
+ console.error(page.error);
46342
+ process.exit(1);
46343
+ }
46164
46344
  console.error(`${hit.pkg} ${hit.version} \xB7 ${path5.relative(projectDir, hit.absPath)}
46165
46345
  `);
46166
- process.stdout.write(fs5.readFileSync(hit.absPath, "utf-8"));
46346
+ process.stdout.write(page.text);
46167
46347
  }
46168
46348
 
46169
46349
  // src/commands.ts
@@ -46295,7 +46475,8 @@ var COMMANDS = [
46295
46475
  verbs: ["docs"],
46296
46476
  help: [
46297
46477
  " lotics docs List the reference docs the npm packages in node_modules ship",
46298
- " lotics docs <area> Print one (e.g. 'lotics docs ai')"
46478
+ " lotics docs <area> Print one, capped at a page (e.g. 'lotics docs ai')",
46479
+ " lotics docs <area>/<section> One section, or one row of a table doc"
46299
46480
  ]
46300
46481
  },
46301
46482
  {
@@ -46639,7 +46820,6 @@ import { createHash as createHash3 } from "node:crypto";
46639
46820
  import { tmpdir as tmpdir2 } from "node:os";
46640
46821
 
46641
46822
  // src/starter_template.ts
46642
- var STARTER_REACT_NATIVE_VERSION = "0.85.3";
46643
46823
  var VITEST_SETUP_FILENAME = "vitest.setup.ts";
46644
46824
  var VITEST_SETUP_CONTENT = `// Runs before every test file (wired by vite.config.ts \`test.setupFiles\`).
46645
46825
  // Empty by default \u2014 put global test setup here: custom matchers, a fetch stub,
@@ -46675,7 +46855,7 @@ function buildStarterTemplate(args) {
46675
46855
  // or a broken vitest config, indefinitely. A detector that cannot
46676
46856
  // fire when the thing is ABSENT rather than excessive is not one.
46677
46857
  //
46678
- // What the scaffold seeds instead is `src/harness.test.ts`, which
46858
+ // What the scaffold seeds instead is `src/harness.test.tsx`, which
46679
46859
  // asserts the HARNESS and nothing else. The file it replaces
46680
46860
  // (`src/App.test.tsx`) asserted the scaffold's placeholder copy
46681
46861
  // ("First item"), so it failed the moment the author did the very
@@ -46693,15 +46873,14 @@ function buildStarterTemplate(args) {
46693
46873
  // that IMPORTS @lotics/docx or @lotics/xlsx directly (its own doc/excel
46694
46874
  // builders) declares its own dependency then.
46695
46875
  "@lotics/ui": uiVersion,
46696
- "@react-native-picker/picker": "^2.7.0",
46697
- "expo-image": "~3.0.9",
46876
+ // @base-ui/react is deliberately absent: @lotics/ui declares it as an
46877
+ // EXACT peer, npm installs a peer, and a second declaration here is a
46878
+ // version that drifts from the kit's — which Base UI punishes
46879
+ // silently, since it passes state through React context and a nested
46880
+ // copy renders parts that never open.
46698
46881
  "lucide-react": "^0.562.0",
46699
- "lucide-react-native": "^0.562.0",
46700
46882
  react: "^19.0.0",
46701
46883
  "react-dom": "^19.0.0",
46702
- "react-native": STARTER_REACT_NATIVE_VERSION,
46703
- "react-native-svg": "^15.0.0",
46704
- "react-native-web": "^0.21.0",
46705
46884
  // In-app routing for the scaffolded list→detail example. The app owns
46706
46885
  // its routing; the SDK's `isEmbedded()` picks memory vs browser history. `react-router` is the
46707
46886
  // canonical package (react-router-dom is a shim over it).
@@ -46750,9 +46929,10 @@ function buildStarterTemplate(args) {
46750
46929
  strict: true,
46751
46930
  noFallthroughCasesInSwitch: true
46752
46931
  },
46753
- // Restrict tsc to the project's own sources. tsgo follows
46754
- // transitive imports into node_modules strict rules
46755
- // (noUnusedLocals etc.) would fire on @lotics/ui's source.
46932
+ // Restrict tsc to the project's own sources a `noEmit` program has
46933
+ // no reason to walk anything else, and under the `LOTICS_UI_SRC`
46934
+ // dev-link `@lotics/ui` resolves to a working copy of the kit's own
46935
+ // .tsx, which strict rules (noUnusedLocals etc.) would then fire on.
46756
46936
  // `.lotics/**/*` (not bare `.lotics`): TypeScript's include glob walk
46757
46937
  // SKIPS dot-directories, so a bare `.lotics` silently loads zero of the
46758
46938
  // generated `.d.ts` augmentations and typed useWorkflow/useQuery/useAgentRun
@@ -46780,87 +46960,25 @@ function buildStarterTemplate(args) {
46780
46960
  path: "vite.config.ts",
46781
46961
  content: `/// <reference types="vitest" />
46782
46962
  import { defineConfig } from "vite";
46783
- import type { Plugin } from "vite";
46784
46963
  import react from "@vitejs/plugin-react";
46785
- import { loticsOptimizeDeps, loticsOptimizeExclude, loticsResolve } from "@lotics/ui/vite";
46786
-
46787
- // @lotics/ui/icon.tsx deep-imports \`lucide-react-native/dist/esm/icons/<name>\`.
46788
- // lucide-react-native's \`exports\` map lists only "." and "./icons", so a strict
46789
- // resolver (the sandbox's vite dev optimizer) blocks the deep path with
46790
- // \`Missing "./dist/esm/icons/<name>" specifier\`. lucide-react ships the same
46791
- // per-icon files with NO exports map, so redirect there. A \`resolveId\` PLUGIN
46792
- // (not \`resolve.alias\`) is required: the dep-optimizer SCAN runs plugin
46793
- // resolveId hooks but does NOT apply regex \`resolve.alias\`, so the alias alone
46794
- // fixes the production build but leaves the dev preview broken.
46795
- function lucideIconsWebAlias(): Plugin {
46796
- return {
46797
- name: "lucide-icons-web-alias",
46798
- enforce: "pre",
46799
- async resolveId(source, importer, options) {
46800
- const m = /^lucide-react-native\\/dist\\/esm\\/icons\\/(.+)$/.exec(source);
46801
- if (!m) return null;
46802
- return this.resolve(\`lucide-react/dist/esm/icons/\${m[1]}\`, importer, {
46803
- ...options,
46804
- skipSelf: true,
46805
- });
46806
- },
46807
- };
46808
- }
46964
+ import { loticsResolve } from "@lotics/ui/vite";
46809
46965
 
46810
46966
  // Vite default base (/) emits absolute asset URLs in index.html. Lotics's
46811
46967
  // render endpoint rewrites those to /v1/apps/{id}/asset/... so the bundle
46812
46968
  // loads via the platform's asset proxy. Don't change \`base\` unless you
46813
46969
  // also adjust the rewrite logic in backend/api/apps.ts:rewriteAssetPaths.
46814
46970
  export default defineConfig({
46815
- plugins: [lucideIconsWebAlias(), react()],
46816
- // RN libraries reference globals Metro injects but Vite does not \u2014 undefined
46817
- // \u21D2 the bundle throws. \`__DEV__\` (react-native-web, @react-native-picker's
46818
- // UnimplementedView): throws at load, blank iframe. \`global\` (rn-web
46819
- // Animated's \`global.cancelAnimationFrame\`): throws when a spring animation is
46820
- // interrupted/torn down \u2014 e.g. toggling @lotics/ui's Switch. rollup hits it;
46821
- // the dev esbuild path masks it, so it only surfaces in the deployed app.
46822
- // rn-web only reads \`global.x\` as a free var, so mapping it to \`globalThis\`
46823
- // is safe. Define both so dev and the deployed build behave identically.
46824
- define: { __DEV__: "false", global: "globalThis" },
46825
- // The kit owns its own resolution \u2014 the react-native \u2192 react-native-web alias,
46826
- // \`.web.tsx\`-first extensions, the React dedupe, and the \`LOTICS_UI_SRC\`
46827
- // dev-link \u2014 for the same reason it owns \`loticsOptimizeDeps\`: every entry is
46828
- // dictated by @lotics/ui's internals, so shipping it WITH the kit means it can
46829
- // never drift from the version installed here. It also stops being an app's job
46830
- // to hand-carry a load-bearing alias that a regex edit twice deleted (GAP-133/142).
46971
+ plugins: [react()],
46972
+ // The kit owns this block: the \`LOTICS_UI_SRC\` dev-link and the React dedupe
46973
+ // are decided by @lotics/ui's own delivery, so shipping it WITH the kit means
46974
+ // it can never drift from the version installed here \u2014 and it stops being an
46975
+ // app's job to hand-carry a line a regex edit twice deleted (GAP-133/142).
46831
46976
  // \`lotics app codegen\` writes the matching \`paths\` into
46832
- // .lotics/tsconfig.link.json, so tsc/vitest/eslint/your editor resolve the same
46833
- // @lotics/ui this does. To add your own alias, spread it:
46977
+ // .lotics/tsconfig.link.json, so tsc/vitest/eslint/your editor resolve the
46978
+ // same @lotics/ui this does. To add your own alias, spread it:
46834
46979
  // const base = loticsResolve();
46835
46980
  // resolve: { ...base, alias: [...base.alias, { find: "x", replacement: "y" }] }
46836
46981
  resolve: loticsResolve(),
46837
- optimizeDeps: {
46838
- // The dev dep-optimizer must pre-bundle @lotics/ui's RN-ecosystem + markdown
46839
- // CJS-interop deps or \`lotics app dev\` blanks the iframe ("does not provide an
46840
- // export named 'default'/'parse'"). Dev-ONLY: the prod rollup build resolves the
46841
- // interop without it, so typecheck/lint/build stay green while the dev iframe
46842
- // goes blank. The canonical list ships WITH @lotics/ui as \`loticsOptimizeDeps\`
46843
- // (imported above) so it can never drift from what the kit's transitive deps
46844
- // require across a major bump \u2014 the per-family rationale lives in that module.
46845
- // To add app-specific entries, spread: include: [...loticsOptimizeDeps, "my-dep"].
46846
- include: loticsOptimizeDeps,
46847
- // The kit is ONE module graph, so it must never be pre-bundled: the optimizer
46848
- // auto-discovers each @lotics/ui subpath and inlines a private copy of every
46849
- // context-owning module it reaches (locale, tooltip, portal, popover), leaving
46850
- // the provider filling one context and the component reading another.
46851
- exclude: loticsOptimizeExclude,
46852
- // The dep optimizer pre-bundles deps with a SEPARATE esbuild pass that
46853
- // top-level \`define\` doesn't always reach, so a pre-bundled RN dep can
46854
- // still hit \`__DEV__ is not defined\` under \`lotics app dev\`. Define it
46855
- // here too \u2014 belt-and-suspenders with the top-level \`define\` above.
46856
- // The optimizer pre-bundles with a SEPARATE esbuild resolver that ignores
46857
- // \`resolve.extensions\`, so \`.web.js\` must be repeated here or a pre-bundled
46858
- // RN dep still resolves \`./Picker\` to the native build.
46859
- esbuildOptions: {
46860
- define: { __DEV__: "false", global: "globalThis" },
46861
- resolveExtensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".json", ".mjs", ".mts"],
46862
- },
46863
- },
46864
46982
  build: {
46865
46983
  outDir: "dist",
46866
46984
  // No source maps in the production build: they ship the original source
@@ -46886,11 +47004,6 @@ export default defineConfig({
46886
47004
  // cross-site WS hijack fix) \u2014 without this entry the proxied preview page
46887
47005
  // loads over HTTP but HMR never connects, so edits stop live-updating.
46888
47006
  allowedHosts: [".lotics-sandbox.app"],
46889
- // @lotics/ui/fonts.css references the API's /iframe/fonts/*.woff2 files
46890
- // by root-relative URL. A deployed app is served from the API origin so
46891
- // they resolve directly; under \`lotics app dev\` the app runs on
46892
- // localhost, so proxy /iframe to the API to load the real fonts.
46893
- proxy: { "/iframe": { target: "https://api.lotics.ai", changeOrigin: true } },
46894
47007
  },
46895
47008
  test: {
46896
47009
  environment: "jsdom",
@@ -46899,39 +47012,31 @@ export default defineConfig({
46899
47012
  // EXIST either way: setupFiles naming a missing file makes vitest fail to
46900
47013
  // load every test. See vitest.setup.ts.
46901
47014
  setupFiles: ["./${VITEST_SETUP_FILENAME}"],
46902
- // RN packages ship Flow (\`import typeof\`) in their native source, reached
46903
- // transitively by RN-Web components (pickers, calendars, anything touching
46904
- // Animated). Vitest's web optimizer is OFF by default and ignores the
46905
- // \`optimizeDeps.resolveExtensions\` above, so it pre-bundles the native
46906
- // \`.js\` / \`src\` Flow files and esbuild can't parse them. Enable it with the
46907
- // same \`.web.js\`-first resolution so it bundles the compiled web variants
46908
- // (Picker.web.js, RN-Web dist) \u2014 without this, any test that renders a tree
46909
- // pulling a Picker / DatePicker fails with "Unexpected token 'typeof'".
47015
+ // @lotics/ui ships built ESM whose every component module carries the
47016
+ // literal \`import "./button.css"\` that delivers its sheet. Vitest hands
47017
+ // node_modules to Node's ESM loader, which has no loader for \`.css\` \u2014 so
47018
+ // without this line EVERY suite that touches a kit component fails to load
47019
+ // with \`Unknown file extension ".css"\`, before a single assertion runs.
47020
+ // Inlining routes those modules through Vite's transform instead, where a
47021
+ // CSS import is inert under jsdom. Any package whose modules import CSS
47022
+ // needs the same entry.
47023
+ server: { deps: { inline: [/@lotics\\/ui/] } },
46910
47024
  deps: {
46911
47025
  optimizer: {
46912
47026
  web: {
46913
47027
  enabled: true,
46914
- // react + react-dom + the @testing-library renderer must ride in the
46915
- // SAME optimized chunk as react-native-web, so a rendered tree and the
46916
- // renderer share ONE react instance. Without this, a test that renders a
46917
- // non-trivial App tree hits a null hooks dispatcher ("Cannot read
46918
- // properties of null (reading 'useState')") the moment it mounts.
47028
+ // Everything here ships COMPILED JS, so vitest pre-bundles each into
47029
+ // its own chunk \u2014 and a chunk carrying its own react import splits the
47030
+ // instance and nulls the hooks dispatcher the moment a rendered tree
47031
+ // calls one ("Cannot read properties of null (reading 'useState')").
47032
+ // They ride in ONE optimized chunk with react and the renderer instead.
46919
47033
  include: [
46920
47034
  "react",
46921
47035
  "react-dom",
46922
47036
  "react-dom/client",
46923
47037
  "react-dom/test-utils",
46924
- "react-native",
46925
- "react-native-web",
46926
- "react-native-svg",
46927
- "@react-native-picker/picker",
46928
47038
  "@testing-library/react",
46929
47039
  "@testing-library/dom",
46930
- // @lotics/app-sdk ships compiled dist/ JS (unlike @lotics/ui, which
46931
- // ships source vitest transforms inline) \u2014 pre-bundled into its own
46932
- // chunk, its react import splits from the shared instance and nulls
46933
- // the hooks dispatcher the instant a hook (useAgentRun / useQuery)
46934
- // runs in a rendered App tree.
46935
47040
  "@lotics/app-sdk",
46936
47041
  // The /router subpath is a SEPARATE optimizer entry \u2014 without it,
46937
47042
  // AppRouter's BrowserRouter bundles apart from the app's
@@ -46943,9 +47048,6 @@ export default defineConfig({
46943
47048
  // ("Invalid hook call") when a test renders the routed App tree.
46944
47049
  "react-router",
46945
47050
  ],
46946
- esbuildOptions: {
46947
- resolveExtensions: [".web.tsx", ".web.ts", ".web.js", ".tsx", ".ts", ".jsx", ".js", ".json", ".mjs", ".mts"],
46948
- },
46949
47051
  },
46950
47052
  },
46951
47053
  },
@@ -46969,8 +47071,8 @@ export default defineConfig({
46969
47071
  * default of html/body being content-sized collapses every \`flex: 1\`
46970
47072
  * to 0 and the surface shrinks to fit only its rendered content.
46971
47073
  *
46972
- * Also: #root is made a flex column so the outermost RN \`<View flex:1>\`
46973
- * inside App.tsx claims the full iframe height.
47074
+ * Also: #root is a flex column, so App.tsx's outermost \`flex: 1\` box
47075
+ * claims the full iframe height.
46974
47076
  */
46975
47077
  html, body, #root { height: 100%; margin: 0; }
46976
47078
  #root { display: flex; flex-direction: column; }
@@ -46985,45 +47087,47 @@ export default defineConfig({
46985
47087
  },
46986
47088
  {
46987
47089
  path: "src/main.tsx",
46988
- content: `import "@lotics/ui/index.css";
47090
+ content: `import "@lotics/ui/styles.css";
46989
47091
  import "@lotics/ui/fonts.css";
46990
- import { PortalHost } from "@lotics/ui/portal";
46991
47092
  import { LoticsLocaleProvider, vi } from "@lotics/ui/locale";
46992
47093
  import { mount } from "@lotics/app-sdk";
46993
47094
  import App from "./App";
46994
47095
 
47096
+ // \`styles.css\` is not optional. It carries the cascade-layer order, every design
47097
+ // token and the reset, and the kit's chrome reads those variables with NO inline
47098
+ // fallback \u2014 without it every colour, size and radius is invalid and the screen
47099
+ // renders unstyled but working.
47100
+ //
46995
47101
  // THIS APP LOOKS LIKE LOTICS unless you say otherwise, which is the right
46996
47102
  // default: the kit's own palette and typeface are what every screen in the
46997
47103
  // product wears, and an app that sets nothing belongs with them.
46998
47104
  //
46999
- // If this app carries someone else's brand \u2014 a customer's typeface, their one
47000
- // colour \u2014 \`applyLoticsTheme\` is the escape hatch. Call it ONCE, here, before
47001
- // \`mount\`, so the variables are in place for the first paint:
47105
+ // If this app carries someone else's brand, a theme is a STYLESHEET \u2014 there is
47106
+ // no theme function. Declare the shadcn/ui variables in your own CSS, imported
47107
+ // after \`styles.css\`; an unlayered rule of yours beats every layered kit rule at
47108
+ // any specificity, with no \`!important\`:
47002
47109
  //
47003
- // import { applyLoticsTheme } from "@lotics/ui/theme";
47004
- // applyLoticsTheme({ primary: "#0F766E" });
47110
+ // :root, .light { --primary: #0F766E; --primary-foreground: #fff; --ring: #0F766E; }
47111
+ // .dark { --primary: #5EEAD4; --primary-foreground: #042F2E; --ring: #5EEAD4; }
47005
47112
  //
47006
- // It is ADDITIVE \u2014 set what you own and every other role keeps the kit's value,
47007
- // rather than resetting to some second set of defaults:
47113
+ // Paste BOTH blocks of whatever palette you start from: a token set only on
47114
+ // \`:root\` keeps its light value in dark mode, because your unlayered rule also
47115
+ // beats the kit's \`.dark\`. The roles worth knowing:
47008
47116
  //
47009
- // accent which of these \u2014 a selected row, an active filter
47010
- // primary this is on \u2014 the filled button, a checkbox, an active tab's rail
47011
- // background the raised surface (Card, Drawer, Modal); leave white unless you
47012
- // deliberately want a toned surface
47013
- // border hairlines \u2014 a register is mostly these, so a small change here is
47014
- // felt against the whole screen
47015
- // bodyFont the typeface for everything (a CSS stack you know RESOLVES, and
47016
- // that carries Vietnamese diacritics \u2014 a face without them turns
47017
- // "Th\u1EA9m \u0111\u1ECBnh" into tofu). Declare its @font-face in your own CSS.
47018
- // displayFont display type only; defaults to bodyFont
47117
+ // --primary this is on \u2014 the filled button, a checkbox, an active tab's rail
47118
+ // --lotics-accent which of these \u2014 a selected row, an active filter (themed: set
47119
+ // it in both blocks, beside --primary)
47120
+ // --card the raised surface (Card, Drawer, Dialog)
47121
+ // --border hairlines \u2014 a register is mostly these, so a small change
47122
+ // here is felt against the whole screen
47123
+ // --font-sans the typeface for everything (a stack you know RESOLVES, and that
47124
+ // carries Vietnamese diacritics \u2014 a face without them turns
47125
+ // "Th\u1EA9m \u0111\u1ECBnh" into tofu). Declare its @font-face in your own CSS.
47126
+ // --lotics-font-display display type only; defaults to --font-sans
47019
47127
  //
47020
- // Prefer it over declaring \`--lotics-*\` in your own stylesheet: two of those
47021
- // values are DERIVED and fail quietly by hand \u2014 the selection wash is the accent
47022
- // at 7%, and one bodyFont has to reach all three weight rungs or medium and
47023
- // semibold stay in the kit's face and the screen reads as a font that half-loaded.
47024
- //
47025
- // PortalHost is the render target for @lotics/ui overlays (Popover, Tooltip,
47026
- // Dialog). Without it, Portal renders nothing \u2014 keep it wrapping the app.
47128
+ // Everything derived \u2014 the selection wash, hover and press shades, the hairline
47129
+ // ladder \u2014 is computed in CSS from those, so there is nothing to keep in step by
47130
+ // hand. The full contract is \`node_modules/@lotics/ui/docs/theming.md\`.
47027
47131
  //
47028
47132
  // LoticsLocaleProvider supplies the kit's own strings (Pagination, sort
47029
47133
  // headers, select-all, the Drawer's record nav\u2026) in one language \u2014 Vietnamese
@@ -47031,9 +47135,7 @@ import App from "./App";
47031
47135
  // LoticsLocale pack) for another language; remove it to fall back to English.
47032
47136
  mount(
47033
47137
  <LoticsLocaleProvider locale={vi}>
47034
- <PortalHost>
47035
- <App />
47036
- </PortalHost>
47138
+ <App />
47037
47139
  </LoticsLocaleProvider>,
47038
47140
  );
47039
47141
  `
@@ -47047,21 +47149,22 @@ mount(
47047
47149
  // host never sees it, so navigation never reloads the app); standalone,
47048
47150
  // real path URLs. Browser Back/Forward walk app screens in both.
47049
47151
  // - The full-container layout pattern is preserved in `Screen`: an outer
47050
- // <View flex:1> claims the iframe height (index.html sets
47051
- // html/body/#root to 100% + #root is a flex column). Keep that chain
47052
- // plain @lotics/ui/stack wraps children in an unstyled <View> that
47053
- // breaks `flex: 1` propagation to a fill-remaining-space child.
47152
+ // `flex: 1` box claims the iframe height (index.html sets
47153
+ // html/body/#root to 100% + #root is a flex column). It is a plain
47154
+ // `<div>` rather than a kit primitive because that chain has to stay
47155
+ // unbroken `Stack` is the right thing INSIDE it, where the gutter is
47156
+ // what you want, not around a fill-remaining-space child.
47054
47157
  // A single-screen app can delete the router and render one Screen directly.
47055
47158
  content: `import type { ReactNode } from "react";
47056
- import { View } from "react-native";
47057
47159
  import { useNavigate, useParams } from "react-router";
47058
47160
  import { AppRouter } from "@lotics/app-sdk/router";
47059
47161
  import { useAiContext } from "@lotics/app-sdk";
47060
47162
  import { Card } from "@lotics/ui/card";
47163
+ import { Stack } from "@lotics/ui/stack";
47061
47164
  import { Text } from "@lotics/ui/text";
47062
47165
  import { Button } from "@lotics/ui/button";
47063
47166
  import { BackButton } from "@lotics/ui/back_button";
47064
- import { EmptyState } from "@lotics/ui/empty_state";
47167
+ import { RegionState } from "@lotics/ui/region_state";
47065
47168
 
47066
47169
  // AppRouter makes the app's screens real URLs \u2014 write plain react-router
47067
47170
  // (useNavigate / useParams / <Link>) and it handles both modes. The app owns its
@@ -47078,14 +47181,16 @@ const ITEMS = [
47078
47181
  { id: "3", name: "Third item" },
47079
47182
  ];
47080
47183
 
47081
- // Outer <View flex:1> claims the full iframe height \u2014 works because index.html
47082
- // sets html/body/#root to 100% and #root is a flex column. Keep this flex chain
47083
- // plain (not @lotics/ui/stack) so a fill-remaining-space child can claim height.
47184
+ // The outer box claims the full iframe height \u2014 it works because index.html sets
47185
+ // html/body/#root to 100% and #root is a flex column. Keep this chain plain: a
47186
+ // box that says nothing about \`flex\` is what breaks it.
47084
47187
  function Screen({ children }: { children: ReactNode }) {
47085
47188
  return (
47086
- <View style={{ flex: 1, padding: 24, alignItems: "center" }}>
47087
- <View style={{ maxWidth: 640, width: "100%", gap: 16 }}>{children}</View>
47088
- </View>
47189
+ <div style={{ flex: 1, padding: 24, display: "flex", justifyContent: "center" }}>
47190
+ <Stack gap={16} style={{ maxWidth: 640, width: "100%" }}>
47191
+ {children}
47192
+ </Stack>
47193
+ </div>
47089
47194
  );
47090
47195
  }
47091
47196
 
@@ -47101,18 +47206,16 @@ function ListScreen() {
47101
47206
  </Text>
47102
47207
  {ITEMS.map((item) => (
47103
47208
  <Card key={item.id}>
47104
- <View
47105
- style={{
47106
- padding: 16,
47107
- flexDirection: "row",
47108
- alignItems: "center",
47109
- justifyContent: "space-between",
47110
- gap: 12,
47111
- }}
47209
+ <Stack
47210
+ direction="row"
47211
+ gap={8}
47212
+ align="center"
47213
+ justify="between"
47214
+ style={{ padding: 16 }}
47112
47215
  >
47113
47216
  <Text weight="medium">{item.name}</Text>
47114
47217
  <Button title="Open" color="primary" onPress={() => navigate("/item/" + item.id)} />
47115
- </View>
47218
+ </Stack>
47116
47219
  </Card>
47117
47220
  ))}
47118
47221
  </Screen>
@@ -47146,20 +47249,20 @@ function ItemDetailScreen() {
47146
47249
  \`<Button title="Back">\` with no \`color\` draws NOTHING at rest \u2014 no
47147
47250
  ground, no border, no underline \u2014 so its only affordance is a hover
47148
47251
  wash, which touch and keyboard never see. BackButton also carries its
47149
- own \`alignSelf\`, so it needs no wrapping View to sit left. */}
47252
+ own \`alignSelf\`, so it needs no wrapping box to sit left. */}
47150
47253
  <BackButton onPress={() => navigate(-1)} />
47151
47254
  <Text size="xl" weight="semibold">{item ? item.name : "Not found"}</Text>
47152
47255
  {/* Every state gets its own content. Swapping only the TITLE left the card
47153
47256
  below still describing the record the title had just said was missing. */}
47154
47257
  {item ? (
47155
47258
  <Card>
47156
- <View style={{ padding: 16, gap: 8 }}>
47259
+ <Stack gap={4} style={{ padding: 16 }}>
47157
47260
  <Text>Detail for item {id}.</Text>
47158
47261
  <Text color="muted">Reached via in-app navigation, not a host route.</Text>
47159
- </View>
47262
+ </Stack>
47160
47263
  </Card>
47161
47264
  ) : (
47162
- <EmptyState message={"No item with id " + id + "."} />
47265
+ <RegionState state="empty" message={"No item with id " + id + "."} />
47163
47266
  )}
47164
47267
  </Screen>
47165
47268
  );
@@ -47230,25 +47333,6 @@ alias the binding is keyed by).
47230
47333
  capabilities only. \`apps.workflows\` has exactly one author: \`set_app_workflow\`
47231
47334
  (which \`lotics app workflow set\` calls). The typed \`inputs\`/\`outputs\` schema for
47232
47335
  each alias lives in \`package.json#lotics.workflows.<alias>\`.
47233
- `
47234
- },
47235
- {
47236
- path: "src/lucide-react-native.d.ts",
47237
- content: `// Type shim for lucide-react-native's deep-icon imports
47238
- // (\`lucide-react-native/dist/esm/icons/<name>\`). The package ships JS-only
47239
- // files without per-file .d.ts. The lucideIconsWebAlias() plugin in
47240
- // vite.config.ts redirects these to lucide-react at resolve time; this
47241
- // declaration covers the TypeScript compile-time gap.
47242
- declare module "lucide-react-native/dist/esm/icons/*" {
47243
- import type { ComponentType } from "react";
47244
- const Icon: ComponentType<{
47245
- size: number;
47246
- color: string;
47247
- fill?: string;
47248
- strokeWidth?: number;
47249
- }>;
47250
- export default Icon;
47251
- }
47252
47336
  `
47253
47337
  },
47254
47338
  {
@@ -47258,13 +47342,16 @@ declare module "lucide-react-native/dist/esm/icons/*" {
47258
47342
  export default classes;
47259
47343
  }
47260
47344
 
47261
- // Plain side-effect CSS imports (@lotics/ui ships .tsx with import "./x.css").
47345
+ // Plain side-effect CSS imports \u2014 @lotics/ui/styles.css, and any sheet of
47346
+ // your own.
47262
47347
  declare module "*.css";
47263
47348
  `
47264
47349
  },
47265
47350
  {
47266
- path: "src/harness.test.ts",
47351
+ path: "src/harness.test.tsx",
47267
47352
  content: `import { describe, test, expect } from "vitest";
47353
+ import { render } from "@testing-library/react";
47354
+ import { Text } from "@lotics/ui/text";
47268
47355
 
47269
47356
  /**
47270
47357
  * The one test the scaffold seeds, and it is about the TEST SETUP rather than
@@ -47272,11 +47359,19 @@ declare module "*.css";
47272
47359
  * never has to be green over zero tests. It imports nothing of yours and
47273
47360
  * nothing generated, so it passes on a fresh clone before any codegen has run.
47274
47361
  *
47362
+ * It renders a KIT component deliberately. @lotics/ui ships built ESM whose
47363
+ * modules import their own \`.css\`, and a vitest that externalizes them dies
47364
+ * with \`Unknown file extension ".css"\` on every suite that touches one \u2014 a
47365
+ * whole-suite failure that a harness test importing only vitest would never
47366
+ * see. \`test.server.deps.inline\` in vite.config.ts is what prevents it; this
47367
+ * is what notices when that line goes.
47368
+ *
47275
47369
  * Delete it once you have tests of your own, or keep it; it costs nothing.
47276
47370
  */
47277
47371
  describe("test harness", () => {
47278
- test("runs in a DOM", () => {
47279
- expect(typeof document).toBe("object");
47372
+ test("renders a kit component in a DOM", () => {
47373
+ const { container } = render(<Text>ok</Text>);
47374
+ expect(container.textContent).toBe("ok");
47280
47375
  });
47281
47376
  });
47282
47377
  `
@@ -47317,6 +47412,59 @@ dist
47317
47412
  .DS_Store
47318
47413
  .lotics
47319
47414
  coverage
47415
+ `
47416
+ },
47417
+ {
47418
+ path: "AGENTS.md",
47419
+ content: `# ${args.app_name} \u2014 for the agent editing this app
47420
+
47421
+ A Lotics custom-code app: React + Vite, data through \`@lotics/app-sdk\`, every pixel from
47422
+ \`@lotics/ui\`. Both are installed here \u2014 read them from \`node_modules\`, never from memory.
47423
+
47424
+ ## Read in this order, and stop when you have the answer
47425
+
47426
+ 1. **\`node_modules/@lotics/ui/llms.txt\`** \u2014 every kit entry by the data role it answers, its
47427
+ import, and the one question that says where it does NOT go. One read, ~12k tokens.
47428
+ **Start here for any UI question**, before writing a component of your own.
47429
+ 2. **\`node_modules/@lotics/ui/dist/<module>.d.ts\`** \u2014 the props of the entry you picked, with the
47430
+ source's own doc comments. **Never guess a prop; open the declaration.**
47431
+ 3. **\`lotics docs ui/catalog/<Entry>\`** \u2014 that entry's full wrong-place test, one row.
47432
+ **Never read \`catalog.md\` whole**: it is 334 entries, and you need one.
47433
+ 4. **\`node_modules/@lotics/ui/examples/tpl_*.tsx\`** \u2014 a whole screen as source, when the SHAPE is
47434
+ the question. Copy and adapt; never import from \`examples/\`.
47435
+ 5. **\`node_modules/@lotics/app-sdk/AGENTS.md\`** \u2014 queries, workflows, files, AI.
47436
+
47437
+ \`lotics docs\` lists what is installed here at the versions installed, so a reference always
47438
+ describes the code beside it. Every answer is capped at a page; a long one hands back its section
47439
+ list, and \`lotics docs <area>/<section>\` prints one section.
47440
+
47441
+ ## The rules that do not change
47442
+
47443
+ - **Reuse before you build.** A capability the kit lacks is added to the kit, never hand-rolled
47444
+ here. Search \`llms.txt\` for the role before writing a component.
47445
+ - **Customize through TOKENS and PROPS.** Tokens are the shadcn variables, the space rungs, the
47446
+ radius ladder and the type ramp; props are \`variant\`, \`size\`, \`tone\`, \`render\` and slots.
47447
+ \`className\`/\`style\` place an entry in your layout \u2014 they do not restyle it. The kit's DOM and
47448
+ class names are private, so CSS that reaches inside an entry breaks on its next release.
47449
+ - **\`import "@lotics/ui/styles.css"\` once**, in \`src/main.tsx\` \u2014 it carries every token the kit's
47450
+ \`var()\`s read, and without it the screen renders unstyled but working.
47451
+ - **A workflow is the only way this app writes.** Queries read; \`useWorkflow\` writes.
47452
+
47453
+ ## Before you commit \u2014 six checks
47454
+
47455
+ 1. **One value, one rendering.** A chart draws the entity its table draws, and a value's filter
47456
+ renders it the way its cell does.
47457
+ 2. **Every figure is formatted, by the reader's locale** \u2014 money, dates, quantities. A number
47458
+ joined to a string is a number in somebody else's convention.
47459
+ 3. **One accent.** Everything else is neutral or a valence, whose colour IS the meaning.
47460
+ 4. **Every gap is a rung and the ratio is 1 : 3 : 6**: 4 inside a pair, 8 inside a group, 24
47461
+ between bands, 48 between sections.
47462
+ 5. **No bare native control.** A \`<button>\`, \`<input>\`, \`<select>\` or \`<table>\` in your source is a
47463
+ control outside the type ramp, the locale and the keyboard contract.
47464
+ 6. **No raw id reaches the reader.** \`rec_\`, \`opt_\` and \`fld_\` are keys; a cell shows the name.
47465
+
47466
+ Then \`npm run typecheck && npm run lint && npm test\`, and \`lotics app deploy\`.
47467
+ The full review \u2014 the gates, the probes, the measurement snippets \u2014 is \`lotics docs ui/reviewing\`.
47320
47468
  `
47321
47469
  },
47322
47470
  {
@@ -47357,12 +47505,13 @@ import { Stack } from "@lotics/ui/stack";
47357
47505
  import { Card } from "@lotics/ui/card";
47358
47506
  import { Button } from "@lotics/ui/button";
47359
47507
  import { Text } from "@lotics/ui/text";
47360
- // ... and Grid, Dialog, DatePicker, FormPicker, BarChart, LineChart, PieChart, Metric, ProgressBar, etc.
47508
+ // ... and Grid, Dialog, DatePicker, Select, BarChart, LineChart, PieChart, Metric, Progress, etc.
47361
47509
  \`\`\`
47362
47510
 
47363
- @lotics/ui ships React Native primitives that render via react-native-web
47364
- in this Vite app (the alias is preconfigured in \`vite.config.ts\`). See
47365
- the full catalog with \`lotics docs ui\`.
47511
+ @lotics/ui ships built React DOM components with their own CSS \u2014 import
47512
+ \`@lotics/ui/styles.css\` once (main.tsx does) and every one of them is themed
47513
+ and addressable. Which entry answers which role is
47514
+ \`node_modules/@lotics/ui/llms.txt\`, one read.
47366
47515
 
47367
47516
  ## Routing
47368
47517
 
@@ -47375,11 +47524,15 @@ shareable and survives a full refresh. Standalone (\`<slug>.lotics.app\`) it's a
47375
47524
  normal browser router with real path URLs. A single-screen app can drop the
47376
47525
  router and render one screen directly.
47377
47526
 
47378
- Run \`lotics docs\` for the SDK reference \u2014 it lists what the packages installed
47379
- here actually ship, at the versions installed, so it can never point at a page
47380
- that moved. \`lotics docs building_an_app\` is the sequence, read once before
47381
- building; the area docs (\`lotics docs ai\`, \`lotics docs queries\`,
47382
- \`lotics docs ui\`) are its detail.
47527
+ ## References
47528
+
47529
+ \`AGENTS.md\` in this directory is the reading order and the six checks before a
47530
+ commit. The references themselves ship inside the packages, so they always
47531
+ describe the versions installed here: \`node_modules/@lotics/ui/llms.txt\` and
47532
+ \`node_modules/@lotics/app-sdk/AGENTS.md\` are readable with no tooling at all.
47533
+ With the CLI on PATH, \`lotics docs\` indexes the same set and prints one page at
47534
+ a time \u2014 \`lotics docs building_an_app\` is the sequence, read once before
47535
+ building.
47383
47536
  `
47384
47537
  }
47385
47538
  ];
@@ -73643,8 +73796,8 @@ function warnAboutDevLink(projectDir, command) {
73643
73796
  const linked = config2.includes("loticsResolve") || config2.includes("LOTICS_UI_SRC");
73644
73797
  if (!linked) {
73645
73798
  console.error(
73646
- `\u26A0 LOTICS_UI_SRC is set but vite.config.ts never reads it \u2014 @lotics/ui will still resolve from node_modules, so kit edits will NOT ${command === "deploy" ? "ship" : "appear"}. Hand the whole resolve block to the kit (it owns the dev-link, the react-native alias, the extensions and the dedupe):
73647
- import { loticsOptimizeDeps, loticsResolve } from "@lotics/ui/vite";
73799
+ `\u26A0 LOTICS_UI_SRC is set but vite.config.ts never reads it \u2014 @lotics/ui will still resolve from node_modules, so kit edits will NOT ${command === "deploy" ? "ship" : "appear"}. Hand the whole resolve block to the kit (it owns the dev-link and the React dedupe):
73800
+ import { loticsResolve } from "@lotics/ui/vite";
73648
73801
  // \u2026
73649
73802
  resolve: loticsResolve(),`
73650
73803
  );
@@ -73722,6 +73875,71 @@ A newer kit is published \u2014 \`npm update ${findings.map((f) => f.pkg).join("
73722
73875
  ` + lines.join("\n")
73723
73876
  );
73724
73877
  }
73878
+ var KIT_SPECIFIER = "@lotics/ui/button";
73879
+ var KIT_INSTALLED_PATH = "/node_modules/@lotics/ui/dist/button.js";
73880
+ function inlineEntries(source) {
73881
+ const out = [];
73882
+ const key = /\binline\s*:\s*/g;
73883
+ for (let match2 = key.exec(source); match2 !== null; match2 = key.exec(source)) {
73884
+ const i2 = match2.index + match2[0].length;
73885
+ if (source.startsWith("true", i2)) {
73886
+ out.push({ kind: "everything" });
73887
+ continue;
73888
+ }
73889
+ if (source[i2] !== "[") continue;
73890
+ for (let depth = 1, i22 = i2 + 1; i22 < source.length && depth > 0; ) {
73891
+ const c = source[i22];
73892
+ if (c === "[") {
73893
+ depth++;
73894
+ i22++;
73895
+ } else if (c === "]") {
73896
+ depth--;
73897
+ i22++;
73898
+ } else if (c === '"' || c === "'" || c === "`") {
73899
+ let j = i22 + 1;
73900
+ while (j < source.length && source[j] !== c) j += source[j] === "\\" ? 2 : 1;
73901
+ out.push({ kind: "string", value: source.slice(i22 + 1, j) });
73902
+ i22 = j + 1;
73903
+ } else if (c === "/" && source[i22 + 1] === "/") {
73904
+ const nl = source.indexOf("\n", i22);
73905
+ i22 = nl === -1 ? source.length : nl + 1;
73906
+ } else if (c === "/" && source[i22 + 1] === "*") {
73907
+ const end = source.indexOf("*/", i22);
73908
+ i22 = end === -1 ? source.length : end + 2;
73909
+ } else if (c === "/") {
73910
+ let j = i22 + 1;
73911
+ for (let inClass = false; j < source.length; j++) {
73912
+ const ch = source[j];
73913
+ if (ch === "\\") j++;
73914
+ else if (ch === "[") inClass = true;
73915
+ else if (ch === "]") inClass = false;
73916
+ else if (ch === "/" && !inClass) break;
73917
+ else if (ch === "\n") break;
73918
+ }
73919
+ if (source[j] === "/") {
73920
+ let k = j + 1;
73921
+ while (k < source.length && /[a-z]/.test(source[k])) k++;
73922
+ out.push({ kind: "regex", source: source.slice(i22 + 1, j), flags: source.slice(j + 1, k) });
73923
+ i22 = k;
73924
+ } else i22++;
73925
+ } else i22++;
73926
+ }
73927
+ }
73928
+ return out;
73929
+ }
73930
+ function inlinesKitUnderVitest(viteConfig) {
73931
+ return inlineEntries(viteConfig).some((entry) => {
73932
+ if (entry.kind === "everything") return true;
73933
+ if (entry.kind === "string")
73934
+ return KIT_SPECIFIER === entry.value || KIT_SPECIFIER.startsWith(`${entry.value}/`);
73935
+ try {
73936
+ const re2 = new RegExp(entry.source, entry.flags.replace("g", ""));
73937
+ return re2.test(KIT_SPECIFIER) || re2.test(KIT_INSTALLED_PATH);
73938
+ } catch {
73939
+ return false;
73940
+ }
73941
+ });
73942
+ }
73725
73943
  function ensureAppVitestSetup(projectDir) {
73726
73944
  const setupPath = path9.join(projectDir, VITEST_SETUP_FILENAME);
73727
73945
  if (!fs8.existsSync(setupPath)) {
@@ -73731,11 +73949,18 @@ function ensureAppVitestSetup(projectDir) {
73731
73949
  const viteConfigPath = path9.join(projectDir, "vite.config.ts");
73732
73950
  if (!fs8.existsSync(viteConfigPath)) return;
73733
73951
  const viteConfig = fs8.readFileSync(viteConfigPath, "utf-8");
73734
- if (/setupFiles\s*:/.test(viteConfig) && viteConfig.includes(VITEST_SETUP_FILENAME)) return;
73735
- console.error(
73736
- `\u26A0 vite.config.ts does not wire ${VITEST_SETUP_FILENAME}. Nothing is broken by that \u2014 it is the slot for global test setup (matchers, stubs, timezone pinning). Add it to the \`test\` block when you need one:
73952
+ if (!(/setupFiles\s*:/.test(viteConfig) && viteConfig.includes(VITEST_SETUP_FILENAME))) {
73953
+ console.error(
73954
+ `\u26A0 vite.config.ts does not wire ${VITEST_SETUP_FILENAME}. Nothing is broken by that \u2014 it is the slot for global test setup (matchers, stubs, timezone pinning). Add it to the \`test\` block when you need one:
73737
73955
  setupFiles: ["./${VITEST_SETUP_FILENAME}"],`
73738
- );
73956
+ );
73957
+ }
73958
+ if (!inlinesKitUnderVitest(viteConfig)) {
73959
+ console.error(
73960
+ `\u26A0 vite.config.ts does not inline @lotics/ui under vitest, so \`npm test\` fails to load every suite that renders a kit component ("Unknown file extension \\".css\\"") \u2014 the kit's modules import their own stylesheets and vitest hands node_modules to Node. Add it to the \`test\` block:
73961
+ server: { deps: { inline: [/@lotics\\/ui/] } },`
73962
+ );
73963
+ }
73739
73964
  }
73740
73965
  async function appCodegen(args) {
73741
73966
  const projectDir = path9.resolve(args.projectDir ?? process.cwd());
@@ -74924,6 +75149,13 @@ function isVersionBelow(version2, floor2) {
74924
75149
  if (minor !== floorMinor) return minor < floorMinor;
74925
75150
  return patch < floorPatch;
74926
75151
  }
75152
+ function bundlesReactNative(projectDir) {
75153
+ const manifest = path9.join(projectDir, "package.json");
75154
+ if (!fs8.existsSync(manifest)) return false;
75155
+ const pkg = JSON.parse(fs8.readFileSync(manifest, "utf-8"));
75156
+ const declared = { ...pkg.dependencies, ...pkg.devDependencies };
75157
+ return "react-native" in declared || "react-native-web" in declared;
75158
+ }
74927
75159
  function warnIfProjectFootguns(projectDir, sourceText) {
74928
75160
  const lines = [];
74929
75161
  const sdkVersion = installedKitVersion(projectDir, "@lotics/app-sdk");
@@ -74939,7 +75171,7 @@ function warnIfProjectFootguns(projectDir, sourceText) {
74939
75171
  );
74940
75172
  }
74941
75173
  const configName = VITE_CONFIG_NAMES.find((name2) => fs8.existsSync(path9.join(projectDir, name2)));
74942
- if (configName !== void 0) {
75174
+ if (configName !== void 0 && bundlesReactNative(projectDir)) {
74943
75175
  const config2 = fs8.readFileSync(path9.join(projectDir, configName), "utf-8");
74944
75176
  const defined = defineBlockKeys(config2);
74945
75177
  const missing = ["global", "__DEV__"].filter((name2) => !defined.has(name2));
@@ -90327,11 +90559,11 @@ function buildXfXml(s, fontId, fillId, borderId, numFmtId) {
90327
90559
  if (s.horizontalAlign || s.verticalAlign || s.wrapText || s.indent || s.textRotation || s.shrinkToFit) {
90328
90560
  const hAlign = s.horizontalAlign ? ` horizontal="${s.horizontalAlign}"` : "";
90329
90561
  const vAlign = s.verticalAlign ? ` vertical="${s.verticalAlign}"` : "";
90330
- const wrap = s.wrapText ? ' wrapText="1"' : "";
90562
+ const wrap2 = s.wrapText ? ' wrapText="1"' : "";
90331
90563
  const indent = s.indent ? ` indent="${s.indent}"` : "";
90332
90564
  const rotation = s.textRotation !== void 0 ? ` textRotation="${s.textRotation === "vertical" ? 255 : s.textRotation}"` : "";
90333
90565
  const shrink = s.shrinkToFit ? ' shrinkToFit="1"' : "";
90334
- return `<xf ${attrs} applyAlignment="1"><alignment${hAlign}${vAlign}${wrap}${indent}${rotation}${shrink}/></xf>`;
90566
+ return `<xf ${attrs} applyAlignment="1"><alignment${hAlign}${vAlign}${wrap2}${indent}${rotation}${shrink}/></xf>`;
90335
90567
  }
90336
90568
  return `<xf ${attrs}/>`;
90337
90569
  }