anentrypoint-design 0.0.358 → 0.0.360
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/247420.css +131 -0
- package/dist/247420.js +24 -22
- package/package.json +4 -1
- package/src/components/freddie.js +83 -1
- package/src/components/git-status.js +153 -0
- package/src/components/shell.js +5 -2
- package/src/components/worktree-switcher.js +57 -0
- package/src/css/app-shell/git-status.css +112 -0
- package/src/css/app-shell/primitives.css +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anentrypoint-design",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.360",
|
|
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": {
|
|
@@ -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
|
+
}
|
package/src/components/shell.js
CHANGED
|
@@ -27,12 +27,15 @@ export function Brand({ name = '247420', leaf } = {}) {
|
|
|
27
27
|
* @param {string} [props.tone=''] - semantic color tone (empty = neutral).
|
|
28
28
|
* @param {'sm'|'md'|'lg'} [props.size='md']
|
|
29
29
|
* @param {boolean} [props.tag=false] - true renders a rectangular sentence-case variant for dense data (drops the all-caps pill styling). Orthogonal to tone.
|
|
30
|
+
* @param {Function} [props.onRemove] - if given, renders a trailing dismiss (x) button that calls onRemove() on click. Omitted entirely (no button) when not supplied.
|
|
30
31
|
* @param {*} props.children
|
|
31
32
|
* @returns {*} webjsx vnode
|
|
32
33
|
*/
|
|
33
|
-
export function Chip({ tone = '', size = 'md', tag = false, children }) {
|
|
34
|
+
export function Chip({ tone = '', size = 'md', tag = false, onRemove, children }) {
|
|
34
35
|
const sizeCls = size === 'sm' ? ' chip--sm' : (size === 'lg' ? ' chip--lg' : '');
|
|
35
|
-
return h('span', { class: 'chip' + sizeCls + (tag ? ' chip--tag' : '') + (tone ? ' tone-' + tone : '') },
|
|
36
|
+
return h('span', { class: 'chip' + sizeCls + (tag ? ' chip--tag' : '') + (tone ? ' tone-' + tone : '') + (onRemove ? ' ds-chip-removable' : '') },
|
|
37
|
+
children,
|
|
38
|
+
onRemove ? h('button', { type: 'button', class: 'ds-chip-remove-btn', 'aria-label': 'Remove', onclick: (e) => { e.stopPropagation(); onRemove(); } }, Icon('x')) : null);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
/**
|
|
@@ -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
|
+
}
|
|
@@ -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; }
|
|
@@ -175,6 +175,25 @@
|
|
|
175
175
|
.chip.tone-orange { background: color-mix(in oklab, var(--flame) 30%, var(--sun) 70%); color: var(--ink); }
|
|
176
176
|
.chip.tone-yellow { background: var(--sun); color: var(--ink); }
|
|
177
177
|
|
|
178
|
+
/* Removable chip — a trailing dismiss (x) button for editable tag/filter
|
|
179
|
+
chip lists (session tags, saved-search chips). Reuses the chip's own
|
|
180
|
+
tone/size; the button itself stays neutral-at-rest, per the standing
|
|
181
|
+
color-reserved-for-meaning convention, tinting only on hover/focus. */
|
|
182
|
+
.chip.ds-chip-removable { padding-right: var(--space-1); }
|
|
183
|
+
.ds-chip-remove-btn {
|
|
184
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
185
|
+
width: 16px; height: 16px; margin-left: var(--space-half);
|
|
186
|
+
padding: 0; border: none; border-radius: var(--r-pill);
|
|
187
|
+
background: transparent; color: currentColor; opacity: 0.7;
|
|
188
|
+
cursor: pointer;
|
|
189
|
+
}
|
|
190
|
+
.ds-chip-remove-btn:hover, .ds-chip-remove-btn:focus-visible { opacity: 1; background: color-mix(in oklab, currentColor 16%, transparent); }
|
|
191
|
+
.ds-chip-remove-btn svg { width: 10px; height: 10px; }
|
|
192
|
+
@media (pointer: coarse) {
|
|
193
|
+
.ds-chip-remove-btn { width: 24px; height: 24px; margin-left: 0; }
|
|
194
|
+
.ds-chip-remove-btn svg { width: 12px; height: 12px; }
|
|
195
|
+
}
|
|
196
|
+
|
|
178
197
|
/* ds-pill — plain neutral chip, distinct from the toned .ds-badge/.chip family
|
|
179
198
|
(gmsniff's .pill: a bare rounded label with no semantic tone). */
|
|
180
199
|
.ds-pill {
|