@youtyan/code-viewer 0.8.0 → 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
@@ -37,6 +37,8 @@ Requires Node.js 20 or newer when installed from npm. Development uses
37
37
  viewer.
38
38
  Local Supabase CLI (`supabase start`) Postgres projects are auto-discovered
39
39
  too, without needing a `docker-compose.yml`.
40
+ Table descriptions appear inside expanded table entries and in the Schema
41
+ tab header when the database provides them.
40
42
  - Read the built-in Help page for getting started, the `.code-viewer/`
41
43
  project files, AI annotations, datastores, the agent skill, and
42
44
  keybindings.
@@ -176,7 +178,9 @@ files or directory names completely.
176
178
 
177
179
  Scope settings control directory exclusions shared by the sidebar, Ctrl+K file
178
180
  palette, Ctrl+G grep palette, the Datastores browser, and the file change
179
- 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
180
184
  Settings is saved on the server under `.code-viewer/settings.json` (no
181
185
  separate project-level config file). `.DS_Store` and a broad set of
182
186
  build/cache directories (`node_modules`, `dist`, `build`, `.next`, `.turbo`,
@@ -247,9 +251,10 @@ concurrency via `_seq_no` / `_primary_term`), create new documents, and delete
247
251
  existing ones. Snapshots and diffs over `_search` iteration are supported.
248
252
 
249
253
  **DynamoDB** support: detect LocalStack services with DynamoDB enabled, list
250
- tables across paginated responses, inspect key schemas, scan or query items,
251
- page with `LastEvaluatedKey`, and open an item detail view with a copyable key.
252
- 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.
253
258
 
254
259
  **S3-compatible object storage** (MinIO, LocalStack): browse
255
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
  });
@@ -10882,7 +10988,7 @@ function buildExecInvocation(config, sql) {
10882
10988
  "-t",
10883
10989
  "-A",
10884
10990
  "-F",
10885
- "\t",
10991
+ PG_FIELD_SEPARATOR,
10886
10992
  "-R",
10887
10993
  PG_RECORD_SEPARATOR,
10888
10994
  "-v",
@@ -11226,14 +11332,14 @@ function decodeMysqlBatchField(value) {
11226
11332
  }
11227
11333
  return out;
11228
11334
  }
11229
- function splitTsvLine(line, decodeFields) {
11230
- const fields = line.split("\t");
11335
+ function splitTsvLine(line, decodeFields, fieldSeparator = "\t") {
11336
+ const fields = line.split(fieldSeparator);
11231
11337
  return decodeFields ? fields.map(decodeMysqlBatchField) : fields;
11232
11338
  }
11233
11339
  function stripFinalRecordSeparator(text, recordSeparator) {
11234
11340
  return text.endsWith(recordSeparator) ? text.slice(0, -recordSeparator.length) : text;
11235
11341
  }
11236
- function parseTsvOutput(stdout, hasHeader, recordSeparator) {
11342
+ function parseTsvOutput(stdout, hasHeader, recordSeparator, fieldSeparator) {
11237
11343
  const text = recordSeparator ? stripFinalRecordSeparator(stripFinalLineBreak(stdout), recordSeparator) : stripFinalLineBreak(stdout);
11238
11344
  if (text.length === 0)
11239
11345
  return { columns: [], rows: [] };
@@ -11241,11 +11347,11 @@ function parseTsvOutput(stdout, hasHeader, recordSeparator) {
11241
11347
  if (lines.length === 0)
11242
11348
  return { columns: [], rows: [] };
11243
11349
  if (hasHeader) {
11244
- const columns = splitTsvLine(lines[0], true);
11245
- const rows2 = lines.slice(1).map((line) => splitTsvLine(line, true));
11350
+ const columns = splitTsvLine(lines[0], true, fieldSeparator);
11351
+ const rows2 = lines.slice(1).map((line) => splitTsvLine(line, true, fieldSeparator));
11246
11352
  return { columns, rows: rows2 };
11247
11353
  }
11248
- const rows = lines.map((line) => splitTsvLine(line, false));
11354
+ const rows = lines.map((line) => splitTsvLine(line, false, fieldSeparator));
11249
11355
  return { columns: [], rows };
11250
11356
  }
11251
11357
  function isMysqlSpatialType(type) {
@@ -11315,7 +11421,7 @@ function createSqlCliAdapter(config) {
11315
11421
  if (result.code !== 0) {
11316
11422
  throw new Error(result.stderr.trim() || "query failed");
11317
11423
  }
11318
- return parseTsvOutput(result.stdout, config.kind === "mysql", config.kind === "postgresql" ? PG_RECORD_SEPARATOR : undefined);
11424
+ return parseTsvOutput(result.stdout, config.kind === "mysql", config.kind === "postgresql" ? PG_RECORD_SEPARATOR : undefined, config.kind === "postgresql" ? PG_FIELD_SEPARATOR : undefined);
11319
11425
  }
11320
11426
  function toDbValue(val) {
11321
11427
  if (val === "NULL" || val === "\\N")
@@ -11392,15 +11498,16 @@ function createSqlCliAdapter(config) {
11392
11498
  async getTablesAsync(signal) {
11393
11499
  let sql;
11394
11500
  if (config.kind === "postgresql") {
11395
- sql = `SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = ${postgresSchemaLiteral()} ORDER BY table_name`;
11501
+ sql = `SELECT t.table_name, t.table_type, COALESCE(obj_description(cls.oid, 'pg_class'), '') FROM information_schema.tables t JOIN pg_namespace n ON n.nspname = t.table_schema JOIN pg_class cls ON cls.relnamespace = n.oid AND cls.relname = t.table_name WHERE t.table_schema = ${postgresSchemaLiteral()} ORDER BY t.table_name`;
11396
11502
  } else {
11397
- sql = `SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_name`;
11503
+ sql = `SELECT table_name, table_type, table_comment FROM information_schema.tables WHERE table_schema = DATABASE() ORDER BY table_name`;
11398
11504
  }
11399
11505
  const result = await execAsync(sql, signal);
11400
11506
  return result.rows.map((row) => ({
11401
11507
  name: row[0],
11402
11508
  type: row[1] === "VIEW" ? "view" : "table",
11403
- rowCount: null
11509
+ rowCount: null,
11510
+ comment: row[2] || null
11404
11511
  }));
11405
11512
  },
11406
11513
  async getColumnsAsync(table, signal) {
@@ -11413,11 +11520,11 @@ function createSqlCliAdapter(config) {
11413
11520
  },
11414
11521
  async getIndexesAsync(signal) {
11415
11522
  let sql;
11416
- const INDEX_COL_SEP = "\x1F";
11523
+ const INDEX_COL_SEP = "\x1D";
11417
11524
  if (config.kind === "postgresql") {
11418
- sql = `SELECT i.relname, t.relname, CASE WHEN ix.indisunique THEN '1' ELSE '0' END, COALESCE(string_agg(a.attname, E'\\x1f' ORDER BY k.ord), '') FROM pg_index ix JOIN pg_class i ON i.oid = ix.indexrelid JOIN pg_class t ON t.oid = ix.indrelid JOIN pg_namespace n ON n.oid = t.relnamespace LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord) ON true LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum WHERE n.nspname = ${postgresSchemaLiteral()} AND i.relname NOT LIKE 'pg_%' GROUP BY i.relname, t.relname, ix.indisunique ORDER BY t.relname, i.relname`;
11525
+ sql = `SELECT i.relname, t.relname, CASE WHEN ix.indisunique THEN '1' ELSE '0' END, COALESCE(string_agg(a.attname, E'\\x1d' ORDER BY k.ord), '') FROM pg_index ix JOIN pg_class i ON i.oid = ix.indexrelid JOIN pg_class t ON t.oid = ix.indrelid JOIN pg_namespace n ON n.oid = t.relnamespace LEFT JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, ord) ON true LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum WHERE n.nspname = ${postgresSchemaLiteral()} AND i.relname NOT LIKE 'pg_%' GROUP BY i.relname, t.relname, ix.indisunique ORDER BY t.relname, i.relname`;
11419
11526
  } else {
11420
- sql = `SELECT index_name, table_name, IF(MAX(non_unique) = 0, '1', '0'), GROUP_CONCAT(column_name ORDER BY seq_in_index SEPARATOR '\x1F') FROM information_schema.statistics WHERE table_schema = DATABASE() GROUP BY index_name, table_name ORDER BY table_name, index_name`;
11527
+ sql = `SELECT index_name, table_name, IF(MAX(non_unique) = 0, '1', '0'), GROUP_CONCAT(column_name ORDER BY seq_in_index SEPARATOR '\x1D') FROM information_schema.statistics WHERE table_schema = DATABASE() GROUP BY index_name, table_name ORDER BY table_name, index_name`;
11421
11528
  }
11422
11529
  const result = await execAsync(sql, signal);
11423
11530
  return result.rows.map((row) => ({
@@ -11818,7 +11925,7 @@ async function listDockerDatabasesAsync(serviceName, kind, env, cwd, signal) {
11818
11925
  return fallback;
11819
11926
  return setDockerDatabasesCache(cacheKey, [], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
11820
11927
  }
11821
- const parsed = parseTsvOutput(result.stdout, kind === "mysql", kind === "postgresql" ? PG_RECORD_SEPARATOR : undefined);
11928
+ const parsed = parseTsvOutput(result.stdout, kind === "mysql", kind === "postgresql" ? PG_RECORD_SEPARATOR : undefined, kind === "postgresql" ? PG_FIELD_SEPARATOR : undefined);
11822
11929
  const dbs = parsed.rows.map((r) => r[0]).filter(Boolean);
11823
11930
  const value = dbs.length > 0 ? dbs : fallbackDockerDatabases(defaultDb);
11824
11931
  return setDockerDatabasesCache(cacheKey, value, value.length > 0 ? DOCKER_DATABASES_POSITIVE_TTL_MS : DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
@@ -11858,7 +11965,7 @@ async function fetchPostgresSchemasViaContainerAsync(config, cacheKey, now, sign
11858
11965
  if (result.code !== 0) {
11859
11966
  return setDockerSchemasCache(cacheKey, ["public"], DOCKER_DATABASES_NEGATIVE_TTL_MS, now);
11860
11967
  }
11861
- const parsed = parseTsvOutput(result.stdout, false, PG_RECORD_SEPARATOR);
11968
+ const parsed = parseTsvOutput(result.stdout, false, PG_RECORD_SEPARATOR, PG_FIELD_SEPARATOR);
11862
11969
  const schemas = parsed.rows.map((r) => r[0]).filter(Boolean);
11863
11970
  const value = schemas.length > 0 ? schemas : ["public"];
11864
11971
  return setDockerSchemasCache(cacheKey, value, DOCKER_DATABASES_POSITIVE_TTL_MS, now);
@@ -11916,7 +12023,7 @@ async function openDockerAdapterAsync(serviceName, kind, env, cwd, overrideDatab
11916
12023
  ...kind === "postgresql" && schema ? { schema } : {}
11917
12024
  });
11918
12025
  }
11919
- var createPgPoolImpl = (config) => new pg.Pool(config), createMysqlPoolImpl = (config) => mysql.createPool(config), COLUMNS_TTL_MS = 30000, ROWCOUNT_TTL_MS = 15000, DOCKER_DATABASES_POSITIVE_TTL_MS = 15000, DOCKER_DATABASES_NEGATIVE_TTL_MS = 3000, dockerDatabasesCache, dockerSchemasCache, spawnSyncImpl2, PG_RECORD_SEPARATOR = "\x1E", MYSQL_SPATIAL_TYPES, createDockerAdapter, SUPABASE_LOCAL_DB_USER = "postgres", SUPABASE_LOCAL_DB_PASSWORD = "postgres", SUPABASE_LOCAL_DB_NAME = "postgres";
12026
+ var createPgPoolImpl = (config) => new pg.Pool(config), createMysqlPoolImpl = (config) => mysql.createPool(config), COLUMNS_TTL_MS = 30000, ROWCOUNT_TTL_MS = 15000, DOCKER_DATABASES_POSITIVE_TTL_MS = 15000, DOCKER_DATABASES_NEGATIVE_TTL_MS = 3000, dockerDatabasesCache, dockerSchemasCache, spawnSyncImpl2, PG_RECORD_SEPARATOR = "\x1E", PG_FIELD_SEPARATOR = "\x1F", MYSQL_SPATIAL_TYPES, createDockerAdapter, SUPABASE_LOCAL_DB_USER = "postgres", SUPABASE_LOCAL_DB_PASSWORD = "postgres", SUPABASE_LOCAL_DB_NAME = "postgres";
11920
12027
  var init_docker = __esm(() => {
11921
12028
  init_mutate();
11922
12029
  init_sql_snapshot();
@@ -23140,7 +23247,8 @@ async function rgAvailableAsync(cwd) {
23140
23247
  return rgAvailableCache;
23141
23248
  }
23142
23249
  function isExcludedScopePath(path, excludeNames) {
23143
- 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));
23144
23252
  }
23145
23253
  function isSafePath(path) {
23146
23254
  if (!path || path.startsWith("/") || path.startsWith("\\") || path.includes("\x00"))
@@ -23339,6 +23447,7 @@ var init_search_service = __esm(() => {
23339
23447
  init_command_resolver();
23340
23448
  init_spawn_runner();
23341
23449
  init_git();
23450
+ init_name_pattern();
23342
23451
  init_search();
23343
23452
  });
23344
23453
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@youtyan/code-viewer",
3
- "version": "0.8.0",
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) {
@@ -15998,7 +16190,8 @@ ${frontmatter.yaml}
15998
16190
  header.className = "db-schema-header";
15999
16191
  const headerTitle = document.createElement("span");
16000
16192
  headerTitle.className = "db-schema-header-title";
16001
- headerTitle.textContent = `Schema: ${table2}`;
16193
+ const tableComment = extra?.tableComment?.trim();
16194
+ headerTitle.textContent = tableComment ? `Schema: ${table2} — ${tableComment}` : `Schema: ${table2}`;
16002
16195
  header.appendChild(headerTitle);
16003
16196
  if (deps.onRefresh) {
16004
16197
  refreshBtn = document.createElement("button");
@@ -19716,7 +19909,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19716
19909
  container.appendChild(colRow);
19717
19910
  }
19718
19911
  }
19719
- function toggleExpand(tableName, _node, arrow, children) {
19912
+ function toggleExpand(tableName, _node, arrow, children, columnsHost) {
19720
19913
  const expanded = expandedTables.has(tableName);
19721
19914
  if (expanded) {
19722
19915
  expandedTables.delete(tableName);
@@ -19728,8 +19921,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19728
19921
  children.hidden = false;
19729
19922
  arrow.classList.add("expanded");
19730
19923
  callbacks.onExpandedTableChange?.(tableName, true);
19731
- if (children.children.length === 0) {
19732
- renderColumns(children, tableName);
19924
+ if (columnsHost.children.length === 0) {
19925
+ renderColumns(columnsHost, tableName);
19733
19926
  }
19734
19927
  }
19735
19928
  }
@@ -19805,12 +19998,22 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
19805
19998
  const children = document.createElement("div");
19806
19999
  children.className = "db-table-children";
19807
20000
  children.hidden = !expandedTables.has(table2.name);
20001
+ const comment2 = columnCommentText(table2.comment);
20002
+ if (comment2) {
20003
+ const tableComment = document.createElement("div");
20004
+ tableComment.className = "db-table-col-comment";
20005
+ tableComment.textContent = comment2;
20006
+ tableComment.title = comment2;
20007
+ children.appendChild(tableComment);
20008
+ }
20009
+ const columnsHost = document.createElement("div");
20010
+ children.appendChild(columnsHost);
19808
20011
  if (expandedTables.has(table2.name)) {
19809
- renderColumns(children, table2.name);
20012
+ renderColumns(columnsHost, table2.name);
19810
20013
  }
19811
20014
  arrow.addEventListener("click", (e2) => {
19812
20015
  e2.stopPropagation();
19813
- toggleExpand(table2.name, node, arrow, children);
20016
+ toggleExpand(table2.name, node, arrow, children, columnsHost);
19814
20017
  });
19815
20018
  row.addEventListener("click", () => callbacks.onSelectTable(table2.name));
19816
20019
  row.addEventListener("contextmenu", (e2) => showContextMenu(e2, table2.name));
@@ -21116,7 +21319,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21116
21319
  if (generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
21117
21320
  return;
21118
21321
  }
21119
- schemaView.render(table2, columns, schemaCache?.indexes || []);
21322
+ schemaView.render(table2, columns, schemaCache?.indexes || [], {
21323
+ tableComment: schemaCache?.tables.find((entry) => entry.name === table2)?.comment
21324
+ });
21120
21325
  }
21121
21326
  }
21122
21327
  async function selectTableSchemaOnly(table2, generation = loadGeneration) {
@@ -21132,7 +21337,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21132
21337
  if (generation !== loadGeneration || currentDbInfo?.id !== requestDbId || currentTable !== table2) {
21133
21338
  return;
21134
21339
  }
21135
- schemaView.render(table2, columns, schemaCache?.indexes || []);
21340
+ schemaView.render(table2, columns, schemaCache?.indexes || [], {
21341
+ tableComment: schemaCache?.tables.find((entry) => entry.name === table2)?.comment
21342
+ });
21136
21343
  }
21137
21344
  async function fetchColumns(table2) {
21138
21345
  if (schemaCache?.columnsMap?.[table2]) {
@@ -21158,7 +21365,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21158
21365
  if (!currentDbInfo)
21159
21366
  return;
21160
21367
  const columns = await fetchColumns(table2);
21161
- schemaView.render(table2, columns, schemaCache?.indexes || []);
21368
+ schemaView.render(table2, columns, schemaCache?.indexes || [], {
21369
+ tableComment: schemaCache?.tables.find((entry) => entry.name === table2)?.comment
21370
+ });
21162
21371
  }
21163
21372
  async function refreshCurrentSchemaView() {
21164
21373
  if (!currentDbInfo || !currentTable)
@@ -21190,7 +21399,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21190
21399
  if (generation !== loadGeneration || currentDbInfo?.id !== dbId || currentTable !== nextTable) {
21191
21400
  return;
21192
21401
  }
21193
- schemaView.render(nextTable, columns, schema.indexes || []);
21402
+ schemaView.render(nextTable, columns, schema.indexes || [], {
21403
+ tableComment: schema.tables.find((entry) => entry.name === nextTable)?.comment
21404
+ });
21194
21405
  cb.onStateChange();
21195
21406
  } catch (err) {
21196
21407
  if (generation !== loadGeneration || currentDbInfo?.id !== dbId || isAbortError2(err)) {
@@ -21217,7 +21428,8 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21217
21428
  schemaView.render(table2, columns, schemaCache?.indexes || [], {
21218
21429
  foreignKeys: schemaCache?.foreignKeys,
21219
21430
  triggers: data.triggers,
21220
- ddl: data.sql
21431
+ ddl: data.sql,
21432
+ tableComment: schemaCache?.tables.find((entry) => entry.name === table2)?.comment
21221
21433
  });
21222
21434
  } catch {}
21223
21435
  }
@@ -21410,11 +21622,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21410
21622
  dbSelect.value = target;
21411
21623
  currentDbInfo = files.find((f2) => f2.id === target) || null;
21412
21624
  syncConnectionActions();
21625
+ const forceReload = options.forceReload ?? false;
21413
21626
  const explorerInitial = {
21414
- redis: pendingRedisInitial,
21415
- es: pendingEsInitial,
21416
- s3: pendingS3Initial,
21417
- 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)
21418
21631
  };
21419
21632
  pendingRedisInitial = undefined;
21420
21633
  pendingEsInitial = undefined;
@@ -21547,7 +21760,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21547
21760
  }
21548
21761
  } else if (currentDbInfo?.kind === "dynamodb") {
21549
21762
  const sel = dynamodbExplorer.getSelection();
21550
- 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") {
21551
21764
  state.dynamodb = sel;
21552
21765
  }
21553
21766
  }
@@ -22612,7 +22825,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22612
22825
  if (!mounted || activeTabId !== id)
22613
22826
  return;
22614
22827
  const state = entry.pane.getState();
22615
- 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 });
22616
22829
  if (!mounted || activeTabId !== id)
22617
22830
  return;
22618
22831
  refreshChipLabel(id);
@@ -26108,7 +26321,7 @@ code-viewer annotate add-db --db app.db --tab query \\
26108
26321
  ],
26109
26322
  [
26110
26323
  "DynamoDB / LocalStack",
26111
- "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."
26112
26325
  ],
26113
26326
  [
26114
26327
  "S3 / MinIO / LocalStack",
@@ -26130,7 +26343,7 @@ code-viewer annotate add-db --db app.db --tab query \\
26130
26343
  ],
26131
26344
  [
26132
26345
  "Sidebar",
26133
- "DB selector, PostgreSQL schema selector, table tree (expand to see columns), filter, Rails FK inference toggle, and icon toolbar for Query / ER / Search / Snapshot tabs."
26346
+ "DB selector, PostgreSQL schema selector, table tree (expand to see columns and a table description when available), filter, Rails FK inference toggle, and icon toolbar for Query / ER / Search / Snapshot tabs."
26134
26347
  ],
26135
26348
  [
26136
26349
  "Data tab",
@@ -26142,7 +26355,7 @@ code-viewer annotate add-db --db app.db --tab query \\
26142
26355
  ],
26143
26356
  [
26144
26357
  "Schema tab",
26145
- "Column definitions, indexes, foreign keys, triggers, and DDL, with an in-tab refresh action for reloading the current table structure."
26358
+ "Table description when available, column definitions, indexes, foreign keys, triggers, and DDL, with an in-tab refresh action for reloading the current table structure."
26146
26359
  ],
26147
26360
  [
26148
26361
  "Query editor",
@@ -26775,7 +26988,7 @@ code-viewer annotate add-db --db app.db --tab query \\
26775
26988
  ],
26776
26989
  [
26777
26990
  "DynamoDB / LocalStack",
26778
- "LocalStack の compose サービスで DynamoDB が有効な場合に検出。テーブル一覧、キースキーマ、scan / query、継続トークンによるページング、コピー可能なキー付きのアイテム詳細を表示します。閲覧専用です。"
26991
+ "LocalStack の compose サービスで DynamoDB が有効な場合に検出。テーブル一覧、構造タブ(キースキーマ・GSI/LSI・読み込み済みアイテムから推測した非キー属性の型)、scan / query、継続トークンによるページング、コピー可能なキー付きのアイテム詳細を表示します。閲覧専用です。"
26779
26992
  ],
26780
26993
  [
26781
26994
  "S3 / MinIO / LocalStack",
@@ -26797,7 +27010,7 @@ code-viewer annotate add-db --db app.db --tab query \\
26797
27010
  ],
26798
27011
  [
26799
27012
  "サイドバー",
26800
- "DB 選択、PostgreSQL スキーマセレクター、テーブルツリー(展開でカラム表示)、フィルター、Rails 命名規約による仮想 FK 推測トグル、Query / ER / Search / Snapshot アイコンツールバー。"
27013
+ "DB 選択、PostgreSQL スキーマセレクター、テーブルツリー(展開でカラムと、あればテーブルコメントを表示)、フィルター、Rails 命名規約による仮想 FK 推測トグル、Query / ER / Search / Snapshot アイコンツールバー。"
26801
27014
  ],
26802
27015
  [
26803
27016
  "Data タブ",
@@ -26809,7 +27022,7 @@ code-viewer annotate add-db --db app.db --tab query \\
26809
27022
  ],
26810
27023
  [
26811
27024
  "Schema タブ",
26812
- "カラム定義、インデックス、外部キー、トリガー、DDL。現在の表構造だけを再読み込みするタブ内更新にも対応。"
27025
+ "テーブルコメント(あれば)、カラム定義、インデックス、外部キー、トリガー、DDL。現在の表構造だけを再読み込みするタブ内更新にも対応。"
26813
27026
  ],
26814
27027
  [
26815
27028
  "クエリエディター",
@@ -36152,9 +36365,9 @@ code-viewer query agent-help`
36152
36365
  displaySource: "Applies to all projects in this browser.",
36153
36366
  excludedDirectories: "Excluded directories",
36154
36367
  omitDirs: "Skip these directory names while browsing and searching",
36155
- 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]).",
36156
36369
  excludeNames: "Hide these file or directory names completely",
36157
- 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]).",
36158
36371
  reset: "Restore defaults",
36159
36372
  autosaveNote: "Changes save automatically.",
36160
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.`,
@@ -36442,9 +36655,9 @@ code-viewer query agent-help`
36442
36655
  displaySource: "このブラウザのすべてのプロジェクトに適用されます。",
36443
36656
  excludedDirectories: "除外ディレクトリ",
36444
36657
  omitDirs: "閲覧と検索でスキップするディレクトリ名",
36445
- 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])に対応しています。",
36446
36659
  excludeNames: "完全に非表示にするファイル名またはディレクトリ名",
36447
- excludeNamesHelp: "リスト中の名前に一致するファイル/ディレクトリを、サイドバー・検索結果・grep 結果から完全に消します。Skip と違い、名前自体が UI に出なくなります。",
36660
+ excludeNamesHelp: "リスト中の名前に一致するファイル/ディレクトリを、サイドバー・検索結果・grep 結果から完全に消します。Skip と違い、名前自体が UI に出なくなります。gitignore方式のワイルドカード(*, ?, [abc], [!abc])に対応しています。",
36448
36661
  reset: "デフォルトに戻す",
36449
36662
  autosaveNote: "変更は自動で保存されます。",
36450
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);