anentrypoint-design 0.0.357 → 0.0.359

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "anentrypoint-design",
3
- "version": "0.0.357",
3
+ "version": "0.0.359",
4
4
  "description": "247420 design system SDK — webjsx + modified ripple-ui, single-file ESM bundle for reproducible use of the AnEntrypoint design.",
5
5
  "type": "module",
6
6
  "main": "./dist/247420.js",
@@ -23,6 +23,9 @@
23
23
  "./components/shell.js": "./src/components/shell.js",
24
24
  "./components/files.js": "./src/components/files.js",
25
25
  "./components/files-modals.js": "./src/components/files-modals.js",
26
+ "./components/overlay-primitives.js": "./src/components/overlay-primitives.js",
27
+ "./components/git-status.js": "./src/components/git-status.js",
28
+ "./components/worktree-switcher.js": "./src/components/worktree-switcher.js",
26
29
  "./web-components/ds-chat.js": "./src/web-components/ds-chat.js",
27
30
  "./web-components/freddie-chat.js": "./src/web-components/freddie-chat.js",
28
31
  "./lint": {
@@ -70,6 +70,20 @@ export function BarRow({ label, value, pct = 0, tone } = {}) {
70
70
  h('span', { class: 'ds-bar-row-value', 'aria-hidden': 'true' }, value));
71
71
  }
72
72
 
73
+ // ---------------------------------------------------------------------------
74
+ // RateCell — a tone-colored numeric cell for dense admin/observability tables
75
+ // (percentile latency columns, success-rate columns). Ported from docstudio's
76
+ // admin-observability-views.js endpointsView() success-rate coloring, which
77
+ // had no kit equivalent: a plain Table cell has no notion of a value implying
78
+ // good/warn/bad. Host computes the tone (this component has no opinion on
79
+ // thresholds, matching Table's onSort host-owns-logic convention) and passes
80
+ // it plus the display text; renders inline so it drops into any Table row.
81
+ // ---------------------------------------------------------------------------
82
+ export function RateCell({ value, tone = 'neutral' } = {}) {
83
+ const cls = 'ds-rate-cell ds-rate-cell-' + tone;
84
+ return h('span', { class: cls }, value == null ? '–' : String(value));
85
+ }
86
+
73
87
  // ---------------------------------------------------------------------------
74
88
  // StatTile / StatsGrid — compact KPI tiles, denser than the existing .kpi.
75
89
  // cls on StatTile selects an accent variant: '' | 'rate-big' | 'err-rate'.
@@ -15,6 +15,8 @@ import { queueMessage, watchReconnect, isOnline } from '../idb-outbox.js';
15
15
  import { ChatMessage, ChatComposer } from './chat.js';
16
16
  import { fmtTime, fmtAgo } from './sessions.js';
17
17
  import { createVirtualizer, measureRef } from '../virtual-scroll.js';
18
+ import { GitStatusPanel, GitDiffView } from './git-status.js';
19
+ import { WorktreeSwitcher } from './worktree-switcher.js';
18
20
 
19
21
  const h = webjsx.createElement;
20
22
 
@@ -868,12 +870,92 @@ export const debug = makePage((ctx) => {
868
870
  };
869
871
  });
870
872
 
873
+ // ---- git ---------------------------------------------------------------
874
+
875
+ export const git = makePage((ctx) => {
876
+ Object.assign(ctx.state, { cwd: null, status: null, log: null, worktrees: null, diff: null, activeFile: null, diffLoading: false, note: null });
877
+ async function load() {
878
+ try {
879
+ const proj = await api('/api/projects').catch(() => null);
880
+ const active = proj && proj.active;
881
+ const cwd = (active && typeof active === 'object' ? active.path : null) || ctx.state.cwd || '';
882
+ const qs = '?cwd=' + encodeURIComponent(cwd);
883
+ const [status, log, worktrees] = await Promise.all([
884
+ api('/api/git/status' + qs).catch((e) => ({ _err: e })),
885
+ api('/api/git/log' + qs + '&limit=20').catch((e) => ({ _err: e })),
886
+ api('/api/worktree' + qs).catch((e) => ({ _err: e })),
887
+ ]);
888
+ ctx.set({ loading: false, cwd, status, log, worktrees, error: null });
889
+ } catch (e) { ctx.set({ loading: false, error: e }); }
890
+ }
891
+ async function openDiff(file) {
892
+ ctx.set({ activeFile: file.path, diffLoading: true, diff: null });
893
+ try {
894
+ const qs = '?cwd=' + encodeURIComponent(ctx.state.cwd || '') + '&file=' + encodeURIComponent(file.path);
895
+ const res = await api('/api/git/diff' + qs);
896
+ ctx.set({ diff: res, diffLoading: false });
897
+ } catch (e) { ctx.set({ note: { kind: 'error', msg: String(e.message || e) }, diffLoading: false }); }
898
+ }
899
+ async function createWorktree() {
900
+ const path = (ctx.state.newWtPath || '').trim();
901
+ const branch = (ctx.state.newWtBranch || '').trim();
902
+ if (!path) { ctx.set({ note: { kind: 'warn', msg: 'path required' } }); return; }
903
+ ctx.set({ busy: true, note: null });
904
+ try {
905
+ await api('/api/worktree', { method: 'POST', body: { cwd: ctx.state.cwd || '', path, branch: branch || undefined } });
906
+ ctx.state.newWtPath = ''; ctx.state.newWtBranch = '';
907
+ await load();
908
+ } catch (e) { ctx.set({ note: { kind: 'error', msg: String(e.message || e) } }); }
909
+ ctx.set({ busy: false });
910
+ }
911
+ load();
912
+ return () => {
913
+ const s = ctx.state;
914
+ if (s.loading) return loadingState('loading git status…');
915
+ if (s.error && !s.status) return errorState(s.error, load);
916
+ const statusFailed = s.status && s.status._err;
917
+ const logFailed = s.log && s.log._err;
918
+ const wtFailed = s.worktrees && s.worktrees._err;
919
+ const files = statusFailed ? [] : (s.status && s.status.files) || [];
920
+ const commits = logFailed ? [] : (s.log && s.log.commits) || s.log || [];
921
+ const worktrees = wtFailed ? [] : (s.worktrees && s.worktrees.worktrees) || s.worktrees || [];
922
+ const current = Array.isArray(worktrees) ? (worktrees.find(w => w.path === s.cwd) || {}).path : undefined;
923
+ return [
924
+ PageHeader({ eyebrow: 'freddie', title: 'git', lede: s.cwd || 'active project' }),
925
+ noteAlert(s.note),
926
+ statusFailed ? refreshError(statusFailed) : null,
927
+ section('worktrees',
928
+ WorktreeSwitcher({
929
+ worktrees: Array.isArray(worktrees) ? worktrees : [],
930
+ current,
931
+ onSwitch: () => {},
932
+ onCreate: () => ctx.set({ showWtForm: !s.showWtForm }),
933
+ }),
934
+ s.showWtForm ? h('div', { class: 'fd-row-actions' },
935
+ TextField({ label: 'path', value: s.newWtPath, onInput: (v) => { s.newWtPath = v; }, placeholder: '/path/to/worktree' }),
936
+ TextField({ label: 'branch (optional)', value: s.newWtBranch, onInput: (v) => { s.newWtBranch = v; }, placeholder: 'feature/x' }),
937
+ Btn({ variant: 'primary', disabled: s.busy, children: s.busy ? 'working…' : 'create', onClick: createWorktree })) : null),
938
+ section('changed files',
939
+ statusFailed ? errorState(statusFailed) : GitStatusPanel({ files, onFileClick: openDiff, active: s.activeFile })),
940
+ section('diff' + (s.activeFile ? ' · ' + s.activeFile : ''),
941
+ s.diffLoading ? loadingState('loading diff…')
942
+ : s.diff ? GitDiffView({ diff: s.diff.diff || s.diff, filename: s.activeFile })
943
+ : emptyState('select a file to view its diff')),
944
+ section('log',
945
+ logFailed ? errorState(logFailed)
946
+ : commits.length
947
+ ? Table({ headers: ['sha', 'message', 'author', 'date'], rows: commits.slice(0, 20).map(c => [String(c.sha || c.hash || '').slice(0, 8), truncSpan(c.message || c.subject, TRUNC_SUB), c.author || '', c.date ? fmtAgo(c.date) : ''])})
948
+ : emptyState('no commits')),
949
+ ].filter(Boolean);
950
+ };
951
+ });
952
+
871
953
  // ---- registry --------------------------------------------------------------
872
954
 
873
955
  export const FREDDIE_PAGES = {
874
956
  home, chat, voice, sessions, projects, agents, analytics,
875
957
  models, cron, skills, config, env, tools, batch, gateway, chains,
876
- machines, health, debug, logs,
958
+ machines, health, debug, logs, git,
877
959
  };
878
960
 
879
961
  export { skillLabel, getRecentPaths, saveRecentPath, renderChatMessages };
@@ -0,0 +1,153 @@
1
+ // Git status + diff primitives — changed-file list and unified-diff renderer.
2
+ // Ported from pi-web's BranchNavigator concept into this design system's
3
+ // pure-webjsx idiom (see files.js for the sibling file-list pattern this
4
+ // mirrors: FileIcon-style type glyph, ds-file-row-style clickable row).
5
+
6
+ import * as webjsx from '../../vendor/webjsx/index.js';
7
+ import { Icon } from './shell.js';
8
+ import { fileGlyph } from './files.js';
9
+ import { highlightAllUnder } from '../highlight.js';
10
+ const h = webjsx.createElement;
11
+
12
+ // git status letter -> { label, tone, icon } used for both the row's status
13
+ // chip and its left-edge tone. Mirrors the porcelain status codes pi-web's
14
+ // git API returns (A/M/D/R/? ...), collapsed to the ones a status list needs.
15
+ const STATUS_META = {
16
+ A: { label: 'added', tone: 'add', glyph: 'A' },
17
+ M: { label: 'modified', tone: 'modify', glyph: 'M' },
18
+ D: { label: 'deleted', tone: 'delete', glyph: 'D' },
19
+ R: { label: 'renamed', tone: 'modify', glyph: 'R' },
20
+ C: { label: 'copied', tone: 'modify', glyph: 'C' },
21
+ U: { label: 'conflicted', tone: 'delete', glyph: 'U' },
22
+ '?': { label: 'untracked', tone: 'add', glyph: '?' },
23
+ '!': { label: 'ignored', tone: 'neutral', glyph: '!' },
24
+ };
25
+
26
+ function statusMeta(status) {
27
+ return STATUS_META[status] || { label: status || 'changed', tone: 'neutral', glyph: (status || '?').slice(0, 1) };
28
+ }
29
+
30
+ // Guess a file "type" (for FileIcon-style glyph reuse) from its extension —
31
+ // git-status entries are paths, not the richer {type} shape files.js rows get
32
+ // from a directory listing.
33
+ const EXT_TYPE = {
34
+ js: 'code', mjs: 'code', cjs: 'code', ts: 'code', tsx: 'code', jsx: 'code',
35
+ py: 'code', rs: 'code', go: 'code', java: 'code', c: 'code', cpp: 'code', h: 'code',
36
+ css: 'code', scss: 'code', html: 'code', json: 'code', yml: 'code', yaml: 'code', toml: 'code', sh: 'code',
37
+ md: 'document', mdx: 'document', txt: 'text',
38
+ png: 'image', jpg: 'image', jpeg: 'image', gif: 'image', svg: 'image', webp: 'image',
39
+ mp4: 'video', mov: 'video', webm: 'video',
40
+ mp3: 'audio', wav: 'audio',
41
+ zip: 'archive', tar: 'archive', gz: 'archive',
42
+ };
43
+
44
+ function fileTypeFromPath(pathname = '') {
45
+ const base = pathname.split('/').pop() || pathname;
46
+ const dot = base.lastIndexOf('.');
47
+ if (dot <= 0) return 'other';
48
+ return EXT_TYPE[base.slice(dot + 1).toLowerCase()] || 'other';
49
+ }
50
+
51
+ // GitStatusPanel — a list of changed files with add/modify/delete indicators.
52
+ // files: [{ path, status, staged?, insertions?, deletions? }]
53
+ // onFileClick(file) opens the diff for a row.
54
+ export function GitStatusPanel({ files = [], onFileClick, emptyText = 'no changes', active } = {}) {
55
+ if (!files.length) {
56
+ return h('div', { class: 'ds-git-empty', role: 'status' },
57
+ h('span', { class: 'ds-git-empty-glyph', 'aria-hidden': 'true' }, Icon('circle-dot', { size: 22 })),
58
+ h('span', {}, emptyText));
59
+ }
60
+ return h('div', { class: 'ds-git-status-grid', role: 'group', 'aria-label': 'changed files' },
61
+ ...files.map((f, i) => GitStatusRow({ key: f.path || i, file: f, onClick: onFileClick, active: active === f.path })));
62
+ }
63
+
64
+ function GitStatusRow({ key, file, onClick, active } = {}) {
65
+ const meta = statusMeta(file.status);
66
+ const type = fileTypeFromPath(file.path);
67
+ const stats = [
68
+ file.insertions != null ? h('span', { class: 'ds-git-stat ds-git-stat-add' }, '+' + file.insertions) : null,
69
+ file.deletions != null ? h('span', { class: 'ds-git-stat ds-git-stat-del' }, '-' + file.deletions) : null,
70
+ ].filter(Boolean);
71
+ return h('button', {
72
+ key, type: 'button',
73
+ class: 'ds-git-row' + (active ? ' active' : ''),
74
+ onclick: onClick ? () => onClick(file) : null,
75
+ disabled: onClick ? null : true,
76
+ 'aria-label': meta.label + ': ' + file.path,
77
+ 'aria-pressed': active ? 'true' : 'false',
78
+ },
79
+ h('span', { class: 'ds-git-status-chip tone-' + meta.tone, 'aria-hidden': 'true' }, meta.glyph),
80
+ h('span', { class: 'ds-git-row-icon', 'aria-hidden': 'true' }, Icon(fileGlyph(type), { size: 15 })),
81
+ h('span', { class: 'ds-git-row-path' }, file.path),
82
+ file.staged ? h('span', { class: 'ds-git-row-tag' }, 'staged') : null,
83
+ stats.length ? h('span', { class: 'ds-git-row-stats' }, ...stats) : null,
84
+ );
85
+ }
86
+
87
+ // Parse a unified diff into hunks of typed lines so we can color +/- context
88
+ // independently of the raw text (avoids relying on Prism's diff grammar for
89
+ // the leading marker column, which we style ourselves).
90
+ function parseUnifiedDiff(diff = '') {
91
+ const lines = diff.split('\n');
92
+ const hunks = [];
93
+ let current = null;
94
+ for (const line of lines) {
95
+ if (line.startsWith('@@')) {
96
+ current = { header: line, lines: [] };
97
+ hunks.push(current);
98
+ continue;
99
+ }
100
+ if (line.startsWith('diff --git') || line.startsWith('index ') ||
101
+ line.startsWith('--- ') || line.startsWith('+++ ')) {
102
+ continue; // file-header noise; the host already knows the filename
103
+ }
104
+ if (!current) continue;
105
+ let kind = 'context';
106
+ if (line.startsWith('+')) kind = 'add';
107
+ else if (line.startsWith('-')) kind = 'del';
108
+ current.lines.push({ kind, text: line });
109
+ }
110
+ return hunks;
111
+ }
112
+
113
+ const EXT_LANG = {
114
+ js: 'javascript', mjs: 'javascript', cjs: 'javascript', jsx: 'jsx',
115
+ ts: 'typescript', tsx: 'tsx', py: 'python', rs: 'rust', go: 'go',
116
+ css: 'css', scss: 'css', html: 'markup', md: 'markdown', json: 'json',
117
+ yml: 'yaml', yaml: 'yaml', sh: 'bash', sql: 'sql', toml: 'toml',
118
+ };
119
+
120
+ function langFromFilename(filename = '') {
121
+ const base = filename.split('/').pop() || filename;
122
+ const dot = base.lastIndexOf('.');
123
+ if (dot <= 0) return null;
124
+ return EXT_LANG[base.slice(dot + 1).toLowerCase()] || null;
125
+ }
126
+
127
+ // GitDiffView — unified-diff renderer with +/- line coloring. Uses
128
+ // highlight.js's Prism loader for language detection by file extension; the
129
+ // +/- marker column is drawn by CSS (tone classes), Prism only tokenizes the
130
+ // code content inside each line.
131
+ export function GitDiffView({ diff = '', filename } = {}) {
132
+ const hunks = parseUnifiedDiff(diff);
133
+ const lang = langFromFilename(filename);
134
+ const highlightRef = (el) => {
135
+ if (!el) return;
136
+ try { highlightAllUnder(el); } catch { /* progressive enhancement only */ }
137
+ };
138
+ if (!hunks.length) {
139
+ return h('div', { class: 'ds-git-diff-empty', role: 'status' }, 'no diff to show');
140
+ }
141
+ return h('div', { class: 'ds-git-diff', ref: highlightRef },
142
+ filename ? h('div', { class: 'ds-git-diff-head' }, h('span', { class: 'name' }, filename)) : null,
143
+ ...hunks.map((hunk, hi) => h('div', { key: 'hunk' + hi, class: 'ds-git-hunk' },
144
+ h('div', { class: 'ds-git-hunk-header' }, hunk.header),
145
+ h('pre', { class: 'ds-git-hunk-body' + (lang ? ' lang-' + lang : '') },
146
+ ...hunk.lines.map((l, li) => h('code', {
147
+ key: li,
148
+ class: 'ds-git-line ds-git-line-' + l.kind + (lang ? ' language-' + lang : ''),
149
+ }, l.text + '\n'))
150
+ )
151
+ ))
152
+ );
153
+ }
@@ -0,0 +1,57 @@
1
+ // WorktreeSwitcher — a dropdown for listing/switching git worktrees + branches,
2
+ // with a "new worktree" action. Built on overlay-primitives.js's Dropdown
3
+ // (itself the Popover/useFloating primitives + roving-focus menu keyboard
4
+ // handling already in this kit) rather than re-deriving popover positioning,
5
+ // outside-click, Escape, and focus-trap logic from scratch.
6
+
7
+ import * as webjsx from '../../vendor/webjsx/index.js';
8
+ import { Icon } from './shell.js';
9
+ import { Dropdown } from './overlay-primitives.js';
10
+ const h = webjsx.createElement;
11
+
12
+ const NEW_WORKTREE_ID = '__ds_new_worktree__';
13
+
14
+ // WorktreeSwitcher({ worktrees, current, onSwitch, onCreate })
15
+ // worktrees: [{ path, branch, current? }]
16
+ // current: path of the active worktree (falls back to a worktree's own `current` flag)
17
+ // onSwitch(worktree) — fired when an existing worktree is picked
18
+ // onCreate() — fired from the trailing "new worktree" row; the host owns the create flow (prompt/modal)
19
+ export function WorktreeSwitcher({ worktrees = [], current, onSwitch, onCreate, ariaLabel = 'switch worktree' } = {}) {
20
+ const isCurrent = (wt) => wt.current || (current != null && wt.path === current);
21
+ const activeWt = worktrees.find(isCurrent) || worktrees[0];
22
+ const byId = new Map(worktrees.map((wt, i) => [wt.path || String(i), wt]));
23
+
24
+ const items = [
25
+ ...worktrees.map((wt, i) => ({
26
+ id: wt.path || String(i),
27
+ label: h('span', { class: 'ds-wts-item-body' },
28
+ h('span', { class: 'ds-wts-item-check', 'aria-hidden': 'true' },
29
+ isCurrent(wt) ? Icon('check', { size: 14 }) : null),
30
+ h('span', { class: 'ds-wts-item-text' },
31
+ h('span', { class: 'ds-wts-item-branch' }, wt.branch || '(detached HEAD)'),
32
+ h('span', { class: 'ds-wts-item-path' }, wt.path)
33
+ ),
34
+ ),
35
+ disabled: isCurrent(wt),
36
+ })),
37
+ worktrees.length && onCreate ? { separator: true } : null,
38
+ onCreate ? {
39
+ id: NEW_WORKTREE_ID,
40
+ glyph: '+',
41
+ label: 'new worktree',
42
+ } : null,
43
+ ].filter(Boolean);
44
+
45
+ const onSelect = (id) => {
46
+ if (id === NEW_WORKTREE_ID) { onCreate && onCreate(); return; }
47
+ const wt = byId.get(id);
48
+ if (wt && onSwitch && !isCurrent(wt)) onSwitch(wt);
49
+ };
50
+
51
+ const trigger = h('button', { type: 'button', class: 'ds-wts-trigger' },
52
+ h('span', { class: 'ds-wts-trigger-branch' }, (activeWt && activeWt.branch) || 'select worktree'),
53
+ h('span', { class: 'ds-wts-trigger-caret', 'aria-hidden': 'true' }, Icon('chevron-down', { size: 13 }))
54
+ );
55
+
56
+ return h('span', { class: 'ds-wts' }, Dropdown({ trigger, items, onSelect, ariaLabel }));
57
+ }
package/src/components.js CHANGED
@@ -35,6 +35,11 @@ export { ContextPane } from './components/context-pane.js';
35
35
 
36
36
  export { SpreadsheetPreview } from './components/spreadsheet-preview.js';
37
37
 
38
+ export {
39
+ DEFAULT_PHASES, PhaseWalk, TreeNode, BarRow, RateCell,
40
+ StatTile, StatsGrid, SubGrid, SessionRow, DevRow, LiveLogEntry, LiveLog
41
+ } from './components/data-density.js';
42
+
38
43
  export {
39
44
  fileGlyph, fmtFileSize,
40
45
  FileIcon, FileRow, FileGrid, FileSkeleton, sortFiles, FileToolbar, RootsPicker,
@@ -77,6 +77,16 @@
77
77
  .ds-stat-val.err-rate { font-size: clamp(24px, 7cqi, 32px); color: var(--warn); }
78
78
  .ds-stat-lbl { font-size: var(--fs-micro); color: var(--fg-3); margin-top: 2px; }
79
79
 
80
+ /* RateCell — tone-colored numeric table cell (success-rate / percentile-latency
81
+ columns), ported from docstudio's endpointsView() success-rate coloring.
82
+ Neutral at rest; color carries meaning only, matching Table's sortable-header
83
+ restraint pattern. */
84
+ .ds-rate-cell { font-variant-numeric: tabular-nums; font-weight: 600; }
85
+ .ds-rate-cell-neutral { color: var(--fg-2); font-weight: 400; }
86
+ .ds-rate-cell-good { color: var(--success); }
87
+ .ds-rate-cell-warn { color: var(--warn); }
88
+ .ds-rate-cell-bad { color: var(--danger); }
89
+
80
90
  /* Inline row — flex row with centered items and a small gap, for demo/kit
81
91
  markup that needs to lay siblings out horizontally without an inline style. */
82
92
  .ds-inline-row { display: flex; align-items: center; gap: var(--space-2, 8px); }
@@ -0,0 +1,112 @@
1
+ /* ============================================================
2
+ Git status panel + diff view — changed-file list, unified diff.
3
+ ============================================================ */
4
+
5
+ .ds-git-status-grid {
6
+ display: flex; flex-direction: column; gap: 2px;
7
+ }
8
+ .ds-git-row {
9
+ display: flex; align-items: center; gap: 10px;
10
+ width: 100%; padding: 7px 12px;
11
+ background: none; border: var(--bw-hair) solid transparent; border-radius: var(--r-2);
12
+ color: var(--fg); font: inherit; text-align: left; cursor: pointer;
13
+ transition: background var(--dur-snap) var(--ease), border-color var(--dur-snap) var(--ease);
14
+ }
15
+ .ds-git-row:hover { background: var(--bg-2); border-color: var(--rule); }
16
+ .ds-git-row.active { background: var(--accent-tint); border-color: var(--accent); }
17
+ .ds-git-row:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
18
+ .ds-git-row[disabled] { cursor: default; }
19
+
20
+ .ds-git-status-chip {
21
+ display: inline-flex; align-items: center; justify-content: center;
22
+ flex: 0 0 auto; width: 18px; height: 18px;
23
+ font-family: var(--ff-mono); font-size: var(--fs-micro); font-weight: 700;
24
+ border-radius: var(--r-1);
25
+ }
26
+ .ds-git-status-chip.tone-add { color: var(--green-2); background: color-mix(in oklab, var(--green-2) 16%, transparent); }
27
+ .ds-git-status-chip.tone-modify { color: var(--sun); background: color-mix(in oklab, var(--sun) 16%, transparent); }
28
+ .ds-git-status-chip.tone-delete { color: var(--flame); background: color-mix(in oklab, var(--flame) 16%, transparent); }
29
+ .ds-git-status-chip.tone-neutral { color: var(--fg-3); background: var(--bg-2); }
30
+
31
+ .ds-git-row-icon { display: inline-flex; flex: 0 0 auto; color: var(--fg-3); }
32
+ .ds-git-row-path {
33
+ flex: 1 1 auto; min-width: 0;
34
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
35
+ font-family: var(--ff-mono); font-size: var(--fs-sm);
36
+ }
37
+ .ds-git-row-tag {
38
+ flex: 0 0 auto; font-size: var(--fs-micro); color: var(--fg-3);
39
+ border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1); padding: 1px 6px;
40
+ }
41
+ .ds-git-row-stats { flex: 0 0 auto; display: inline-flex; gap: 6px; font-family: var(--ff-mono); font-size: var(--fs-micro); }
42
+ .ds-git-stat-add { color: var(--green-2); }
43
+ .ds-git-stat-del { color: var(--flame); }
44
+
45
+ .ds-git-empty {
46
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
47
+ gap: var(--space-2); padding: var(--space-6) var(--space-3);
48
+ color: var(--fg-3); text-align: center;
49
+ }
50
+ .ds-git-empty-glyph { opacity: 0.55; }
51
+
52
+ /* Diff view */
53
+ .ds-git-diff {
54
+ display: flex; flex-direction: column; gap: var(--space-2);
55
+ background: var(--ink); color: var(--paper);
56
+ border-radius: var(--r-2); overflow: hidden;
57
+ }
58
+ .ds-git-diff-head {
59
+ padding: var(--space-2) var(--space-3);
60
+ border-bottom: var(--bw-hair) solid color-mix(in oklab, var(--paper) 18%, transparent);
61
+ font-family: var(--ff-mono); font-size: var(--fs-xs);
62
+ }
63
+ .ds-git-diff-empty { padding: var(--space-3); color: var(--fg-3); font-size: var(--fs-sm); }
64
+ .ds-git-hunk + .ds-git-hunk { border-top: var(--bw-hair) solid color-mix(in oklab, var(--paper) 12%, transparent); }
65
+ .ds-git-hunk-header {
66
+ padding: 4px var(--space-3);
67
+ color: var(--purple-2); font-family: var(--ff-mono); font-size: var(--fs-micro);
68
+ background: color-mix(in oklab, var(--paper) 6%, transparent);
69
+ }
70
+ .ds-git-hunk-body {
71
+ margin: 0; padding: 0 0 var(--space-2);
72
+ font-family: var(--ff-mono); font-size: var(--fs-xs); line-height: var(--lh-base);
73
+ white-space: pre; overflow-x: auto; tab-size: 2;
74
+ }
75
+ .ds-git-line {
76
+ display: block; padding: 0 var(--space-3);
77
+ }
78
+ .ds-git-line-add { background: color-mix(in oklab, var(--green-2) 16%, transparent); color: var(--paper); }
79
+ .ds-git-line-del { background: color-mix(in oklab, var(--flame) 16%, transparent); color: var(--paper); }
80
+ .ds-git-line-context { color: var(--paper-3); }
81
+
82
+ /* ============================================================
83
+ WorktreeSwitcher — trigger + dropdown menu of worktrees/branches.
84
+ ============================================================ */
85
+ .ds-wts { display: inline-flex; }
86
+ .ds-wts-trigger {
87
+ display: inline-flex; align-items: center; gap: 8px;
88
+ padding: 5px 10px; min-height: 32px;
89
+ background: var(--bg); border: var(--bw-hair) solid var(--rule); border-radius: var(--r-1);
90
+ color: var(--fg); font: inherit; font-size: var(--fs-sm); cursor: pointer;
91
+ max-width: 220px;
92
+ }
93
+ .ds-wts-trigger:hover { background: var(--bg-2); }
94
+ .ds-wts-trigger:focus-visible { outline: var(--focus-w) solid var(--focus-color); outline-offset: var(--focus-offset); }
95
+ .ds-wts-trigger-branch {
96
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
97
+ font-family: var(--ff-mono);
98
+ }
99
+ .ds-wts-trigger-caret { display: inline-flex; flex: 0 0 auto; color: var(--fg-3); }
100
+
101
+ .ds-wts-item-body { display: flex; align-items: center; gap: 8px; min-width: 0; }
102
+ .ds-wts-item-check { display: inline-flex; flex: 0 0 16px; color: var(--accent-ink); }
103
+ .ds-wts-item-text { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
104
+ .ds-wts-item-branch {
105
+ font-family: var(--ff-mono); font-size: var(--fs-sm);
106
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
107
+ }
108
+ .ds-wts-item-path {
109
+ font-size: var(--fs-micro); color: var(--fg-3);
110
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
111
+ }
112
+ .ds-dropdown-item[aria-disabled="true"] .ds-wts-item-branch { color: var(--fg); font-weight: 600; }