@tinkerlandai/indiana-iam 0.5.0 → 0.7.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/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # @tinkerlandai/indiana-iam
2
2
 
3
- Build-time generator for the Indiana App Manifest (IAM). On every production build it scans your App's source, generates
4
- and validates the IAM, and writes the release descriptor your pages use to bind visual changes to that exact source.
5
- Indiana's App page map edits pages from these files; the browser SDK applies the published changes.
3
+ Build-time generator for the Indiana App Manifest (IAM). It scans your App's source, generates and validates the IAM,
4
+ and writes an immutable release descriptor. Web integrations bind published visual changes to that exact source.
5
+ React Native / Expo integrations produce source previews; native visual-change execution is a separate capability.
6
6
 
7
7
  Install it as a development dependency (Node.js 22 or later):
8
8
 
9
9
  ```sh
10
- npm install --save-dev @tinkerlandai/indiana-iam
10
+ npm install --save-dev --save-exact @tinkerlandai/indiana-iam@0.7.0
11
11
  ```
12
12
 
13
13
  ## Next.js (15.4.1 or later)
@@ -73,6 +73,77 @@ Browser apps use the single local module script in `index.html` to select reacha
73
73
  For multiple entry points, set `indianaIam({ entry: "src/main.tsx" })`, or pass `--entry src/main.tsx` to standalone generation.
74
74
  The path must stay inside the scanned root. Next.js uses its App/Pages root instead.
75
75
 
76
+ ## React Native and Expo Router
77
+
78
+ Use the build-only `@tinkerlandai/indiana-iam/react-native` entry from a Node.js build script. Generate a separate artifact
79
+ for each platform. It reuses the same IAM format, validator, asset packaging, release identity and upload client as Web.
80
+
81
+ ```js
82
+ import { prepareReactNativeIam, finishReactNativeIam } from "@tinkerlandai/indiana-iam/react-native";
83
+
84
+ const options = { root: process.cwd(), platform: "ios", routerRoot: "app", strict: true };
85
+ const prepared = await prepareReactNativeIam(options);
86
+ // Run your existing Expo / Metro bundle step here, using the same unchanged source.
87
+ const result = await finishReactNativeIam({
88
+ ...options,
89
+ releaseId: prepared.releaseId,
90
+ bundleDirectory: "dist/ios", // asset staging directory inside root
91
+ });
92
+ // Include dist/ios + result.descriptorPath with this release's native assets.
93
+ // result.file is the validated IAM outside the checkout.
94
+ ```
95
+
96
+ For Android, use `platform: "android"` and a separate staging directory. With Expo Router, `routerRoot` defaults to
97
+ the sole `app/` or `src/app/` directory. Ordinary route groups, index/dynamic/catch-all paths and nested `_layout`
98
+ modules with `Slot`, `Stack` or `Tabs` retain route and source provenance. Group paths that collapse to the same URL
99
+ are rejected. Shared array groups and native navigation state remain explicit gaps. Expo platform routes and layouts
100
+ need a non-platform base module. For a single exported React Native screen, use `entry: "App.tsx"` instead of `routerRoot`.
101
+ That entry does not extract a React Navigation graph or invoke `AppRegistry`.
102
+
103
+ The adapter selects `.ios` / `.android`, then `.native`, then base modules using Metro's default JS/JSX/TS/TSX
104
+ extensions. Literal `Platform.OS`, `Platform.select`, `StyleSheet.create`, `flatten`, `compose` and static style arrays
105
+ are interpreted from source. Basic View/Text/Image, buttons, text inputs, scrolling and modal declarations project to
106
+ inert semantic HTML. Local raster imports and literal `require` references use the existing bounded asset packer.
107
+ Supported local state setters on `onPress` / `onChangeText` reuse the bounded preview interpreter. Business handlers,
108
+ effects, remote data, custom Metro/Babel transforms, Yoga/device layout, safe-area measurement, navigation stacks,
109
+ gestures and virtual lists are not executed; their missing behavior stays in coverage gaps.
110
+
111
+ Every native preview node stays synthetic and supplies no SDK target binding, even when it has a `testID` or `nativeID`.
112
+ This entry adds no native runtime dependency or automatic source-anchor transform. The descriptor records release identity;
113
+ it does not install a native visual executor or make a Web visual script applicable to native views. Package its file
114
+ with native assets in your existing release process; the helper does not configure Metro or Xcode/Gradle for you.
115
+
116
+ Uploads are disabled by default. Explicitly authorized builds can pass
117
+ `upload: { upload: "always", client: "rn-ios", apiKey, baseUrl }` to `finishReactNativeIam`;
118
+ the default client is `rn-ios` / `rn-android`, and each client's platform is immutable. Use the matching Indiana
119
+ ingestion validator version before uploading new source adapters. Local generation does not deploy it.
120
+
121
+ Standalone generation uses the same adapter:
122
+
123
+ ```sh
124
+ indiana-iam generate --root . --platform ios --router-root app --out <new folder outside checkout>
125
+ indiana-iam generate --root . --platform android --entry App.tsx --out <another new folder>
126
+ ```
127
+
128
+ ## Memos integration example
129
+
130
+ [`examples/memos/indiana.vite.config.mjs`](examples/memos/indiana.vite.config.mjs) wraps the original Vite configuration of
131
+ [Memos v0.25.3](https://github.com/usememos/memos/tree/e17cd163c6c37726f8397c8f426d585b540c9562). It keeps Memos’s Go backend,
132
+ SQLite database and application source, adds build anchors and binds the visual SDK to the generated release descriptor.
133
+
134
+ After installing Memos’s own Web dependencies, set `INDIANA_MEMOS_WEB_ROOT` to its absolute `web` directory,
135
+ `INDIANA_ENVIRONMENT_ID` to the intended Sandbox environment UUID, `INDIANA_CONSOLE_ORIGIN` to `https://dev.tinker.cash`
136
+ and `INDIANA_API_ORIGIN` to `https://api.dev.tinker.cash`. From Memos’s `web` directory, run its installed `vite build`
137
+ with `--config <Indiana checkout>/packages/iam/examples/memos/indiana.vite.config.mjs --mode release --outDir dist --emptyOutDir`.
138
+ The example disables automatic uploads; upload the IAM file reported in the build log through the separately authorized IAM upload flow.
139
+
140
+ Copy `web/dist` into Memos’s `server/router/frontend/dist` before building its native binary. Run that binary on
141
+ `127.0.0.1:15230`, then run [`examples/memos/gateway.mjs`](examples/memos/gateway.mjs) with the same Web root. The gateway
142
+ listens on `127.0.0.1:15231`, serves the generated `.well-known/indiana/releases/` descriptor omitted by Go’s `dist/*` embed,
143
+ and forwards every other request unchanged to native Memos. Ports can be set with `INDIANA_MEMOS_PORT` and
144
+ `INDIANA_MEMOS_BACKEND_PORT`. Allow the exact gateway origin in the same Sandbox environment before publishing page changes.
145
+ Keep accounts, keys, SQLite files, generated manifests and binaries outside the committed example.
146
+
76
147
  ## Options
77
148
 
78
149
  Both integrations accept `{ enabled, name, automaticAnchors, strict, rootFont, omitSvgResources, logger, apiKey, baseUrl, upload, client }`;
@@ -83,8 +154,8 @@ If source changes during compilation, generation fails instead of publishing a d
83
154
 
84
155
  ## Target identity and release changes
85
156
 
86
- Framework builds reuse `id`, `data-testid`, `data-test`, `data-qa`, then `data-indiana-id`. When none is declared,
87
- the source loader adds a deterministic `data-indiana-id` to unambiguous native JSX in the build output and IAM.
157
+ Next.js / Vite builds reuse `id`, `data-testid`, `data-test`, `data-qa`, then `data-indiana-id`. When none is declared,
158
+ the source loader adds a deterministic `data-indiana-id` to unambiguous HTML JSX in the build output and IAM.
88
159
  Source files stay unchanged; formatting, comments and unrelated sibling insertions preserve a leaf's marker.
89
160
  Set `automaticAnchors: false` to use only existing attributes.
90
161
 
@@ -157,7 +228,7 @@ Source-proven `useSyncExternalStore` matchMedia getters retain both typed viewpo
157
228
  viewport. Subscriptions are not run. UI preference cookies with an explicit source SSR guard use an empty-cookie
158
229
  preview; auth/session cookies and arbitrary stores stay unobserved. Pure lazy local-state initializers are interpreted
159
230
  under the same bounded rules; merchant callbacks and browser storage are never executed or read.
160
- IAM has no configured page, node or total artifact byte limit. The exporter retains every supported route.
231
+ Local IAM generation has no configured page, node or total artifact byte limit. The exporter retains every supported route. Online admission uses the environment's 512 MiB source storage budget, 256-version limit and build-key upload rate limit; it has no separate 8 MiB file cap.
161
232
  Large artifacts store sizeable HTML/preorder-map pairs as `domPayload` (`gzip-json-v1`), while keeping `html: ""`
162
233
  and `nodes: []` in that document's wire record. This lossless storage encoding preserves all source IDs, DOM values,
163
234
  HTML/DOM hashes and targets. Route navigation stores the exact interaction-array index instead of repeating its ID; validation checks node, route, destination and parameters. Source files retain their full hashes; component/element records cover reachable route compositions and shared fragments. Text node IDs that follow the original parent/index identity formula are derived locally instead of repeated; explicit exceptions remain stored, and every resolved source-map hash is checked. The document hash pins its encoded storage body; decoding is local and checks the exact declared size and hashes. Exported page HTML is expanded.
@@ -166,7 +237,7 @@ the selected document on demand, cancels superseded reads and retains one expand
166
237
  come from the complete validated IAM; the complete JSON remains the source of release identity.
167
238
  Serve the output directory over loopback HTTP to open `index.html`; browser `file://` restrictions prevent
168
239
  on-demand reads of its document files. Serving the exported files does not run the App or its backend.
169
- Use the matching 0.4 generator, validator and viewer together. Indiana ingestion/viewer sources must be updated
240
+ Use the matching generator, validator and viewer from the same package version together. Indiana ingestion/viewer sources must be updated
170
241
  before uploading compressed artifacts; publishing this npm package alone does not deploy Indiana. The offline
171
242
  viewer uses the browser's built-in `DecompressionStream` and never fetches a decoder or merchant content.
172
243
  The scanner reads source files only; it never runs your build, imports your code or reads env files. Only the upload
@@ -175,11 +246,25 @@ Values that look like credentials, tokens, emails or private URLs are removed, b
175
246
 
176
247
  ## Upload
177
248
 
249
+ IAM 0.7.0 uses the matching Indiana resumable ingestion protocol. Upgrade the build package together with the receiver.
250
+
178
251
  Create a Key with only the `app-manifests:write` permission (Console → developer settings) and set it as
179
252
  `INDIANA_IAM_KEY` in your build environment. Each production build then registers the IAM, stores it through a
180
- server-signed URL and asks Indiana to validate it; the validated version becomes its client's current version.
253
+ private GCS resumable session and asks Indiana to validate it; the validated version becomes its client's current version.
181
254
  Rebuilding unchanged source only makes the existing version current again.
182
255
 
256
+ Interrupted uploads resume when the same command or build runs again on the same machine. The uploader keeps the
257
+ request ID, original label and private GCS session in `~/.cache/indiana/iam-uploads` (directory mode 0700, files 0600),
258
+ scoped by Indiana origin, key fingerprint, client, platform and exact content. Set `INDIANA_IAM_CACHE_DIR` to a private
259
+ directory to retain this state in a CI cache; CI workers with no shared cache start a new upload. Never publish this
260
+ directory or place it in source/public build output. It contains bearer upload capabilities, but no API keys.
261
+ On recovery, Indiana checks current key authority before the cached storage session is used. GCS's persisted offset
262
+ selects the next byte; chunks are at most 8 MiB. Transient failures retry up to three times, while completed transfers
263
+ retry validation without uploading again. Recovery retains the existing 15-minute SQL upload deadline; an expired
264
+ intent starts a new version. A successful validation removes the local checkpoint. Storage sessions can remain valid
265
+ longer than the SQL intent, but cannot make an expired or unauthorized version ready. The matching Indiana ingestion
266
+ and build package must be deployed/published together for this transfer protocol.
267
+
183
268
  An App environment can hold several clients, each a named build with its own current version: for example `web` and
184
269
  `web-dashboard`, or later `ios` and `android` apps. Set the `client` option (or `INDIANA_IAM_CLIENT`) to 1-40 lowercase
185
270
  letters, digits or hyphens starting with a letter; the default is `web`. A client keeps the platform of its first
@@ -9,7 +9,14 @@
9
9
  }
10
10
  let machine = null, activeTabs = [],sourceUi=null,sourceRenderKey=null;
11
11
  const patches = new Map();
12
- const node = id => [...document.querySelectorAll("[data-iam-node]")].find(e => e.dataset.iamNode === id);
12
+ const previewNodes = new Map([...document.querySelectorAll("[data-iam-node]")].map(el => [el.dataset.iamNode, el]));
13
+ const node = id => {
14
+ const cached = previewNodes.get(id);
15
+ if (cached?.isConnected) return cached;
16
+ const current = [...document.querySelectorAll("[data-iam-node]")].find(el => el.dataset.iamNode === id);
17
+ if (current) previewNodes.set(id, current);
18
+ return current;
19
+ };
13
20
  function viewportValues(graph,routeKey,renderKey){
14
21
  const values={};for(const variable of graph.variables??[]){
15
22
  if(variable.routeKey!==routeKey||variable.renderKey!==(renderKey??graph.routes.find(r=>r.routeKey===routeKey)?.initialRenderKey)||variable.valueType!=='boolean')continue;
@@ -34,6 +41,49 @@
34
41
  applySourceCollections(document,sourceUi,sourceRenderKey,values,true);
35
42
  markUnobservedData(document,sourceUi,true);
36
43
  for (const patch of patches.values()) applyPatch(patch);
44
+ reportProjection();
45
+ }
46
+ function reportProjection() {
47
+ const visibility = new WeakMap();
48
+ const visible = el => {
49
+ if (!el) return false;
50
+ if (visibility.has(el)) return visibility.get(el);
51
+ const style = getComputedStyle(el);
52
+ const result = !el.hidden && style.display !== "none" && style.visibility !== "hidden" &&
53
+ (!el.parentElement || visible(el.parentElement));
54
+ visibility.set(el, result);
55
+ return result;
56
+ };
57
+ const variables = new Set();
58
+ const reads = expression => {
59
+ if (!expression || typeof expression !== "object") return;
60
+ if (expression.op === "var") variables.add(expression.key);
61
+ for (const value of Object.values(expression)) {
62
+ if (Array.isArray(value)) value.forEach(reads);
63
+ else if (value && typeof value === "object") reads(value);
64
+ }
65
+ };
66
+ for (const binding of viewportGraph.bindings) {
67
+ if (binding.routeKey !== viewportRoute || binding.renderKey !== sourceRenderKey) continue;
68
+ const el = node(binding.nodeId);
69
+ if (el && visible(binding.property === "visible" ? el.parentElement : el)) reads(binding.value);
70
+ }
71
+ for (const transition of activeTransitions) if (visible(node(transition.trigger.nodeId))) {
72
+ transition.stateReads.forEach(key => variables.add(key));
73
+ transition.stateWrites.forEach(write => variables.add(write.variableKey));
74
+ }
75
+ const unobserved = [...document.querySelectorAll("[data-iam-preview-note]")].some(visible);
76
+ let content = false;
77
+ if (unobserved) {
78
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
79
+ let text;
80
+ while ((text = walker.nextNode())) if (text.textContent.trim() &&
81
+ !text.parentElement.closest("script,style,[data-iam-preview-note],[data-iam-observation]") && visible(text.parentElement)) {
82
+ content = true; break;
83
+ }
84
+ if (!content) content = [...document.querySelectorAll("img,svg,canvas,input,textarea,select,button")].some(visible);
85
+ }
86
+ send("projection", { visibleVariableKeys: [...variables], dataUnavailable: unobserved && !content });
37
87
  }
38
88
  function applyPatch(data) {
39
89
  const el = node(data.nodeId); if (!el) return;
@@ -245,6 +245,9 @@ function markUnobservedData(doc, ui, enabled, selections = new Map()) {
245
245
  field.setAttribute("placeholder", "未观测");
246
246
  field.setAttribute("title", "此字段的数据未观测");
247
247
  }
248
+ for (const root of doc.querySelectorAll('[data-iam-source-template="unobserved"]')) {
249
+ const note=doc.createElement('div');note.setAttribute('data-iam-preview-note','unobserved');note.className='iam-observation-note';note.setAttribute('role','status');note.textContent='集合数据未观测 · 可检查源码组件结构';root.before(note);
250
+ }
248
251
  return unobserved;
249
252
  }
250
253
  /* Indiana UI simulation runtime 0.2.0. No merchant code, requests, authentication or purchases. */
@@ -258,7 +261,14 @@ function markUnobservedData(doc, ui, enabled, selections = new Map()) {
258
261
  }
259
262
  let machine = null, activeTabs = [],sourceUi=null,sourceRenderKey=null;
260
263
  const patches = new Map();
261
- const node = id => [...document.querySelectorAll("[data-iam-node]")].find(e => e.dataset.iamNode === id);
264
+ const previewNodes = new Map([...document.querySelectorAll("[data-iam-node]")].map(el => [el.dataset.iamNode, el]));
265
+ const node = id => {
266
+ const cached = previewNodes.get(id);
267
+ if (cached?.isConnected) return cached;
268
+ const current = [...document.querySelectorAll("[data-iam-node]")].find(el => el.dataset.iamNode === id);
269
+ if (current) previewNodes.set(id, current);
270
+ return current;
271
+ };
262
272
  function viewportValues(graph,routeKey,renderKey){
263
273
  const values={};for(const variable of graph.variables??[]){
264
274
  if(variable.routeKey!==routeKey||variable.renderKey!==(renderKey??graph.routes.find(r=>r.routeKey===routeKey)?.initialRenderKey)||variable.valueType!=='boolean')continue;
@@ -283,6 +293,49 @@ function markUnobservedData(doc, ui, enabled, selections = new Map()) {
283
293
  applySourceCollections(document,sourceUi,sourceRenderKey,values,true);
284
294
  markUnobservedData(document,sourceUi,true);
285
295
  for (const patch of patches.values()) applyPatch(patch);
296
+ reportProjection();
297
+ }
298
+ function reportProjection() {
299
+ const visibility = new WeakMap();
300
+ const visible = el => {
301
+ if (!el) return false;
302
+ if (visibility.has(el)) return visibility.get(el);
303
+ const style = getComputedStyle(el);
304
+ const result = !el.hidden && style.display !== "none" && style.visibility !== "hidden" &&
305
+ (!el.parentElement || visible(el.parentElement));
306
+ visibility.set(el, result);
307
+ return result;
308
+ };
309
+ const variables = new Set();
310
+ const reads = expression => {
311
+ if (!expression || typeof expression !== "object") return;
312
+ if (expression.op === "var") variables.add(expression.key);
313
+ for (const value of Object.values(expression)) {
314
+ if (Array.isArray(value)) value.forEach(reads);
315
+ else if (value && typeof value === "object") reads(value);
316
+ }
317
+ };
318
+ for (const binding of viewportGraph.bindings) {
319
+ if (binding.routeKey !== viewportRoute || binding.renderKey !== sourceRenderKey) continue;
320
+ const el = node(binding.nodeId);
321
+ if (el && visible(binding.property === "visible" ? el.parentElement : el)) reads(binding.value);
322
+ }
323
+ for (const transition of activeTransitions) if (visible(node(transition.trigger.nodeId))) {
324
+ transition.stateReads.forEach(key => variables.add(key));
325
+ transition.stateWrites.forEach(write => variables.add(write.variableKey));
326
+ }
327
+ const unobserved = [...document.querySelectorAll("[data-iam-preview-note]")].some(visible);
328
+ let content = false;
329
+ if (unobserved) {
330
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);
331
+ let text;
332
+ while ((text = walker.nextNode())) if (text.textContent.trim() &&
333
+ !text.parentElement.closest("script,style,[data-iam-preview-note],[data-iam-observation]") && visible(text.parentElement)) {
334
+ content = true; break;
335
+ }
336
+ if (!content) content = [...document.querySelectorAll("img,svg,canvas,input,textarea,select,button")].some(visible);
337
+ }
338
+ send("projection", { visibleVariableKeys: [...variables], dataUnavailable: unobserved && !content });
286
339
  }
287
340
  function applyPatch(data) {
288
341
  const el = node(data.nodeId); if (!el) return;
package/assets/viewer.css CHANGED
@@ -706,6 +706,15 @@ button:disabled:hover { background: transparent; }
706
706
  .field input:disabled, .field textarea:disabled { color: var(--iam-ink-3); background: var(--iam-surface-sunk); }
707
707
  .state-control { display: grid; gap: 8px; margin-block: 12px; }
708
708
  .state-control select { width: 100%; min-height: 40px; border: 1px solid var(--iam-field-border); border-radius: 2px; background: var(--iam-surface); }
709
+ .state-group { border: 0; padding: 0; margin: 16px 0; min-width: 0; }
710
+ .state-group legend { color: var(--iam-ink); font-weight: 500; overflow-wrap: anywhere; }
711
+ .state-group .field { margin: 8px 0; }
712
+ .state-group[hidden], .state-group .field[hidden] { display: none; }
713
+ .state-other { margin-block: 12px; }
714
+ .state-other summary { cursor: pointer; color: var(--iam-ink-2); }
715
+ .projection-note { padding: 12px 16px; border-bottom: 1px solid var(--iam-line); background: var(--iam-surface-sunk); color: var(--iam-ink-2); }
716
+ .projection-note p { margin: 0 0 8px; max-width: 80ch; }
717
+ .projection-note[hidden] { display: none; }
709
718
  #preview { color-scheme: normal; background: #ffffff; }
710
719
  .frame-wrap { background: #ffffff; }
711
720
  body.embedded #export { display: none; }
@@ -63,6 +63,10 @@
63
63
  <div class="breadcrumb">
64
64
  <span id="route"></span><span id="document-status"></span>
65
65
  </div>
66
+ <div id="projection-note" class="projection-note" role="status" hidden>
67
+ <p>此页面依赖未观测的运行时数据,当前无法展示完整内容。可查看未覆盖项,或从页面地图查看源码分支。</p>
68
+ <button id="projection-gaps">查看未覆盖项</button>
69
+ </div>
66
70
  <div class="canvas" id="canvas">
67
71
  <div class="frame-wrap" id="frame-wrap">
68
72
  <iframe
package/assets/viewer.js CHANGED
@@ -67,7 +67,9 @@ const iamTranslations = {
67
67
  "界面状态": "UI states",
68
68
  "分析范围与未覆盖项": "Coverage and gaps",
69
69
  "DOM 映射 / SHA-256 / 源码证据": "DOM mappings / SHA-256 / source evidence",
70
- "正在打开页面资料…": "Opening page data…"
70
+ "正在打开页面资料…": "Opening page data…",
71
+ "此页面依赖未观测的运行时数据,当前无法展示完整内容。可查看未覆盖项,或从页面地图查看源码分支。": "This page depends on runtime data that has not been observed. Its full content is unavailable. Review coverage gaps or inspect source branches in the page map.",
72
+ "查看未覆盖项": "Review coverage gaps"
71
73
  };
72
74
  const tr = value => iamEnglish ? iamTranslations[value] ?? value : value;
73
75
  // Theme changes are scoped to the Console tool shell, never its merchant iframe.
@@ -107,6 +109,7 @@ async function mountIamViewer(m, consoleContext) {
107
109
  nonce = "",
108
110
  viewport = matchMedia("(max-width: 900px)").matches ? 390 : 1280,
109
111
  pendingAnchor = null;
112
+ let projection = null, stateSearch = "";
110
113
  function setMobilePanel(panel) {
111
114
  document.body.dataset.panel = panel;
112
115
  for (const button of document.querySelectorAll(".mobile-panels [data-panel]")) {
@@ -305,6 +308,8 @@ async function mountIamViewer(m, consoleContext) {
305
308
  );
306
309
  }
307
310
  async function openPage(next, targetUrl = next.route, renderKey, historyMove=null) {
311
+ projection = null; stateSearch = "";
312
+ $("projection-note").hidden = true;
308
313
  if (consoleContext) targetUrl = viewUrl(targetUrl);
309
314
  retryOpen={pageKey:next.pageKey,targetUrl,renderKey,historyMove};
310
315
  if(consoleContext)consoleContext.documentReady=false;
@@ -479,6 +484,11 @@ async function mountIamViewer(m, consoleContext) {
479
484
  }
480
485
  }
481
486
  if (data.type === "selected") showSelection(data.element);
487
+ if (data.type === "projection" && Array.isArray(data.visibleVariableKeys)) {
488
+ projection = data;
489
+ $("projection-note").hidden = data.dataUnavailable !== true;
490
+ renderStates(uiValues.get(viewKey()) ?? {});
491
+ }
482
492
  if (data.type === "stateChanged") {uiValues.set(viewKey(),data.values);renderStates(data.values);}
483
493
  if (data.type === "navigate") {
484
494
  const interaction = m.interactions.find(
@@ -507,23 +517,66 @@ async function mountIamViewer(m, consoleContext) {
507
517
  if (data.type === "simulated") toast(data.message);
508
518
  });
509
519
  function renderStates(values = {}) {
510
- const target = $("state-list"), canPreviewValues = scriptFor(doc).js.includes('type === "previewValues"'); target.replaceChildren();
520
+ const target = $("state-list"), canPreviewValues = scriptFor(doc).js.includes('type === "previewValues"');
521
+ const focused = document.activeElement?.dataset.variableKey;
522
+ const otherOpen = target.querySelector(".state-other")?.open ?? false;
523
+ target.replaceChildren();
511
524
  const renders=m.routeTree.renders?.filter(row=>row.routeKey===page.pageKey)??[];
512
525
  if(renders.length>1){const label=text('label',iamEnglish?'Page state':'页面状态','state-control'),select=document.createElement('select');select.setAttribute('aria-label',iamEnglish?'Page state':'页面状态');for(const [index,row]of renders.entries()){const option=document.createElement('option');option.value=row.renderKey;option.textContent=row.entryMode==='hard'?(iamEnglish?'Direct load':'直接打开'):(iamEnglish?'Soft navigation ':'软导航 ')+index;option.selected=row.renderKey===render?.renderKey;select.append(option);}select.addEventListener('change',()=>openPage(page,currentUrl,select.value));label.append(select);target.append(label);}
513
526
  const variables = m.routeTree.variables.filter(v => v.routeKey === page.pageKey && (!render || v.renderKey===render.renderKey));
514
527
  const stateSection = target.closest("details"), hasControls = String(variables.length > 0 || renders.length > 1);
515
528
  if (consoleContext && stateSection.dataset.hasControls !== hasControls) { stateSection.open = hasControls === "true"; stateSection.dataset.hasControls = hasControls; }
529
+ const active = new Set(projection?.visibleVariableKeys ?? []);
530
+ const other = document.createElement("details"); other.className = "state-other"; other.open = otherOpen;
531
+ other.append(text("summary", (iamEnglish ? "Other source states" : "其他源码状态") + ` (${variables.filter(v => !active.has(v.variableKey)).length})`));
532
+ const search = document.createElement("input"); search.type = "search"; search.value = stateSearch;
533
+ search.placeholder = iamEnglish ? "Find states or components" : "查找状态或组件";
534
+ search.setAttribute("aria-label", search.placeholder);
535
+ const searchLabel = text("label", iamEnglish ? "Filter states" : "筛选状态", "field"); searchLabel.append(search);
536
+ if (variables.length > 8) target.append(searchLabel);
537
+ const groups = new Map();
538
+ const filter = () => {
539
+ stateSearch = search.value;
540
+ const query = stateSearch.trim().toLocaleLowerCase();
541
+ for (const group of groups.values()) {
542
+ let shown = false;
543
+ for (const field of group.querySelectorAll(".field")) {
544
+ field.hidden = !field.dataset.search.includes(query); shown ||= !field.hidden;
545
+ }
546
+ group.hidden = !shown;
547
+ }
548
+ if (query) other.open = true;
549
+ };
550
+ search.addEventListener("input", filter);
516
551
  for (const variable of variables) {
552
+ const source = typeof variable.source === "number" ? m.routeTree.sourcePool?.[variable.source] : variable.source;
553
+ const owner = source?.symbol || source?.file?.split("/").at(-1) || (iamEnglish ? "Page" : "页面");
554
+ const isActive = active.has(variable.variableKey), groupKey = `${isActive}:${source?.file}:${owner}`;
555
+ if (!groups.has(groupKey)) {
556
+ const group = document.createElement("fieldset"); group.className = "state-group";
557
+ group.append(text("legend", owner)); groups.set(groupKey, group);
558
+ (isActive ? target : other).append(group);
559
+ }
560
+ const group = groups.get(groupKey);
517
561
  const value = Object.hasOwn(values, variable.variableKey) ? values[variable.variableKey] : variable.initialValue;
518
- if (!canPreviewValues) { target.append(text("p", variable.label + " = " + JSON.stringify(value))); continue; }
562
+ if (!canPreviewValues) {
563
+ const field = text("p", variable.label + " = " + JSON.stringify(value), "field");
564
+ field.dataset.search = `${variable.label} ${owner} ${source?.file ?? ""}`.toLocaleLowerCase();
565
+ group.append(field); continue;
566
+ }
519
567
  const label = text("label", variable.label, "field"), input = document.createElement("input");
568
+ label.dataset.search = `${variable.label} ${owner} ${source?.file ?? ""}`.toLocaleLowerCase();
569
+ if (source) label.title = `${source.file}:${source.line}`;
520
570
  input.dataset.variableKey = variable.variableKey;
521
571
  input.type = variable.valueType === "boolean" ? "checkbox" : variable.valueType === "number" ? "number" : "text";
522
572
  input.setAttribute("aria-label", variable.label); input.maxLength = 4000;
523
573
  if (variable.valueType === "boolean") input.checked = value; else input.value = String(value);
524
574
  input.addEventListener("change", () => message("previewValues", { values: { [variable.variableKey]: variable.valueType === "boolean" ? input.checked : variable.valueType === "number" ? Number(input.value) : input.value } }));
525
- label.append(input); target.append(label);
575
+ label.append(input); group.append(label);
526
576
  }
577
+ if (variables.some(v => !active.has(v.variableKey))) target.append(other);
578
+ filter();
579
+ if (focused) [...target.querySelectorAll("input")].find(input => input.dataset.variableKey === focused)?.focus();
527
580
  if (variables.length && canPreviewValues) target.append(text("p", tr("仅模拟界面值,不执行业务操作,也不保存为线上修改。")));
528
581
  const transitions = m.routeTree.transitions.filter(t => t.routeKey === page.pageKey && (!render || t.renderKey===render.renderKey));
529
582
  target.append(text("p", transitions.length + tr(" 个可模拟变化;其他业务行为列于未覆盖项。")));
@@ -761,11 +814,17 @@ function expandDomNodes(document, body, deriveTextId) {
761
814
  toast(tr("字号须为 8–120,已恢复当前值。"));
762
815
  });
763
816
  $("color").addEventListener("input", () => patch("color", $("color").value));
817
+ $("pick").setAttribute("aria-pressed", String(selecting));
764
818
  $("pick").addEventListener("click", () => {
765
819
  selecting = !selecting;
766
820
  $("pick").classList.toggle("active", selecting);
821
+ $("pick").setAttribute("aria-pressed", String(selecting));
767
822
  message("mode", { selecting });
768
823
  });
824
+ $("projection-gaps").addEventListener("click", () => {
825
+ const section = $("gaps").closest("details"); section.open = true;
826
+ setMobilePanel("inspector"); section.querySelector("summary").focus();
827
+ });
769
828
  let fitRequest = 0;
770
829
  if (typeof ResizeObserver !== "undefined") new ResizeObserver(() => {
771
830
  cancelAnimationFrame(fitRequest);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tinkerlandai/indiana-iam",
3
- "version": "0.5.0",
4
- "description": "Build-time Indiana App Manifest (IAM) generator for Next.js and Vite apps: scans source after the production build and writes the IAM and its release descriptor.",
3
+ "version": "0.7.0",
4
+ "description": "Build-time Indiana App Manifest (IAM) generator for Next.js, Vite and React Native / Expo source.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
7
7
  "type": "git",
@@ -38,6 +38,10 @@
38
38
  "./vite": {
39
39
  "types": "./src/vite.d.ts",
40
40
  "import": "./src/vite.mjs"
41
+ },
42
+ "./react-native": {
43
+ "types": "./src/react-native.d.ts",
44
+ "import": "./src/react-native.mjs"
41
45
  }
42
46
  },
43
47
  "scripts": {