dsh-file-activity 0.5.0 → 0.5.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.
@@ -1,126 +1,170 @@
1
- // ── row rendering helpers (recent list & stats tree) ──────────────────
2
- const opClass = (op) => (op === 'create' ? 'dfa-op-create' : op === 'modify' ? 'dfa-op-modify' : op === 'delete' ? 'dfa-op-delete' : 'dfa-op-read')
3
- const opLabel = (op) => (op === 'create' ? strings.create() : op === 'modify' ? strings.modify() : op === 'delete' ? strings.delete() : strings.read())
1
+ // ── row rendering helpers (recent list & stats tree) ──────────────────
2
+ const opClass = (op) =>
3
+ op === 'create'
4
+ ? 'dfa-op-create'
5
+ : op === 'modify'
6
+ ? 'dfa-op-modify'
7
+ : op === 'delete'
8
+ ? 'dfa-op-delete'
9
+ : 'dfa-op-read'
10
+ const opLabel = (op) =>
11
+ op === 'create'
12
+ ? strings.create()
13
+ : op === 'modify'
14
+ ? strings.modify()
15
+ : op === 'delete'
16
+ ? strings.delete()
17
+ : strings.read()
4
18
 
5
- /** Tooltip for a stats file row: absolute path + created / last-seen times. */
6
- const fileTitle = (abs, firstSeen, lastSeen) => {
7
- const times = []
8
- if (typeof firstSeen === 'number') times.push(`${strings.created()} ${formatTime(firstSeen)}`)
9
- if (typeof lastSeen === 'number') times.push(`${strings.lastSeen()} ${formatTime(lastSeen)}`)
10
- return times.length > 0 ? `${abs}\n${times.join(' · ')}` : abs
11
- }
19
+ /** Tooltip for a stats file row: absolute path + created / last-seen times. */
20
+ const fileTitle = (abs, firstSeen, lastSeen) => {
21
+ const times = []
22
+ if (typeof firstSeen === 'number') times.push(`${strings.created()} ${formatTime(firstSeen)}`)
23
+ if (typeof lastSeen === 'number') times.push(`${strings.lastSeen()} ${formatTime(lastSeen)}`)
24
+ return times.length > 0 ? `${abs}\n${times.join(' · ')}` : abs
25
+ }
12
26
 
13
- /** Count pills for a file/dir node — only actions that actually happened are
14
- * shown (a zero count renders no pill; all-zero nodes render no pill group,
15
- * keeping untouched files visually quiet). */
16
- const countPills = (node) => {
17
- const pills = []
18
- if (node.read > 0) pills.push(createElement('span', { className: 'dfa-count dfa-count-read' }, `${strings.readShort()} ${node.read}`))
19
- if (node.create > 0) pills.push(createElement('span', { className: 'dfa-count dfa-count-create' }, `${strings.createShort()} ${node.create}`))
20
- if (node.modify > 0) pills.push(createElement('span', { className: 'dfa-count dfa-count-modify' }, `${strings.modifyShort()} ${node.modify}`))
21
- if (pills.length === 0) return null
22
- return createElement('span', { className: 'dfa-counts', style: { paddingLeft: '6px' } }, ...pills)
23
- }
27
+ /** Count pills for a file/dir node — only actions that actually happened are
28
+ * shown (a zero count renders no pill; all-zero nodes render no pill group,
29
+ * keeping untouched files visually quiet). */
30
+ const countPills = (node) => {
31
+ const pills = []
32
+ if (node.read > 0)
33
+ pills.push(createElement('span', { className: 'dfa-count dfa-count-read' }, `${strings.readShort()} ${node.read}`))
34
+ if (node.create > 0)
35
+ pills.push(
36
+ createElement('span', { className: 'dfa-count dfa-count-create' }, `${strings.createShort()} ${node.create}`),
37
+ )
38
+ if (node.modify > 0)
39
+ pills.push(
40
+ createElement('span', { className: 'dfa-count dfa-count-modify' }, `${strings.modifyShort()} ${node.modify}`),
41
+ )
42
+ if (pills.length === 0) return null
43
+ return createElement('span', { className: 'dfa-counts', style: { paddingLeft: '6px' } }, ...pills)
44
+ }
24
45
 
25
- /** A stats-tree file row: icon + name + count pills + relative time. */
26
- const fileRow = (file, depth, onOpen) =>
27
- createElement(
28
- 'div',
29
- {
30
- key: file.abs,
31
- className: 'dfa-row',
32
- onClick: () => onOpen(file.abs),
33
- style: { paddingLeft: 8 + depth * 20 },
34
- title: fileTitle(file.abs, file.firstSeen, file.lastSeen),
35
- },
36
- createElement('span', { className: 'dfa-row-icon dfa-icon-file' }, icon.file(14)),
37
- createElement('span', { className: 'dfa-row-name dfa-name-file' }, file.name),
38
- countPills(file),
39
- file.lastSeen
40
- ? createElement('span', { className: 'dfa-time' }, formatRelative(file.lastSeen))
41
- : null,
42
- )
46
+ /** Extension of a file name (lowercase, no leading dot); '' when none.
47
+ * Dotfiles map to their whole name ('.gitignore' → 'gitignore') so the
48
+ * badge table can cover them; 'notes.' still yields ''. */
49
+ const extOf = (name) => {
50
+ const dot = name.lastIndexOf('.')
51
+ if (dot > 0) return name.slice(dot + 1).toLowerCase()
52
+ if (dot === 0) return name.slice(1).toLowerCase()
53
+ return ''
54
+ }
43
55
 
44
- /** One stats-tree node: file rows render inline, dirs toggle collapse. */
45
- function renderTreeNode(node, depth, collapsedDirs, onToggleDir, onOpen) {
46
- if (node.type === 'file') return fileRow(node, depth, onOpen)
47
- const collapsed = collapsedDirs.has(node.path)
48
- return createElement(
49
- 'div',
50
- { key: node.path },
51
- createElement(
52
- 'div',
53
- {
54
- className: 'dfa-row dfa-row-dir',
55
- onClick: () => onToggleDir(node.path),
56
- style: { paddingLeft: 8 + depth * 20 },
57
- title: `${node.path}/`,
58
- },
59
- createElement('span', { className: 'dfa-chevron' },
60
- collapsed ? icon.chevronRight(13) : icon.chevronDown(13),
61
- ),
62
- createElement('span', { className: 'dfa-row-icon dfa-icon-folder' }, icon.folder(14)),
63
- createElement('span', { className: 'dfa-row-name' },
64
- node.compressed ? node.name : node.name + '/',
65
- ),
66
- countPills(node),
67
- ),
68
- collapsed ? null : node.children.map((child) => renderTreeNode(child, depth + 1, collapsedDirs, onToggleDir, onOpen)),
69
- )
70
- }
56
+ /** Extension-less but common build files → their badge key. */
57
+ const NAME_BADGES = {
58
+ makefile: 'makefile',
59
+ dockerfile: 'dockerfile',
60
+ 'cmakelists.txt': 'cmake',
61
+ }
71
62
 
72
- /** A recent-list row: op badge + basename + relative time. */
73
- const recentEntry = (entry, onOpen) =>
74
- createElement(
75
- 'div',
76
- {
77
- key: `${entry.path}:${entry.time}:${entry.op}`,
78
- className: 'dfa-row',
79
- onClick: () => onOpen(entry.path),
80
- title: entry.path,
81
- },
82
- createElement('span', { className: `dfa-op ${opClass(entry.op)}` }, opLabel(entry.op)),
83
- createElement('span', { className: 'dfa-row-name' }, basenameOf(entry.path)),
84
- createElement('span', { className: 'dfa-time' }, formatRelative(entry.time)),
85
- )
63
+ /** Badge key for a file name: basename match first, then extension. */
64
+ const badgeKeyOf = (name) => {
65
+ const base = name.toLowerCase()
66
+ const named = NAME_BADGES[base]
67
+ if (named !== undefined) return named
68
+ return extOf(name)
69
+ }
70
+
71
+ /** A stats-tree file row: icon + name + count pills + relative time. */
72
+ const fileRow = (file, depth, onOpen) =>
73
+ createElement(
74
+ 'div',
75
+ {
76
+ key: file.abs,
77
+ className: 'dfa-row',
78
+ onClick: () => onOpen(file.abs),
79
+ style: { paddingLeft: 8 + depth * 20 },
80
+ title: fileTitle(file.abs, file.firstSeen, file.lastSeen),
81
+ },
82
+ createElement('span', { className: 'dfa-row-icon dfa-icon-file' }, fileIconByExt(badgeKeyOf(file.name))),
83
+ createElement('span', { className: 'dfa-row-name dfa-name-file' }, file.name),
84
+ countPills(file),
85
+ file.lastSeen ? createElement('span', { className: 'dfa-time' }, formatRelative(file.lastSeen)) : null,
86
+ )
87
+
88
+ /** One stats-tree node: file rows render inline, dirs toggle collapse. */
89
+ function renderTreeNode(node, depth, collapsedDirs, onToggleDir, onOpen) {
90
+ if (node.type === 'file') return fileRow(node, depth, onOpen)
91
+ const collapsed = collapsedDirs.has(node.path)
92
+ return createElement(
93
+ 'div',
94
+ { key: node.path },
95
+ createElement(
96
+ 'div',
97
+ {
98
+ className: 'dfa-row dfa-row-dir',
99
+ onClick: () => onToggleDir(node.path),
100
+ style: { paddingLeft: 8 + depth * 20 },
101
+ title: `${node.path}/`,
102
+ },
103
+ createElement('span', { className: 'dfa-chevron' }, collapsed ? icon.chevronRight(13) : icon.chevronDown(13)),
104
+ createElement('span', { className: 'dfa-row-icon dfa-icon-folder' }, icon.folder(14)),
105
+ createElement('span', { className: 'dfa-row-name' }, node.compressed ? node.name : node.name + '/'),
106
+ countPills(node),
107
+ ),
108
+ collapsed
109
+ ? null
110
+ : node.children.map((child) => renderTreeNode(child, depth + 1, collapsedDirs, onToggleDir, onOpen)),
111
+ )
112
+ }
86
113
 
87
- /** Toggle a key in a Set (directory collapse state). */
88
- function toggleInSet(set, key) {
89
- const next = new Set(set)
90
- if (next.has(key)) next.delete(key)
91
- else next.add(key)
92
- return next
93
- }
114
+ /** A recent-list row: op badge + basename + relative time. */
115
+ const recentEntry = (entry, onOpen) =>
116
+ createElement(
117
+ 'div',
118
+ {
119
+ key: `${entry.path}:${entry.time}:${entry.op}`,
120
+ className: 'dfa-row',
121
+ onClick: () => onOpen(entry.path),
122
+ title: entry.path,
123
+ },
124
+ createElement('span', { className: `dfa-op ${opClass(entry.op)}` }, opLabel(entry.op)),
125
+ createElement('span', { className: 'dfa-row-name' }, basenameOf(entry.path)),
126
+ createElement('span', { className: 'dfa-time' }, formatRelative(entry.time)),
127
+ )
94
128
 
95
- /** Clear the current session's records host-side and reset its bucket. */
96
- function clearSessionData(dataStore, sessionId) {
97
- if (!window.confirm(strings.clearConfirm())) return
98
- postClear(sessionId)
129
+ /** Toggle a key in a Set (directory collapse state). */
130
+ function toggleInSet(set, key) {
131
+ const next = new Set(set)
132
+ if (next.has(key)) next.delete(key)
133
+ else next.add(key)
134
+ return next
135
+ }
136
+
137
+ /** Clear the current session's records host-side and reset its bucket. */
138
+ function clearSessionData(dataStore, sessionId) {
139
+ if (!window.confirm(strings.clearConfirm())) return
140
+ postClear(sessionId)
141
+ const current = dataStore.getSnapshot()
142
+ dataStore.set({
143
+ bySession: {
144
+ ...(current.bySession ?? {}),
145
+ [sessionId]: { recent: [], counts: {}, loading: false },
146
+ },
147
+ })
148
+ }
149
+
150
+ /** Manual refresh: fetch stats + the authoritative cwd for this session. */
151
+ function refreshSessionData(dataStore, sessionId, setCwd, setError) {
152
+ if (sessionId === '') return
153
+ void fetchStats(sessionId)
154
+ .then((value) => {
155
+ if (value === null) return
156
+ setCwd((prev) => prev || value.cwd || '')
99
157
  const current = dataStore.getSnapshot()
100
158
  dataStore.set({
101
159
  bySession: {
102
160
  ...(current.bySession ?? {}),
103
- [sessionId]: { recent: [], counts: {}, loading: false },
161
+ [sessionId]: { recent: value.recent ?? [], counts: value.counts ?? {}, loading: false },
104
162
  },
105
163
  })
106
- }
107
-
108
- /** Manual refresh: fetch stats + the authoritative cwd for this session. */
109
- function refreshSessionData(dataStore, sessionId, setCwd, setError) {
110
- if (sessionId === '') return
111
- void fetchStats(sessionId).then((value) => {
112
- if (value === null) return
113
- setCwd((prev) => prev || value.cwd || '')
114
- const current = dataStore.getSnapshot()
115
- dataStore.set({
116
- bySession: {
117
- ...(current.bySession ?? {}),
118
- [sessionId]: { recent: value.recent ?? [], counts: value.counts ?? {}, loading: false },
119
- },
120
- })
121
- setError(false)
122
- }).catch(() => setError(true))
123
- void fetchSessionCwd(sessionId).then((cwd) => {
124
- if (cwd !== '') setCwd(cwd)
125
- })
126
- }
164
+ setError(false)
165
+ })
166
+ .catch(() => setError(true))
167
+ void fetchSessionCwd(sessionId).then((cwd) => {
168
+ if (cwd !== '') setCwd(cwd)
169
+ })
170
+ }
@@ -1,16 +1,16 @@
1
- // ── tiny external store ───────────────────────────────────────────────
2
- function createStore(initial) {
3
- let state = initial
4
- const listeners = new Set()
5
- return {
6
- getSnapshot: () => state,
7
- set(patch) {
8
- state = { ...state, ...patch }
9
- for (const listener of [...listeners]) listener()
10
- },
11
- subscribe(listener) {
12
- listeners.add(listener)
13
- return () => listeners.delete(listener)
14
- },
15
- }
16
- }
1
+ // ── tiny external store ───────────────────────────────────────────────
2
+ function createStore(initial) {
3
+ let state = initial
4
+ const listeners = new Set()
5
+ return {
6
+ getSnapshot: () => state,
7
+ set(patch) {
8
+ state = { ...state, ...patch }
9
+ for (const listener of [...listeners]) listener()
10
+ },
11
+ subscribe(listener) {
12
+ listeners.add(listener)
13
+ return () => listeners.delete(listener)
14
+ },
15
+ }
16
+ }
@@ -1,8 +1,8 @@
1
- // ── themed stylesheet (injected once per activation) ──────────────────
2
- // Mirrors the better-sidebar explorer surface: tight 2px 6px 8px body,
3
- // 30px rows, box-sizing border-box indentation, folder rows use the
4
- // strong type face to read as directories, files stay regular.
5
- const STYLES = `
1
+ // ── themed stylesheet (injected once per activation) ──────────────────
2
+ // Mirrors the better-sidebar explorer surface: tight 2px 6px 8px body,
3
+ // 30px rows, box-sizing border-box indentation, folder rows use the
4
+ // strong type face to read as directories, files stay regular.
5
+ const STYLES = `
6
6
  .dfa { display:flex; flex-direction:column; height:100%; overflow-y:auto; overflow-x:hidden;
7
7
  padding:2px 6px 8px; gap:2px; font:var(--dsw-font-s-14); color:var(--dsw-alias-label-primary); }
8
8
  .dfa-iconbtn { display:inline-flex; align-items:center; justify-content:center; width:24px; height:24px; padding:0;
@@ -74,4 +74,7 @@
74
74
  .dfa-pdf-download:hover { text-decoration:underline; }
75
75
  .dfa-pdf-frame { flex:1; min-height:0; width:100%; border:none; border-radius:6px; background:transparent; }
76
76
  @keyframes dfa-row-in { from { opacity:0; transform:translateY(1px); } to { opacity:1; transform:none; } }
77
+ /* issue #60: 移除 #25 的侧边栏页签选中态品牌蓝覆盖([class*="tab"][class*=
78
+ "tabActive"] 全局子串选择器误伤宿主对话/工作区 tab 选中态,出现用户不
79
+ 想要的蓝色高亮)。页签选中态回归宿主(dsh-better-sidebar)默认样式。 */
77
80
  `
@@ -1,71 +1,84 @@
1
- // ── directory tree construction ───────────────────────────────────────
2
- /**
3
- * Collapse chain directories: a directory whose only child is another
4
- * directory merges into it (a → a.b → a.b.c …). Deep single-child paths
5
- * render as one dotted label with the file(s) directly beneath.
6
- * `root` itself is never collapsed (its name is '' and would drop the
7
- * top-level directory).
8
- */
9
- function compressChains(node, isRoot) {
10
- for (const child of node.children) {
11
- if (child.type === 'dir') compressChains(child, false)
12
- }
13
- if (isRoot) return
14
- while (node.children.length === 1 && node.children[0].type === 'dir') {
15
- const only = node.children[0]
16
- node.name = `${node.name}.${only.name}`
17
- node.children = only.children
18
- node.compressed = true
19
- }
20
- }
1
+ // ── directory tree construction ───────────────────────────────────────
2
+ /**
3
+ * Collapse chain directories: a directory whose only child is another
4
+ * directory merges into it (a → a.b → a.b.c …). Deep single-child paths
5
+ * render as one dotted label with the file(s) directly beneath.
6
+ * `root` itself is never collapsed (its name is '' and would drop the
7
+ * top-level directory).
8
+ */
9
+ function compressChains(node, isRoot) {
10
+ for (const child of node.children) {
11
+ if (child.type === 'dir') compressChains(child, false)
12
+ }
13
+ if (isRoot) return
14
+ while (node.children.length === 1 && node.children[0].type === 'dir') {
15
+ const only = node.children[0]
16
+ node.name = `${node.name}.${only.name}`
17
+ node.children = only.children
18
+ node.compressed = true
19
+ }
20
+ }
21
21
 
22
- /**
23
- * Sort a directory node: directories first (alphabetically), then files
24
- * (by total activity, then name); recurse into directories.
25
- */
26
- function sortNode(node) {
27
- node.children.sort((a, b) => {
28
- if (a.type !== b.type) return a.type === 'dir' ? -1 : 1
29
- if (a.type === 'dir') return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
30
- const ta = a.read + a.create + a.modify
31
- const tb = b.read + b.create + b.modify
32
- return tb - ta || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
33
- })
34
- for (const child of node.children) {
35
- if (child.type === 'dir') sortNode(child)
36
- }
37
- }
22
+ /**
23
+ * Sort a directory node: directories first (alphabetically), then files
24
+ * (by total activity, then name); recurse into directories.
25
+ */
26
+ function sortNode(node) {
27
+ node.children.sort((a, b) => {
28
+ if (a.type !== b.type) return a.type === 'dir' ? -1 : 1
29
+ if (a.type === 'dir') return a.name < b.name ? -1 : a.name > b.name ? 1 : 0
30
+ const ta = a.read + a.create + a.modify
31
+ const tb = b.read + b.create + b.modify
32
+ return tb - ta || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)
33
+ })
34
+ for (const child of node.children) {
35
+ if (child.type === 'dir') sortNode(child)
36
+ }
37
+ }
38
38
 
39
- /**
40
- * Build a nested directory tree from per-file counts, keyed by the file's
41
- * absolute path. Every directory node aggregates its subtree counters and
42
- * sorts directories first (alphabetically), then files (by activity).
43
- */
44
- function buildTree(counts) {
45
- const root = { type: 'dir', name: '', path: '', children: [], read: 0, create: 0, modify: 0 }
46
- for (const [abs, counter] of Object.entries(counts)) {
47
- const parts = abs.split('/').filter((part) => part !== '')
48
- if (parts.length === 0) continue
49
- const name = parts[parts.length - 1]
50
- let node = root
51
- for (const dir of parts.slice(0, -1)) {
52
- let child = node.children.find((c) => c.type === 'dir' && c.name === dir)
53
- if (child === undefined) {
54
- child = { type: 'dir', name: dir, path: `${node.path}/${dir}`, children: [], read: 0, create: 0, modify: 0 }
55
- node.children.push(child)
56
- }
57
- node = child
58
- node.read += counter.read
59
- node.create += counter.create
60
- node.modify += counter.modify
39
+ /**
40
+ * Build a nested directory tree from per-file counts, keyed by the file's
41
+ * absolute path. Every directory node aggregates its subtree counters and
42
+ * sorts directories first (alphabetically), then files (by activity).
43
+ */
44
+ function buildTree(counts) {
45
+ const root = { type: 'dir', name: '', path: '', children: [], read: 0, create: 0, modify: 0 }
46
+ for (const [abs, counter] of Object.entries(counts)) {
47
+ const parts = abs.split('/').filter((part) => part !== '')
48
+ if (parts.length === 0) continue
49
+ const name = parts[parts.length - 1]
50
+ let node = root
51
+ for (const dir of parts.slice(0, -1)) {
52
+ let child = node.children.find((c) => c.type === 'dir' && c.name === dir)
53
+ if (child === undefined) {
54
+ child = {
55
+ type: 'dir',
56
+ name: dir,
57
+ path: `${node.path}/${dir}`,
58
+ children: [],
59
+ read: 0,
60
+ create: 0,
61
+ modify: 0,
61
62
  }
62
- node.children.push({
63
- type: 'file', name, abs,
64
- read: counter.read, create: counter.create, modify: counter.modify,
65
- firstSeen: counter.firstSeen, lastSeen: counter.lastSeen,
66
- })
63
+ node.children.push(child)
67
64
  }
68
- sortNode(root)
69
- compressChains(root, true)
70
- return root
65
+ node = child
66
+ node.read += counter.read
67
+ node.create += counter.create
68
+ node.modify += counter.modify
71
69
  }
70
+ node.children.push({
71
+ type: 'file',
72
+ name,
73
+ abs,
74
+ read: counter.read,
75
+ create: counter.create,
76
+ modify: counter.modify,
77
+ firstSeen: counter.firstSeen,
78
+ lastSeen: counter.lastSeen,
79
+ })
80
+ }
81
+ sortNode(root)
82
+ compressChains(root, true)
83
+ return root
84
+ }