@youtyan/code-viewer 0.8.1 → 0.8.2

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
@@ -178,7 +178,9 @@ files or directory names completely.
178
178
 
179
179
  Scope settings control directory exclusions shared by the sidebar, Ctrl+K file
180
180
  palette, Ctrl+G grep palette, the Datastores browser, and the file change
181
- watcher — the same list applies to all five. Everything you change in Viewer
181
+ watcher — the same list applies to all five. Both the skip list and the hide
182
+ list accept gitignore-style wildcards (`*`, `?`, `[abc]`, `[!abc]`) in
183
+ addition to exact names. Everything you change in Viewer
182
184
  Settings is saved on the server under `.code-viewer/settings.json` (no
183
185
  separate project-level config file). `.DS_Store` and a broad set of
184
186
  build/cache directories (`node_modules`, `dist`, `build`, `.next`, `.turbo`,
@@ -249,9 +251,10 @@ concurrency via `_seq_no` / `_primary_term`), create new documents, and delete
249
251
  existing ones. Snapshots and diffs over `_search` iteration are supported.
250
252
 
251
253
  **DynamoDB** support: detect LocalStack services with DynamoDB enabled, list
252
- tables across paginated responses, inspect key schemas, scan or query items,
253
- page with `LastEvaluatedKey`, and open an item detail view with a copyable key.
254
- The explorer is read-only.
254
+ tables across paginated responses, and browse a Structure tab (key schema,
255
+ global/local secondary indexes, and non-key attribute types inferred from
256
+ loaded items) alongside scan or query items, `LastEvaluatedKey` pagination,
257
+ and an item detail view with a copyable key. The explorer is read-only.
255
258
 
256
259
  **S3-compatible object storage** (MinIO, LocalStack): browse
257
260
  buckets as a folder tree, search by prefix or filename, sort scanned objects by
@@ -835,6 +835,110 @@ var init_command_resolver = __esm(() => {
835
835
  activeOverrides = new Map;
836
836
  });
837
837
 
838
+ // web-src/server/name-pattern.ts
839
+ function parseGlobSegment(pattern) {
840
+ const matchers = [];
841
+ for (let i = 0;i < pattern.length; i++) {
842
+ const ch = pattern[i];
843
+ if (ch === "*") {
844
+ matchers.push({ kind: "star" });
845
+ } else if (ch === "?") {
846
+ matchers.push({ kind: "any" });
847
+ } else if (ch === "[") {
848
+ const close = pattern.indexOf("]", i + 1);
849
+ if (close === -1) {
850
+ matchers.push({ kind: "literal", ch: "[" });
851
+ continue;
852
+ }
853
+ const rawBody = pattern.slice(i + 1, close);
854
+ const negate = rawBody.startsWith("!");
855
+ matchers.push({
856
+ kind: "class",
857
+ body: negate ? rawBody.slice(1) : rawBody,
858
+ negate
859
+ });
860
+ i = close;
861
+ } else {
862
+ matchers.push({ kind: "literal", ch });
863
+ }
864
+ }
865
+ return matchers;
866
+ }
867
+ function charInClassBody(ch, body) {
868
+ for (let i = 0;i < body.length; ) {
869
+ if (body[i + 1] === "-" && i + 2 < body.length) {
870
+ if (ch >= body[i] && ch <= body[i + 2])
871
+ return true;
872
+ i += 3;
873
+ } else {
874
+ if (ch === body[i])
875
+ return true;
876
+ i += 1;
877
+ }
878
+ }
879
+ return false;
880
+ }
881
+ function matchesAt(matcher, ch) {
882
+ switch (matcher.kind) {
883
+ case "literal":
884
+ return matcher.ch === ch;
885
+ case "any":
886
+ return true;
887
+ case "class":
888
+ return charInClassBody(ch, matcher.body) !== matcher.negate;
889
+ case "star":
890
+ return false;
891
+ }
892
+ }
893
+ function matchGlobSegment(matchers, name) {
894
+ let mi = 0;
895
+ let ni = 0;
896
+ let starMi = -1;
897
+ let starNi = -1;
898
+ while (ni < name.length) {
899
+ const matcher = matchers[mi];
900
+ if (matcher && matcher.kind === "star") {
901
+ starMi = mi;
902
+ starNi = ni;
903
+ mi++;
904
+ } else if (matcher && matchesAt(matcher, name[ni])) {
905
+ mi++;
906
+ ni++;
907
+ } else if (starMi !== -1) {
908
+ mi = starMi + 1;
909
+ starNi++;
910
+ ni = starNi;
911
+ } else {
912
+ return false;
913
+ }
914
+ }
915
+ while (matchers[mi]?.kind === "star")
916
+ mi++;
917
+ return mi === matchers.length;
918
+ }
919
+ function compileNamePatterns(patterns) {
920
+ const literals = new Set;
921
+ const globs = [];
922
+ for (const pattern of patterns) {
923
+ const lower = pattern.toLowerCase();
924
+ if (GLOB_CHARS.test(lower)) {
925
+ globs.push(parseGlobSegment(lower));
926
+ } else {
927
+ literals.add(lower);
928
+ }
929
+ }
930
+ return {
931
+ matches(name) {
932
+ const lower = name.toLowerCase();
933
+ return literals.has(lower) || globs.some((matchers) => matchGlobSegment(matchers, lower));
934
+ }
935
+ };
936
+ }
937
+ var GLOB_CHARS;
938
+ var init_name_pattern = __esm(() => {
939
+ GLOB_CHARS = /[*?[\]]/;
940
+ });
941
+
838
942
  // web-src/server/runtime.ts
839
943
  import { spawn, spawnSync } from "node:child_process";
840
944
  import { createReadStream, promises as fs } from "node:fs";
@@ -2110,7 +2214,7 @@ function sortTreeEntries(entries) {
2110
2214
  function omittedWorktreeDirectoryReason(name, omitDirNames) {
2111
2215
  if (name === ".git")
2112
2216
  return "internal";
2113
- return omitDirNames.has(name) ? "heavy" : undefined;
2217
+ return omitDirNames.matches(name) ? "heavy" : undefined;
2114
2218
  }
2115
2219
  function worktreeSubmodulePaths(cwd) {
2116
2220
  if (!existsSync(join3(cwd, ".gitmodules")))
@@ -2137,7 +2241,7 @@ async function worktreeSubmodulePathsAsync(cwd) {
2137
2241
  }).filter(Boolean));
2138
2242
  }
2139
2243
  function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames, submodulePaths) {
2140
- if (excludeNames.has(name.toLowerCase()))
2244
+ if (excludeNames.matches(name))
2141
2245
  return {
2142
2246
  name,
2143
2247
  path: "",
@@ -2157,8 +2261,8 @@ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, exc
2157
2261
  function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES, excludeNames = []) {
2158
2262
  const base = normalizeTreePath(path);
2159
2263
  const root = join3(cwd, base);
2160
- const omitDirNameSet = new Set(omitDirNames);
2161
- const excludeNameSet = new Set(excludeNames.map((name) => name.toLowerCase()));
2264
+ const omitDirNameSet = compileNamePatterns(omitDirNames);
2265
+ const excludeNameSet = compileNamePatterns(excludeNames);
2162
2266
  const submodulePaths = worktreeSubmodulePaths(cwd);
2163
2267
  let directEntries;
2164
2268
  try {
@@ -2200,7 +2304,7 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
2200
2304
  return;
2201
2305
  }
2202
2306
  for (const entry of entries) {
2203
- if (excludeNameSet.has(entry.name.toLowerCase()))
2307
+ if (excludeNameSet.matches(entry.name))
2204
2308
  continue;
2205
2309
  const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
2206
2310
  const full = join3(dir, entry.name);
@@ -2236,8 +2340,8 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
2236
2340
  async function worktreeFilesystemEntriesAsync(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES, excludeNames = []) {
2237
2341
  const base = normalizeTreePath(path);
2238
2342
  const root = join3(cwd, base);
2239
- const omitDirNameSet = new Set(omitDirNames);
2240
- const excludeNameSet = new Set(excludeNames.map((name) => name.toLowerCase()));
2343
+ const omitDirNameSet = compileNamePatterns(omitDirNames);
2344
+ const excludeNameSet = compileNamePatterns(excludeNames);
2241
2345
  const submodulePaths = await worktreeSubmodulePathsAsync(cwd);
2242
2346
  let directEntries;
2243
2347
  try {
@@ -2287,7 +2391,7 @@ async function worktreeFilesystemEntriesAsync(cwd, path, recursive, omitDirNames
2287
2391
  return;
2288
2392
  }
2289
2393
  for (const entry of entries) {
2290
- if (excludeNameSet.has(entry.name.toLowerCase()))
2394
+ if (excludeNameSet.matches(entry.name))
2291
2395
  continue;
2292
2396
  const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
2293
2397
  const full = join3(dir, entry.name);
@@ -2726,6 +2830,7 @@ function truncateToNHunks(diffText, n, maxLines = Number.POSITIVE_INFINITY) {
2726
2830
  var BLAME_ZERO_SHA = "0000000000000000000000000000000000000000", WORKTREE_RECURSIVE_DEPTH_LIMIT = 32, WORKTREE_RECURSIVE_ENTRY_LIMIT = 50000, DEFAULT_REF_COMMIT_LIMIT = 100, MAX_REF_COMMIT_LIMIT = 500, COMMIT_FORMAT = "%H%x00%s%x00%an%x00%aI", ALWAYS_WORKTREE_OMIT_DIR_NAMES, DEFAULT_WORKTREE_OMIT_DIR_NAMES, GIT_COMMAND_TIMEOUT_MS = 20000, HISTORY_FORMAT = "%H%x00%s%x00%an%x00%aI%x00%P%x00%b", MAX_HISTORY_LIMIT = 200;
2727
2831
  var init_git = __esm(() => {
2728
2832
  init_command_resolver();
2833
+ init_name_pattern();
2729
2834
  init_runtime();
2730
2835
  ALWAYS_WORKTREE_OMIT_DIR_NAMES = [".devbox", ".direnv"];
2731
2836
  DEFAULT_WORKTREE_OMIT_DIR_NAMES = [
@@ -8766,11 +8871,11 @@ function normalizeGrepMax(value) {
8766
8871
  return Math.min(parsed, GREP_ABSOLUTE_MAX);
8767
8872
  }
8768
8873
  function isSkippableSearchPath(path, omitDirNames = [], excludeNames = []) {
8769
- const omitDirs = new Set(omitDirNames.map((name) => name.toLowerCase()));
8770
- const excluded = new Set(excludeNames.map((name) => name.toLowerCase()));
8874
+ const omitDirs = compileNamePatterns(omitDirNames);
8875
+ const excluded = compileNamePatterns(excludeNames);
8771
8876
  return path.split(/[\\/]+/).some((part) => {
8772
8877
  const lower = part.toLowerCase();
8773
- return lower === ".git" || lower === ".code-viewer" || omitDirs.has(lower) || excluded.has(lower);
8878
+ return lower === ".git" || lower === ".code-viewer" || omitDirs.matches(part) || excluded.matches(part);
8774
8879
  });
8775
8880
  }
8776
8881
  function fixedStringLineMatches(path, text, query, max) {
@@ -8877,6 +8982,7 @@ function parseGitGrepOutput(stdout, ref, max, omitDirNames = [], excludeNames =
8877
8982
  }
8878
8983
  var GREP_DEFAULT_MAX = 200, GREP_ABSOLUTE_MAX = 500, GREP_MAX_FILE_BYTES, FILE_SEARCH_ABSOLUTE_MAX = 50000, DEFAULT_EXCLUDE_NAMES;
8879
8984
  var init_search = __esm(() => {
8985
+ init_name_pattern();
8880
8986
  GREP_MAX_FILE_BYTES = 2 * 1024 * 1024;
8881
8987
  DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
8882
8988
  });
@@ -23141,7 +23247,8 @@ async function rgAvailableAsync(cwd) {
23141
23247
  return rgAvailableCache;
23142
23248
  }
23143
23249
  function isExcludedScopePath(path, excludeNames) {
23144
- return path.split(/[\\/]+/).some((part) => excludeNames.some((name) => part.toLowerCase() === name.toLowerCase()));
23250
+ const excluded = compileNamePatterns(excludeNames);
23251
+ return path.split(/[\\/]+/).some((part) => excluded.matches(part));
23145
23252
  }
23146
23253
  function isSafePath(path) {
23147
23254
  if (!path || path.startsWith("/") || path.startsWith("\\") || path.includes("\x00"))
@@ -23340,6 +23447,7 @@ var init_search_service = __esm(() => {
23340
23447
  init_command_resolver();
23341
23448
  init_spawn_runner();
23342
23449
  init_git();
23450
+ init_name_pattern();
23343
23451
  init_search();
23344
23452
  });
23345
23453
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Local browser-based code and git diff viewer",
5
5
  "type": "module",
6
6
  "bin": {
package/web/app.js CHANGED
@@ -10480,6 +10480,80 @@ ${frontmatter.yaml}
10480
10480
  return true;
10481
10481
  }
10482
10482
 
10483
+ // web-src/views/database/detail-table.ts
10484
+ function createDetailTable(headers, rows, emptyText) {
10485
+ const table2 = document.createElement("table");
10486
+ table2.className = "db-detail-table";
10487
+ const thead = document.createElement("thead");
10488
+ const headRow = document.createElement("tr");
10489
+ for (const label of headers) {
10490
+ const th = document.createElement("th");
10491
+ th.textContent = label;
10492
+ headRow.appendChild(th);
10493
+ }
10494
+ thead.appendChild(headRow);
10495
+ table2.appendChild(thead);
10496
+ const tbody = document.createElement("tbody");
10497
+ if (rows.length === 0) {
10498
+ const row = document.createElement("tr");
10499
+ const td = document.createElement("td");
10500
+ td.colSpan = headers.length;
10501
+ td.className = "db-value-empty";
10502
+ td.textContent = emptyText;
10503
+ row.appendChild(td);
10504
+ tbody.appendChild(row);
10505
+ }
10506
+ for (const cells of rows) {
10507
+ const row = document.createElement("tr");
10508
+ cells.forEach((cellText, index) => {
10509
+ const td = document.createElement("td");
10510
+ td.textContent = cellText;
10511
+ td.className = index === 0 ? "db-detail-table-primary" : "db-detail-table-muted";
10512
+ row.appendChild(td);
10513
+ });
10514
+ tbody.appendChild(row);
10515
+ }
10516
+ table2.appendChild(tbody);
10517
+ return table2;
10518
+ }
10519
+
10520
+ // web-src/views/database/detail-tabs.ts
10521
+ function createDetailTabs(specs, initial, onSelect) {
10522
+ const tabsEl = document.createElement("div");
10523
+ tabsEl.className = "db-detail-tabs";
10524
+ const buttons = {};
10525
+ const bodies = {};
10526
+ let active = initial;
10527
+ function setActive(tab) {
10528
+ active = tab;
10529
+ for (const spec of specs) {
10530
+ buttons[spec.id].classList.toggle("active", spec.id === tab);
10531
+ bodies[spec.id].hidden = spec.id !== tab;
10532
+ }
10533
+ }
10534
+ for (const spec of specs) {
10535
+ const btn = document.createElement("button");
10536
+ btn.type = "button";
10537
+ btn.className = "db-detail-tab";
10538
+ btn.textContent = spec.label;
10539
+ btn.addEventListener("click", () => {
10540
+ setActive(spec.id);
10541
+ onSelect(spec.id);
10542
+ });
10543
+ buttons[spec.id] = btn;
10544
+ tabsEl.appendChild(btn);
10545
+ const body = document.createElement("div");
10546
+ body.className = "db-detail-tab-body";
10547
+ bodies[spec.id] = body;
10548
+ }
10549
+ setActive(initial);
10550
+ function setLabels(labels) {
10551
+ for (const spec of specs)
10552
+ buttons[spec.id].textContent = labels[spec.id];
10553
+ }
10554
+ return { tabsEl, bodies, getActive: () => active, setActive, setLabels };
10555
+ }
10556
+
10483
10557
  // web-src/views/database/i18n.ts
10484
10558
  var EN = {
10485
10559
  nav: {
@@ -10820,7 +10894,21 @@ ${frontmatter.yaml}
10820
10894
  copied: "Copied",
10821
10895
  copyFailed: "Copy failed",
10822
10896
  invalidAttributeValues: "Invalid attribute values JSON",
10823
- runQuery: "Run"
10897
+ runQuery: "Run",
10898
+ structureTab: "Structure",
10899
+ itemTab: "Item",
10900
+ selectTable: "Select a table to view its structure.",
10901
+ attributeHeader: "Attribute",
10902
+ typeHeader: "Type",
10903
+ keyRoleHeader: "Key",
10904
+ noAttributes: "(no attributes)",
10905
+ globalSecondaryIndexes: "Global secondary indexes",
10906
+ localSecondaryIndexes: "Local secondary indexes",
10907
+ projectionAll: "ALL",
10908
+ projectionKeysOnly: "KEYS_ONLY",
10909
+ projectionInclude: (attrs) => `INCLUDE (${attrs})`,
10910
+ keySchemaOnlyHint: "DynamoDB only enforces types for key attributes. Additional attributes will appear here once items are loaded.",
10911
+ inferredAttributesNote: (count) => `Attributes beyond the key schema are inferred from ${count.toLocaleString()} loaded item${count === 1 ? "" : "s"} and may not reflect every item.`
10824
10912
  }
10825
10913
  }
10826
10914
  };
@@ -11163,7 +11251,21 @@ ${frontmatter.yaml}
11163
11251
  copied: "コピーしました",
11164
11252
  copyFailed: "コピーに失敗しました",
11165
11253
  invalidAttributeValues: "属性値の JSON が不正です",
11166
- runQuery: "実行"
11254
+ runQuery: "実行",
11255
+ structureTab: "構造",
11256
+ itemTab: "アイテム",
11257
+ selectTable: "テーブルを選択すると構造が表示されます。",
11258
+ attributeHeader: "属性",
11259
+ typeHeader: "型",
11260
+ keyRoleHeader: "キー",
11261
+ noAttributes: "(属性がありません)",
11262
+ globalSecondaryIndexes: "グローバルセカンダリインデックス",
11263
+ localSecondaryIndexes: "ローカルセカンダリインデックス",
11264
+ projectionAll: "ALL",
11265
+ projectionKeysOnly: "KEYS_ONLY",
11266
+ projectionInclude: (attrs) => `INCLUDE (${attrs})`,
11267
+ keySchemaOnlyHint: "DynamoDBがスキーマとして強制するのはキー属性のみです。アイテムを読み込むと追加の属性がここに表示されます。",
11268
+ inferredAttributesNote: (count) => `キー以外の属性は、読み込み済みの${count.toLocaleString()}件のアイテムから検出したものです (全アイテムを網羅するとは限りません)。`
11167
11269
  }
11168
11270
  }
11169
11271
  };
@@ -11237,6 +11339,25 @@ ${frontmatter.yaml}
11237
11339
  out[k] = unwrapAttributeValue(v);
11238
11340
  return out;
11239
11341
  }
11342
+ var ATTRIBUTE_VALUE_TAG_ORDER = [
11343
+ "S",
11344
+ "N",
11345
+ "B",
11346
+ "BOOL",
11347
+ "NULL",
11348
+ "M",
11349
+ "L",
11350
+ "SS",
11351
+ "NS",
11352
+ "BS"
11353
+ ];
11354
+ function attributeValueTag(av) {
11355
+ for (const tag of ATTRIBUTE_VALUE_TAG_ORDER) {
11356
+ if (tag in av)
11357
+ return tag;
11358
+ }
11359
+ return "?";
11360
+ }
11240
11361
  function previewItem(item) {
11241
11362
  const entries = Object.entries(item).slice(0, 6);
11242
11363
  const parts = entries.map(([key, rawValue]) => {
@@ -11347,8 +11468,17 @@ ${frontmatter.yaml}
11347
11468
  moreBtn.hidden = true;
11348
11469
  itemListPane.appendChild(moreBtn);
11349
11470
  const detailPane = document.createElement("div");
11350
- detailPane.className = "dynamodb-detail-pane";
11351
- setPaneEmpty(detailPane, text3().dynamodb.selectItem);
11471
+ detailPane.className = "db-detail-pane";
11472
+ const detailTabs = createDetailTabs([
11473
+ { id: "structure", label: text3().dynamodb.structureTab },
11474
+ { id: "item", label: text3().dynamodb.itemTab }
11475
+ ], "structure", () => notifySelectionChange());
11476
+ detailPane.appendChild(detailTabs.tabsEl);
11477
+ const structureBody = detailTabs.bodies.structure;
11478
+ setPaneEmpty(structureBody, text3().dynamodb.selectTable);
11479
+ const itemBody = detailTabs.bodies.item;
11480
+ setPaneEmpty(itemBody, text3().dynamodb.selectItem);
11481
+ detailPane.append(structureBody, itemBody);
11352
11482
  container.append(itemListPane, detailPane);
11353
11483
  let currentDbId = null;
11354
11484
  let currentTable = null;
@@ -11360,6 +11490,7 @@ ${frontmatter.yaml}
11360
11490
  let currentItemKeyToken = null;
11361
11491
  let cumulativeShownCount = 0;
11362
11492
  let cumulativeScannedCount = 0;
11493
+ let lastRenderedItem = null;
11363
11494
  let disposed = false;
11364
11495
  let loadRunId = 0;
11365
11496
  let itemRunId = 0;
@@ -11384,6 +11515,9 @@ ${frontmatter.yaml}
11384
11515
  queryModeBtn.classList.toggle("active", mode === "query");
11385
11516
  keyConditionInput.hidden = mode !== "query";
11386
11517
  }
11518
+ function setDetailTab(tab) {
11519
+ detailTabs.setActive(tab);
11520
+ }
11387
11521
  function renderTables(tableNames, append = false) {
11388
11522
  if (!append) {
11389
11523
  tableList.innerHTML = "";
@@ -11436,10 +11570,51 @@ ${frontmatter.yaml}
11436
11570
  }
11437
11571
  itemList.appendChild(fragment);
11438
11572
  }
11439
- function renderTableInfo() {
11440
- detailPane.innerHTML = "";
11441
- if (!currentTableInfo)
11573
+ function projectionText(projection) {
11574
+ const t2 = text3().dynamodb;
11575
+ if (!projection?.ProjectionType)
11576
+ return "";
11577
+ if (projection.ProjectionType === "ALL")
11578
+ return t2.projectionAll;
11579
+ if (projection.ProjectionType === "KEYS_ONLY")
11580
+ return t2.projectionKeysOnly;
11581
+ return t2.projectionInclude((projection.NonKeyAttributes ?? []).join(", "));
11582
+ }
11583
+ function renderSecondaryIndexes(label, indexes) {
11584
+ if (!indexes || indexes.length === 0)
11442
11585
  return;
11586
+ const section = document.createElement("div");
11587
+ section.className = "dynamodb-index-section";
11588
+ const heading2 = document.createElement("div");
11589
+ heading2.className = "dynamodb-index-heading";
11590
+ heading2.textContent = label;
11591
+ section.appendChild(heading2);
11592
+ for (const ix of indexes) {
11593
+ const row = document.createElement("div");
11594
+ row.className = "dynamodb-index-row";
11595
+ const name = document.createElement("div");
11596
+ name.className = "dynamodb-index-name";
11597
+ name.textContent = ix.IndexName ?? "";
11598
+ const detail = document.createElement("div");
11599
+ detail.className = "dynamodb-index-detail";
11600
+ const keyPart = (ix.KeySchema ?? []).map((k) => `${k.AttributeName} (${k.KeyType})`).join(", ");
11601
+ detail.textContent = [
11602
+ keyPart,
11603
+ projectionText(ix.Projection),
11604
+ ix.ItemCount !== undefined ? `${ix.ItemCount.toLocaleString()} items` : undefined
11605
+ ].filter(Boolean).join(" / ");
11606
+ row.append(name, detail);
11607
+ section.appendChild(row);
11608
+ }
11609
+ structureBody.appendChild(section);
11610
+ }
11611
+ function renderTableStructure() {
11612
+ structureBody.innerHTML = "";
11613
+ if (!currentTableInfo) {
11614
+ setPaneEmpty(structureBody, text3().dynamodb.selectTable);
11615
+ return;
11616
+ }
11617
+ const t2 = text3().dynamodb;
11443
11618
  const header = document.createElement("div");
11444
11619
  header.className = "dynamodb-table-info-header";
11445
11620
  header.textContent = currentTableInfo.TableName ?? currentTable ?? "";
@@ -11447,26 +11622,54 @@ ${frontmatter.yaml}
11447
11622
  meta.className = "dynamodb-table-info-meta";
11448
11623
  meta.textContent = [
11449
11624
  currentTableInfo.TableStatus,
11450
- currentTableInfo.ItemCount !== undefined ? `${currentTableInfo.ItemCount.toLocaleString()} items` : undefined
11625
+ currentTableInfo.ItemCount !== undefined ? `${currentTableInfo.ItemCount.toLocaleString()} items` : undefined,
11626
+ currentTableInfo.TableSizeBytes !== undefined ? formatBytes(currentTableInfo.TableSizeBytes) : undefined,
11627
+ currentTableInfo.BillingModeSummary?.BillingMode
11451
11628
  ].filter(Boolean).join(" / ");
11452
- detailPane.append(header, meta);
11453
- const keySchema = currentTableInfo.KeySchema ?? [];
11454
- if (keySchema.length > 0) {
11455
- const keyList = document.createElement("div");
11456
- keyList.className = "dynamodb-table-info-keys";
11457
- keyList.textContent = keySchema.map((k) => `${k.AttributeName} (${k.KeyType})`).join(", ");
11458
- detailPane.appendChild(keyList);
11459
- }
11460
- const empty = document.createElement("div");
11461
- empty.className = "db-pane-empty dynamodb-table-info-empty";
11462
- const emptyTitle = document.createElement("div");
11463
- emptyTitle.className = "db-pane-empty-title";
11464
- emptyTitle.textContent = text3().dynamodb.selectItem;
11465
- empty.appendChild(emptyTitle);
11466
- detailPane.appendChild(empty);
11629
+ structureBody.append(header, meta);
11630
+ const attributeTypeByName = new Map((currentTableInfo.AttributeDefinitions ?? []).map((a2) => [
11631
+ a2.AttributeName,
11632
+ a2.AttributeType
11633
+ ]));
11634
+ const keyRoleByName = new Map((currentTableInfo.KeySchema ?? []).map((k) => [
11635
+ k.AttributeName,
11636
+ k.KeyType
11637
+ ]));
11638
+ const attrNames = [...attributeTypeByName.keys()];
11639
+ const inferredTypesByName = new Map;
11640
+ for (const item of itemsByKeyToken.values()) {
11641
+ for (const [attrName, av] of Object.entries(item)) {
11642
+ if (attributeTypeByName.has(attrName))
11643
+ continue;
11644
+ const set2 = inferredTypesByName.get(attrName) ?? new Set;
11645
+ set2.add(attributeValueTag(av));
11646
+ inferredTypesByName.set(attrName, set2);
11647
+ }
11648
+ }
11649
+ const inferredNames = [...inferredTypesByName.keys()].sort();
11650
+ const rows = [
11651
+ ...attrNames.map((name) => [
11652
+ name,
11653
+ attributeTypeByName.get(name) ?? "",
11654
+ keyRoleByName.get(name) ?? ""
11655
+ ]),
11656
+ ...inferredNames.map((name) => [
11657
+ name,
11658
+ [...inferredTypesByName.get(name) ?? []].join(", "),
11659
+ ""
11660
+ ])
11661
+ ];
11662
+ structureBody.appendChild(createDetailTable([t2.attributeHeader, t2.typeHeader, t2.keyRoleHeader], rows, t2.noAttributes));
11663
+ const note = document.createElement("div");
11664
+ note.className = "dynamodb-attr-note";
11665
+ note.textContent = inferredNames.length === 0 ? t2.keySchemaOnlyHint : t2.inferredAttributesNote(itemsByKeyToken.size);
11666
+ structureBody.appendChild(note);
11667
+ renderSecondaryIndexes(t2.globalSecondaryIndexes, currentTableInfo.GlobalSecondaryIndexes);
11668
+ renderSecondaryIndexes(t2.localSecondaryIndexes, currentTableInfo.LocalSecondaryIndexes);
11467
11669
  }
11468
11670
  function renderItemDetail(item) {
11469
- detailPane.innerHTML = "";
11671
+ lastRenderedItem = item;
11672
+ itemBody.innerHTML = "";
11470
11673
  const header = document.createElement("div");
11471
11674
  header.className = "dynamodb-item-detail-header";
11472
11675
  const title = document.createElement("span");
@@ -11489,7 +11692,7 @@ ${frontmatter.yaml}
11489
11692
  }
11490
11693
  });
11491
11694
  header.append(title, copyBtn, copyStatus);
11492
- detailPane.appendChild(header);
11695
+ itemBody.appendChild(header);
11493
11696
  const pre = document.createElement("pre");
11494
11697
  pre.className = "dynamodb-item-source";
11495
11698
  try {
@@ -11497,12 +11700,13 @@ ${frontmatter.yaml}
11497
11700
  } catch {
11498
11701
  pre.textContent = String(item);
11499
11702
  }
11500
- detailPane.appendChild(pre);
11703
+ itemBody.appendChild(pre);
11501
11704
  }
11502
11705
  function selectItem(item, token) {
11503
11706
  currentItemKeyToken = token ?? itemKeyToken(extractItemKey(item, currentTableInfo?.KeySchema));
11504
11707
  highlightActiveItem(currentItemKeyToken);
11505
11708
  renderItemDetail(item);
11709
+ setDetailTab("item");
11506
11710
  notifySelectionChange();
11507
11711
  }
11508
11712
  async function loadItems(append) {
@@ -11538,8 +11742,6 @@ ${frontmatter.yaml}
11538
11742
  cumulativeShownCount = 0;
11539
11743
  cumulativeScannedCount = 0;
11540
11744
  setPaneStatus(itemList, "Loading items...");
11541
- if (!currentTableInfo)
11542
- setPaneEmpty(detailPane, text3().dynamodb.selectItem);
11543
11745
  }
11544
11746
  try {
11545
11747
  const params = new URLSearchParams({
@@ -11581,6 +11783,8 @@ ${frontmatter.yaml}
11581
11783
  } else {
11582
11784
  appendItems(data.items);
11583
11785
  }
11786
+ if (currentTableInfo)
11787
+ renderTableStructure();
11584
11788
  currentNextToken = data.lastEvaluatedKey;
11585
11789
  moreBtn.hidden = !data.lastEvaluatedKey;
11586
11790
  cumulativeShownCount += data.items.length;
@@ -11602,21 +11806,29 @@ ${frontmatter.yaml}
11602
11806
  return;
11603
11807
  const slot = tableInfoGuard.start();
11604
11808
  const requestDbId = currentDbId;
11809
+ const isStaleRequest = () => disposed || slot.isStale() || requestDbId !== currentDbId || currentTable !== table2;
11605
11810
  try {
11606
11811
  const params = new URLSearchParams({ db: requestDbId, table: table2 });
11607
11812
  const res = await trackLoad(fetch(`/_db/dynamodb/table?${params}`, { signal: slot.signal }));
11608
- if (disposed || slot.isStale())
11813
+ if (isStaleRequest())
11609
11814
  return;
11610
- if (!res.ok)
11815
+ if (!res.ok) {
11816
+ const errText = await res.text();
11817
+ setPaneStatus(structureBody, `Error: ${errText || res.statusText}`, {
11818
+ error: true
11819
+ });
11611
11820
  return;
11821
+ }
11612
11822
  const data = await res.json();
11613
- if (disposed || slot.isStale() || requestDbId !== currentDbId || currentTable !== table2) {
11823
+ if (isStaleRequest())
11614
11824
  return;
11615
- }
11616
11825
  currentTableInfo = data.table;
11617
- if (!currentItemKeyToken)
11618
- renderTableInfo();
11619
- } catch {} finally {
11826
+ renderTableStructure();
11827
+ } catch (err) {
11828
+ if (isStaleRequest())
11829
+ return;
11830
+ setPaneStatus(structureBody, `Error: ${err instanceof Error ? err.message : String(err)}`, { error: true });
11831
+ } finally {
11620
11832
  slot.finish();
11621
11833
  }
11622
11834
  }
@@ -11656,7 +11868,14 @@ ${frontmatter.yaml}
11656
11868
  currentNextToken = undefined;
11657
11869
  highlightActiveTable(name);
11658
11870
  notifySelectionChange();
11659
- setPaneEmpty(detailPane, text3().dynamodb.selectItem);
11871
+ lastRenderedItem = null;
11872
+ itemsByKeyToken.clear();
11873
+ itemRowsByKeyToken.clear();
11874
+ structureBody.innerHTML = "";
11875
+ setPaneStatus(structureBody, "Loading table...");
11876
+ itemBody.innerHTML = "";
11877
+ setPaneEmpty(itemBody, text3().dynamodb.selectItem);
11878
+ setDetailTab("structure");
11660
11879
  await fetchTableInfo(name);
11661
11880
  await loadItems(false);
11662
11881
  }
@@ -11773,7 +11992,12 @@ ${frontmatter.yaml}
11773
11992
  activeItemRow = null;
11774
11993
  moreBtn.hidden = true;
11775
11994
  tableMoreBtn.hidden = true;
11776
- setPaneEmpty(detailPane, text3().dynamodb.selectItem);
11995
+ lastRenderedItem = null;
11996
+ structureBody.innerHTML = "";
11997
+ setPaneEmpty(structureBody, text3().dynamodb.selectTable);
11998
+ itemBody.innerHTML = "";
11999
+ setPaneEmpty(itemBody, text3().dynamodb.selectItem);
12000
+ setDetailTab("structure");
11777
12001
  setPaneStatus(tableList, "Loading tables...");
11778
12002
  try {
11779
12003
  const res = await trackLoad(fetch(`/_db/dynamodb/tables?db=${encodeURIComponent(dbId)}`, {
@@ -11809,6 +12033,8 @@ ${frontmatter.yaml}
11809
12033
  await selectItemByKey(key);
11810
12034
  } catch {}
11811
12035
  }
12036
+ if (initial?.detailTab)
12037
+ setDetailTab(initial.detailTab);
11812
12038
  } finally {
11813
12039
  suppressNotify = false;
11814
12040
  }
@@ -11854,7 +12080,12 @@ ${frontmatter.yaml}
11854
12080
  itemRowsByKeyToken.clear();
11855
12081
  activeItemRow = null;
11856
12082
  moreBtn.hidden = true;
11857
- setPaneEmpty(detailPane, text3().dynamodb.selectItem);
12083
+ lastRenderedItem = null;
12084
+ structureBody.innerHTML = "";
12085
+ setPaneEmpty(structureBody, text3().dynamodb.selectTable);
12086
+ itemBody.innerHTML = "";
12087
+ setPaneEmpty(itemBody, text3().dynamodb.selectItem);
12088
+ setDetailTab("structure");
11858
12089
  }
11859
12090
  setMode("scan");
11860
12091
  function getSelection() {
@@ -11865,7 +12096,8 @@ ${frontmatter.yaml}
11865
12096
  filterExpression: filterInput.value.trim() || undefined,
11866
12097
  expressionAttributeValues: attributeValuesInput.value.trim() || undefined,
11867
12098
  scanIndexForward: currentMode === "query" ? currentScanIndexForward : undefined,
11868
- itemKey: currentItemKeyToken ?? undefined
12099
+ itemKey: currentItemKeyToken ?? undefined,
12100
+ detailTab: detailTabs.getActive()
11869
12101
  };
11870
12102
  }
11871
12103
  function dispose() {
@@ -11883,12 +12115,18 @@ ${frontmatter.yaml}
11883
12115
  runBtn.textContent = t2.dynamodb.runQuery;
11884
12116
  tableMoreBtn.textContent = t2.common.loadMore;
11885
12117
  moreBtn.textContent = t2.common.loadMore;
11886
- if (!activeItemRow) {
11887
- if (currentTableInfo)
11888
- renderTableInfo();
11889
- else
11890
- setPaneEmpty(detailPane, t2.dynamodb.selectItem);
11891
- }
12118
+ detailTabs.setLabels({
12119
+ structure: t2.dynamodb.structureTab,
12120
+ item: t2.dynamodb.itemTab
12121
+ });
12122
+ if (currentTableInfo)
12123
+ renderTableStructure();
12124
+ else
12125
+ setPaneEmpty(structureBody, t2.dynamodb.selectTable);
12126
+ if (lastRenderedItem)
12127
+ renderItemDetail(lastRenderedItem);
12128
+ else
12129
+ setPaneEmpty(itemBody, t2.dynamodb.selectItem);
11892
12130
  }
11893
12131
  return {
11894
12132
  el: container,
@@ -11951,25 +12189,15 @@ ${frontmatter.yaml}
11951
12189
  docMoreBtn.hidden = true;
11952
12190
  docListPane.appendChild(docMoreBtn);
11953
12191
  const detailPane = document.createElement("div");
11954
- detailPane.className = "es-detail-pane";
11955
- const detailTabs = document.createElement("div");
11956
- detailTabs.className = "es-detail-tabs";
11957
- const tabMapping = document.createElement("button");
11958
- tabMapping.type = "button";
11959
- tabMapping.className = "es-detail-tab active";
11960
- tabMapping.textContent = text3().es.mapping;
11961
- const tabDoc = document.createElement("button");
11962
- tabDoc.type = "button";
11963
- tabDoc.className = "es-detail-tab";
11964
- tabDoc.textContent = text3().es.doc;
11965
- detailTabs.append(tabMapping, tabDoc);
11966
- detailPane.appendChild(detailTabs);
11967
- const mappingBody = document.createElement("div");
11968
- mappingBody.className = "es-mapping-body";
12192
+ detailPane.className = "db-detail-pane";
12193
+ const detailTabs = createDetailTabs([
12194
+ { id: "mapping", label: text3().es.mapping },
12195
+ { id: "doc", label: text3().es.doc }
12196
+ ], "mapping", () => {});
12197
+ detailPane.appendChild(detailTabs.tabsEl);
12198
+ const mappingBody = detailTabs.bodies.mapping;
11969
12199
  setPaneEmpty(mappingBody, text3().es.selectIndex);
11970
- const docBody = document.createElement("div");
11971
- docBody.className = "es-doc-body";
11972
- docBody.hidden = true;
12200
+ const docBody = detailTabs.bodies.doc;
11973
12201
  setPaneEmpty(docBody, text3().es.selectDoc);
11974
12202
  detailPane.append(mappingBody, docBody);
11975
12203
  container.append(docListPane, detailPane);
@@ -12094,13 +12322,8 @@ ${frontmatter.yaml}
12094
12322
  }
12095
12323
  function setDetailTab(tab) {
12096
12324
  detailTab = tab;
12097
- tabMapping.classList.toggle("active", tab === "mapping");
12098
- tabDoc.classList.toggle("active", tab === "doc");
12099
- mappingBody.hidden = tab !== "mapping";
12100
- docBody.hidden = tab !== "doc";
12325
+ detailTabs.setActive(tab);
12101
12326
  }
12102
- tabMapping.addEventListener("click", () => setDetailTab("mapping"));
12103
- tabDoc.addEventListener("click", () => setDetailTab("doc"));
12104
12327
  function renderMapping(resp) {
12105
12328
  lastMapping = resp;
12106
12329
  mappingBody.innerHTML = "";
@@ -12108,43 +12331,13 @@ ${frontmatter.yaml}
12108
12331
  header.className = "es-mapping-header";
12109
12332
  header.textContent = resp.mapping.index;
12110
12333
  mappingBody.appendChild(header);
12111
- const table2 = document.createElement("table");
12112
- table2.className = "es-mapping-table";
12113
- const thead = document.createElement("thead");
12114
- const headRow = document.createElement("tr");
12115
- for (const label of [text3().es.fieldHeader, text3().es.typeHeader]) {
12116
- const th = document.createElement("th");
12117
- th.textContent = label;
12118
- headRow.appendChild(th);
12119
- }
12120
- thead.appendChild(headRow);
12121
- table2.appendChild(thead);
12122
- const tbody = document.createElement("tbody");
12123
12334
  const props = resp.mapping.properties;
12124
12335
  const keys = Object.keys(props).sort();
12125
- if (keys.length === 0) {
12126
- const row = document.createElement("tr");
12127
- const td = document.createElement("td");
12128
- td.colSpan = 2;
12129
- td.className = "es-value-empty";
12130
- td.textContent = text3().es.noMappedFields;
12131
- row.appendChild(td);
12132
- tbody.appendChild(row);
12133
- }
12134
- for (const key of keys) {
12135
- const row = document.createElement("tr");
12136
- const fieldTd = document.createElement("td");
12137
- fieldTd.className = "es-mapping-field";
12138
- fieldTd.textContent = key;
12139
- const typeTd = document.createElement("td");
12140
- typeTd.className = "es-mapping-type";
12336
+ const rows = keys.map((key) => {
12141
12337
  const p2 = props[key];
12142
- typeTd.textContent = p2.type ?? (p2.properties ? "object" : "(unknown)");
12143
- row.append(fieldTd, typeTd);
12144
- tbody.appendChild(row);
12145
- }
12146
- table2.appendChild(tbody);
12147
- mappingBody.appendChild(table2);
12338
+ return [key, p2.type ?? (p2.properties ? "object" : "(unknown)")];
12339
+ });
12340
+ mappingBody.appendChild(createDetailTable([text3().es.fieldHeader, text3().es.typeHeader], rows, text3().es.noMappedFields));
12148
12341
  }
12149
12342
  function mkBtn(label, cls) {
12150
12343
  const b2 = document.createElement("button");
@@ -12649,11 +12842,10 @@ ${frontmatter.yaml}
12649
12842
  searchInput.placeholder = t2.es.queryPlaceholder;
12650
12843
  searchBtn.textContent = t2.common.search;
12651
12844
  docMoreBtn.textContent = t2.common.loadMore;
12652
- tabMapping.textContent = t2.es.mapping;
12653
- tabDoc.textContent = t2.es.doc;
12845
+ detailTabs.setLabels({ mapping: t2.es.mapping, doc: t2.es.doc });
12654
12846
  if (!currentIndex) {
12655
12847
  setPaneEmpty(mappingBody, t2.es.selectIndex);
12656
- } else if (lastMapping && mappingBody.querySelector(".es-mapping-table")) {
12848
+ } else if (lastMapping && mappingBody.querySelector(".db-detail-table")) {
12657
12849
  renderMapping(lastMapping);
12658
12850
  }
12659
12851
  if (!activeDocRow) {
@@ -21430,11 +21622,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21430
21622
  dbSelect.value = target;
21431
21623
  currentDbInfo = files.find((f2) => f2.id === target) || null;
21432
21624
  syncConnectionActions();
21625
+ const forceReload = options.forceReload ?? false;
21433
21626
  const explorerInitial = {
21434
- redis: pendingRedisInitial,
21435
- es: pendingEsInitial,
21436
- s3: pendingS3Initial,
21437
- dynamodb: pendingDynamodbInitial
21627
+ redis: pendingRedisInitial ?? (forceReload && currentDbInfo?.kind === "redis" ? redisExplorer.getSelection() : undefined),
21628
+ es: pendingEsInitial ?? (forceReload && currentDbInfo?.kind === "elasticsearch" ? esExplorer.getSelection() : undefined),
21629
+ s3: pendingS3Initial ?? (forceReload && currentDbInfo?.kind === "s3" ? s3Explorer.getSelection() : undefined),
21630
+ dynamodb: pendingDynamodbInitial ?? (forceReload && currentDbInfo?.kind === "dynamodb" ? dynamodbExplorer.getSelection() : undefined)
21438
21631
  };
21439
21632
  pendingRedisInitial = undefined;
21440
21633
  pendingEsInitial = undefined;
@@ -21567,7 +21760,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21567
21760
  }
21568
21761
  } else if (currentDbInfo?.kind === "dynamodb") {
21569
21762
  const sel = dynamodbExplorer.getSelection();
21570
- if (sel.table !== undefined || sel.mode !== "scan" || sel.keyConditionExpression !== undefined || sel.filterExpression !== undefined || sel.expressionAttributeValues !== undefined || sel.itemKey !== undefined) {
21763
+ if (sel.table !== undefined || sel.mode !== "scan" || sel.keyConditionExpression !== undefined || sel.filterExpression !== undefined || sel.expressionAttributeValues !== undefined || sel.itemKey !== undefined || sel.detailTab === "item") {
21571
21764
  state.dynamodb = sel;
21572
21765
  }
21573
21766
  }
@@ -22632,7 +22825,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22632
22825
  if (!mounted || activeTabId !== id)
22633
22826
  return;
22634
22827
  const state = entry.pane.getState();
22635
- await entry.pane.enter(state.dbId ?? undefined, state.schema ?? undefined, state.table ?? undefined, state.view, { autoSelectFirst: state.dbId !== null });
22828
+ await entry.pane.enter(state.dbId ?? undefined, state.schema ?? undefined, state.table ?? undefined, state.view, { autoSelectFirst: state.dbId !== null, forceReload: true });
22636
22829
  if (!mounted || activeTabId !== id)
22637
22830
  return;
22638
22831
  refreshChipLabel(id);
@@ -26128,7 +26321,7 @@ code-viewer annotate add-db --db app.db --tab query \\
26128
26321
  ],
26129
26322
  [
26130
26323
  "DynamoDB / LocalStack",
26131
- "Detected when DynamoDB is enabled on a LocalStack compose service. List tables, inspect key schemas, scan or query items, follow pagination tokens, and open item details with a copyable key. Browsing is read-only."
26324
+ "Detected when DynamoDB is enabled on a LocalStack compose service. List tables, browse a Structure tab (key schema, GSI/LSI, and non-key attribute types inferred from loaded items), scan or query items, follow pagination tokens, and open item details with a copyable key. Browsing is read-only."
26132
26325
  ],
26133
26326
  [
26134
26327
  "S3 / MinIO / LocalStack",
@@ -26795,7 +26988,7 @@ code-viewer annotate add-db --db app.db --tab query \\
26795
26988
  ],
26796
26989
  [
26797
26990
  "DynamoDB / LocalStack",
26798
- "LocalStack の compose サービスで DynamoDB が有効な場合に検出。テーブル一覧、キースキーマ、scan / query、継続トークンによるページング、コピー可能なキー付きのアイテム詳細を表示します。閲覧専用です。"
26991
+ "LocalStack の compose サービスで DynamoDB が有効な場合に検出。テーブル一覧、構造タブ(キースキーマ・GSI/LSI・読み込み済みアイテムから推測した非キー属性の型)、scan / query、継続トークンによるページング、コピー可能なキー付きのアイテム詳細を表示します。閲覧専用です。"
26799
26992
  ],
26800
26993
  [
26801
26994
  "S3 / MinIO / LocalStack",
@@ -36172,9 +36365,9 @@ code-viewer query agent-help`
36172
36365
  displaySource: "Applies to all projects in this browser.",
36173
36366
  excludedDirectories: "Excluded directories",
36174
36367
  omitDirs: "Skip these directory names while browsing and searching",
36175
- omitDirsHelp: "Reads no contents inside these directories. Applies to the sidebar (Files), Ctrl+K (file search), Ctrl+G (grep), Datastores, and the file change watcher.",
36368
+ omitDirsHelp: "Reads no contents inside these directories. Applies to the sidebar (Files), Ctrl+K (file search), Ctrl+G (grep), Datastores, and the file change watcher. Supports gitignore-style wildcards (*, ?, [abc], [!abc]).",
36176
36369
  excludeNames: "Hide these file or directory names completely",
36177
- excludeNamesHelp: "Removes matching files or directories from the sidebar, search, and grep results entirely. Unlike Skip, the names themselves disappear from the UI.",
36370
+ excludeNamesHelp: "Removes matching files or directories from the sidebar, search, and grep results entirely. Unlike Skip, the names themselves disappear from the UI. Supports gitignore-style wildcards (*, ?, [abc], [!abc]).",
36178
36371
  reset: "Restore defaults",
36179
36372
  autosaveNote: "Changes save automatically.",
36180
36373
  scopeSource: (project, source) => `Saved for project "${project}" in this browser. Source: ${source}. Used by the sidebar, Ctrl+K, Ctrl+G, Datastores, and the file change watcher. Restore defaults removes the browser override.`,
@@ -36462,9 +36655,9 @@ code-viewer query agent-help`
36462
36655
  displaySource: "このブラウザのすべてのプロジェクトに適用されます。",
36463
36656
  excludedDirectories: "除外ディレクトリ",
36464
36657
  omitDirs: "閲覧と検索でスキップするディレクトリ名",
36465
- omitDirsHelp: "これらのディレクトリの中身は読み込みません。サイドバー(Files)・Ctrl+K(ファイル検索)・Ctrl+G(grep)・Datastores・File change watcher の5機能すべてに適用されます。",
36658
+ omitDirsHelp: "これらのディレクトリの中身は読み込みません。サイドバー(Files)・Ctrl+K(ファイル検索)・Ctrl+G(grep)・Datastores・File change watcher の5機能すべてに適用されます。gitignore方式のワイルドカード(*, ?, [abc], [!abc])に対応しています。",
36466
36659
  excludeNames: "完全に非表示にするファイル名またはディレクトリ名",
36467
- excludeNamesHelp: "リスト中の名前に一致するファイル/ディレクトリを、サイドバー・検索結果・grep 結果から完全に消します。Skip と違い、名前自体が UI に出なくなります。",
36660
+ excludeNamesHelp: "リスト中の名前に一致するファイル/ディレクトリを、サイドバー・検索結果・grep 結果から完全に消します。Skip と違い、名前自体が UI に出なくなります。gitignore方式のワイルドカード(*, ?, [abc], [!abc])に対応しています。",
36468
36661
  reset: "デフォルトに戻す",
36469
36662
  autosaveNote: "変更は自動で保存されます。",
36470
36663
  scopeSource: (project, source) => `このブラウザのプロジェクト "${project}" に保存されます。ソース: ${source}。サイドバー、Ctrl+K、Ctrl+G、Datastores、File change watcher で使われます。「デフォルトに戻す」でブラウザ側の上書きを削除します。`,
package/web/style.css CHANGED
@@ -11654,20 +11654,23 @@ body.db-resizing {
11654
11654
  cursor: default;
11655
11655
  color: var(--fg-muted);
11656
11656
  }
11657
- .es-detail-pane {
11657
+ /* .db-detail-* は es-explorer (mapping/doc) と dynamodb-explorer
11658
+ (structure/item) が共有する詳細ペイン/タブ切替/テーブルの共通スタイル。
11659
+ detail-tabs.ts / detail-table.ts のクラス名と対応する。 */
11660
+ .db-detail-pane {
11658
11661
  flex: 1;
11659
11662
  display: flex;
11660
11663
  flex-direction: column;
11661
11664
  overflow: hidden;
11662
11665
  background: var(--bg);
11663
11666
  }
11664
- .es-detail-tabs {
11667
+ .db-detail-tabs {
11665
11668
  display: flex;
11666
11669
  border-bottom: 1px solid var(--border);
11667
11670
  background: var(--bg-soft);
11668
11671
  flex-shrink: 0;
11669
11672
  }
11670
- .es-detail-tab {
11673
+ .db-detail-tab {
11671
11674
  padding: 6px 14px;
11672
11675
  border: none;
11673
11676
  background: transparent;
@@ -11676,12 +11679,11 @@ body.db-resizing {
11676
11679
  color: var(--fg-muted);
11677
11680
  border-bottom: 2px solid transparent;
11678
11681
  }
11679
- .es-detail-tab.active {
11682
+ .db-detail-tab.active {
11680
11683
  color: var(--fg);
11681
11684
  border-bottom-color: var(--accent);
11682
11685
  }
11683
- .es-mapping-body,
11684
- .es-doc-body {
11686
+ .db-detail-tab-body {
11685
11687
  flex: 1;
11686
11688
  overflow: auto;
11687
11689
  padding: 12px;
@@ -11753,26 +11755,27 @@ body.db-resizing {
11753
11755
  color: var(--fg-muted);
11754
11756
  margin-bottom: 8px;
11755
11757
  }
11756
- .es-mapping-table {
11758
+ .db-detail-table {
11757
11759
  width: 100%;
11758
11760
  border-collapse: collapse;
11759
11761
  font-family: var(--mono, monospace);
11760
11762
  font-size: var(--db-font-mono);
11763
+ margin-bottom: 16px;
11761
11764
  }
11762
- .es-mapping-table th,
11763
- .es-mapping-table td {
11765
+ .db-detail-table th,
11766
+ .db-detail-table td {
11764
11767
  text-align: left;
11765
11768
  padding: 4px 8px;
11766
11769
  border-bottom: 1px solid var(--border);
11767
11770
  }
11768
- .es-mapping-table th {
11771
+ .db-detail-table th {
11769
11772
  font-weight: 600;
11770
11773
  background: var(--bg-soft);
11771
11774
  }
11772
- .es-mapping-field {
11775
+ .db-detail-table-primary {
11773
11776
  font-weight: 500;
11774
11777
  }
11775
- .es-mapping-type {
11778
+ .db-detail-table-muted {
11776
11779
  color: var(--fg-muted);
11777
11780
  }
11778
11781
  .es-doc-source {
@@ -11782,7 +11785,7 @@ body.db-resizing {
11782
11785
  word-break: break-all;
11783
11786
  margin: 0;
11784
11787
  }
11785
- .es-value-empty {
11788
+ .db-value-empty {
11786
11789
  color: var(--fg-muted);
11787
11790
  font-size: var(--db-font-base);
11788
11791
  font-style: italic;
@@ -12472,13 +12475,6 @@ body.db-resizing {
12472
12475
  cursor: default;
12473
12476
  color: var(--fg-muted);
12474
12477
  }
12475
- .dynamodb-detail-pane {
12476
- flex: 1;
12477
- overflow: auto;
12478
- padding: 12px;
12479
- color: var(--fg);
12480
- background: var(--bg);
12481
- }
12482
12478
  .dynamodb-table-info-header,
12483
12479
  .dynamodb-item-detail-header {
12484
12480
  display: flex;
@@ -12491,8 +12487,7 @@ body.db-resizing {
12491
12487
  border-bottom: 1px solid var(--border);
12492
12488
  margin-bottom: 8px;
12493
12489
  }
12494
- .dynamodb-table-info-meta,
12495
- .dynamodb-table-info-keys {
12490
+ .dynamodb-table-info-meta {
12496
12491
  font-family: var(--mono, monospace);
12497
12492
  font-size: var(--db-font-sm);
12498
12493
  color: var(--fg-muted);
@@ -12504,8 +12499,35 @@ body.db-resizing {
12504
12499
  font-size: var(--db-font-sm);
12505
12500
  font-weight: 400;
12506
12501
  }
12507
- .dynamodb-table-info-empty {
12508
- margin-top: 24px;
12502
+ .dynamodb-index-section {
12503
+ margin-bottom: 16px;
12504
+ }
12505
+ .dynamodb-index-heading {
12506
+ font-weight: 600;
12507
+ font-size: var(--db-font-sm);
12508
+ color: var(--fg-muted);
12509
+ margin-bottom: 4px;
12510
+ }
12511
+ .dynamodb-index-row {
12512
+ padding: 4px 0;
12513
+ border-bottom: 1px solid var(--border);
12514
+ }
12515
+ .dynamodb-index-name {
12516
+ font-family: var(--mono, monospace);
12517
+ font-size: var(--db-font-mono);
12518
+ font-weight: 500;
12519
+ }
12520
+ .dynamodb-index-detail {
12521
+ font-family: var(--mono, monospace);
12522
+ font-size: var(--db-font-sm);
12523
+ color: var(--fg-muted);
12524
+ }
12525
+ .dynamodb-attr-note {
12526
+ margin-top: -4px;
12527
+ margin-bottom: 16px;
12528
+ font-size: var(--db-font-sm);
12529
+ color: var(--fg-muted);
12530
+ font-style: italic;
12509
12531
  }
12510
12532
  .dynamodb-item-source {
12511
12533
  font-family: var(--mono, monospace);