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,129 +1,183 @@
1
- // ── view component ────────────────────────────────────────────────────
2
- /** Shared empty bucket for sessions that have never loaded data (stable ref). */
3
- const EMPTY_SESSION = { recent: [], counts: {}, loading: true }
1
+ // ── view component ────────────────────────────────────────────────────
2
+ /** Shared empty bucket for sessions that have never loaded data (stable ref). */
3
+ const EMPTY_SESSION = { recent: [], counts: {}, loading: true }
4
4
 
5
- /**
6
- * Polling loader for one session: fetches stats on mount and on a fixed
7
- * interval while visible, prefers the sidebar's authoritative session.cwd
8
- * for relative display, and writes results into the per-session bucket.
9
- */
10
- function useSessionLoader(visible, sessionId, scope, dataStore, setCwd, setError) {
11
- useEffect(() => {
12
- if (!visible || sessionId === '') return
13
- let cancelled = false
14
- const load = () => {
15
- void fetchStats(sessionId).then((value) => {
16
- if (cancelled || value === null) return
17
- setCwd((prev) => prev || value.cwd || '')
18
- const current = dataStore.getSnapshot()
19
- dataStore.set({
20
- bySession: {
21
- ...(current.bySession ?? {}),
22
- [sessionId]: { recent: value.recent ?? [], counts: value.counts ?? {}, loading: false },
5
+ /**
6
+ * Polling loader for one session: fetches stats on mount and on a fixed
7
+ * interval while visible, prefers the sidebar's authoritative session.cwd
8
+ * for relative display, and writes results into the per-session bucket.
9
+ */
10
+ function useSessionLoader(visible, sessionId, scope, dataStore, setCwd, setError) {
11
+ useEffect(() => {
12
+ if (!visible || sessionId === '') return
13
+ let cancelled = false
14
+ const load = () => {
15
+ void fetchStats(sessionId)
16
+ .then((value) => {
17
+ if (cancelled || value === null) return
18
+ setCwd((prev) => prev || value.cwd || '')
19
+ const current = dataStore.getSnapshot()
20
+ dataStore.set({
21
+ bySession: {
22
+ ...(current.bySession ?? {}),
23
+ [sessionId]: {
24
+ recent: value.recent ?? [],
25
+ counts: value.counts ?? {},
26
+ loading: false,
23
27
  },
24
- })
25
- setError(false)
26
- }).catch(() => {
27
- if (!cancelled) setError(true)
28
+ },
28
29
  })
29
- }
30
- load()
31
- void fetchSessionCwd(sessionId).then((cwd) => {
32
- if (!cancelled && cwd !== '') setCwd(cwd)
30
+ setError(false)
31
+ })
32
+ .catch(() => {
33
+ if (!cancelled) setError(true)
33
34
  })
34
- const timer = window.setInterval(load, POLL_MS)
35
- return () => {
36
- cancelled = true
37
- window.clearInterval(timer)
38
- }
39
- }, [visible, sessionId, dataStore])
40
35
  }
41
-
42
- /** Error banner element, or null when the last load succeeded. */
43
- function renderError(error) {
44
- if (!error) return null
45
- return createElement('div', { style: { color: 'var(--dsw-alias-state-error-primary)', padding: '4px 6px', font: 'var(--dsw-font-xxs-12)' } }, strings.loadError())
36
+ load()
37
+ void fetchSessionCwd(sessionId).then((cwd) => {
38
+ if (!cancelled && cwd !== '') setCwd(cwd)
39
+ })
40
+ const timer = window.setInterval(load, POLL_MS)
41
+ return () => {
42
+ cancelled = true
43
+ window.clearInterval(timer)
46
44
  }
45
+ }, [visible, sessionId, dataStore])
46
+ }
47
47
 
48
- /** "最近访问" section: collapsible head with refresh/clear actions. */
49
- function renderRecentSection(recent, recentOpen, onToggle, onRefresh, onClear, onOpen) {
50
- return createElement(
51
- 'div',
52
- { className: 'dfa-section' },
48
+ /** Error banner element, or null when the last load succeeded. */
49
+ function renderError(error) {
50
+ if (!error) return null
51
+ return createElement(
52
+ 'div',
53
+ {
54
+ style: {
55
+ color: 'var(--dsw-alias-state-error-primary)',
56
+ padding: '4px 6px',
57
+ font: 'var(--dsw-font-xxs-12)',
58
+ },
59
+ },
60
+ strings.loadError(),
61
+ )
62
+ }
63
+
64
+ /** "最近访问" section: collapsible head with refresh/clear actions. */
65
+ function renderRecentSection(recent, recentOpen, onToggle, onRefresh, onClear, onOpen) {
66
+ return createElement(
67
+ 'div',
68
+ { className: 'dfa-section' },
69
+ createElement(
70
+ 'div',
71
+ { className: 'dfa-section-head' },
72
+ createElement(
73
+ 'button',
74
+ { className: 'dfa-section-head-toggle', onClick: onToggle },
75
+ recentOpen ? icon.chevronDown(13) : icon.chevronRight(13),
76
+ strings.recent(),
77
+ ),
78
+ createElement(
79
+ 'span',
80
+ { className: 'dfa-section-head-actions' },
53
81
  createElement(
54
- 'div',
55
- { className: 'dfa-section-head' },
56
- createElement(
57
- 'button',
58
- { className: 'dfa-section-head-toggle', onClick: onToggle },
59
- recentOpen ? icon.chevronDown(13) : icon.chevronRight(13),
60
- strings.recent(),
61
- ),
62
- createElement('span', { className: 'dfa-section-head-actions' },
63
- createElement('button', { className: 'dfa-iconbtn dfa-iconbtn-xs', onClick: onRefresh, title: strings.refresh(), 'aria-label': strings.refresh() }, icon.refresh(14)),
64
- createElement('button', { className: 'dfa-iconbtn dfa-iconbtn-xs dfa-iconbtn-danger', onClick: onClear, title: strings.clear(), 'aria-label': strings.clear() }, icon.trash(14)),
65
- ),
82
+ 'button',
83
+ {
84
+ className: 'dfa-iconbtn dfa-iconbtn-xs',
85
+ onClick: onRefresh,
86
+ title: strings.refresh(),
87
+ 'aria-label': strings.refresh(),
88
+ },
89
+ icon.refresh(14),
66
90
  ),
67
- !recentOpen ? null : recent.length === 0
68
- ? createElement(
69
- 'div',
70
- { className: 'dfa-empty' },
71
- strings.empty(),
72
- createElement('span', { className: 'dfa-empty-hint' }, strings.emptyHint()),
73
- )
74
- : createElement('div', { className: 'dfa-list' }, recent.map((entry) => recentEntry(entry, onOpen))),
75
- )
76
- }
91
+ createElement(
92
+ 'button',
93
+ {
94
+ className: 'dfa-iconbtn dfa-iconbtn-xs dfa-iconbtn-danger',
95
+ onClick: onClear,
96
+ title: strings.clear(),
97
+ 'aria-label': strings.clear(),
98
+ },
99
+ icon.trash(14),
100
+ ),
101
+ ),
102
+ ),
103
+ !recentOpen
104
+ ? null
105
+ : recent.length === 0
106
+ ? createElement(
107
+ 'div',
108
+ { className: 'dfa-empty' },
109
+ strings.empty(),
110
+ createElement('span', { className: 'dfa-empty-hint' }, strings.emptyHint()),
111
+ )
112
+ : createElement(
113
+ 'div',
114
+ { className: 'dfa-list' },
115
+ recent.map((entry) => recentEntry(entry, onOpen)),
116
+ ),
117
+ )
118
+ }
77
119
 
78
- /** "文件统计" section: the directory tree, or an empty hint. */
79
- function renderStatsSection(tree, collapsedDirs, onToggleDir, onOpen) {
80
- return createElement(
81
- 'div',
82
- { className: 'dfa-section' },
83
- createElement('div', { className: 'dfa-section-head' }, strings.stats()),
84
- tree.children.length === 0
85
- ? createElement('div', { className: 'dfa-empty' }, strings.empty())
86
- : createElement('div', { className: 'dfa-list' }, tree.children.map((child) => renderTreeNode(child, 0, collapsedDirs, onToggleDir, onOpen))),
87
- )
88
- }
120
+ /** "文件统计" section: the directory tree, or an empty hint. */
121
+ function renderStatsSection(tree, collapsedDirs, onToggleDir, onOpen) {
122
+ return createElement(
123
+ 'div',
124
+ { className: 'dfa-section' },
125
+ createElement('div', { className: 'dfa-section-head' }, strings.stats()),
126
+ tree.children.length === 0
127
+ ? createElement('div', { className: 'dfa-empty' }, strings.empty())
128
+ : createElement(
129
+ 'div',
130
+ { className: 'dfa-list' },
131
+ tree.children.map((child) => renderTreeNode(child, 0, collapsedDirs, onToggleDir, onOpen)),
132
+ ),
133
+ )
134
+ }
89
135
 
90
- /**
91
- * The file-activity tab. Each session renders only its own store bucket:
92
- * a fresh conversation shows an empty list immediately, with no residue
93
- * from the previous session. Clicking any file opens a FLOATING preview
94
- * that reuses the sidebar's NATIVE viewer via matchFileViewer.
95
- */
96
- function FileActivityView({ ctx, store, scope, visible, dataStore }) {
97
- const data = useSyncExternalStore(dataStore.subscribe, dataStore.getSnapshot)
98
- const [cwd, setCwd] = useState(scope?.cwd || '')
99
- const [error, setError] = useState(false)
100
- const [recentOpen, setRecentOpen] = useState(true)
101
- const [collapsedDirs, setCollapsedDirs] = useState(() => new Set())
102
- const sessionId = scope?.sessionId ?? ''
103
- const sessionData = (data.bySession ?? {})[sessionId] ?? EMPTY_SESSION
104
- const tree = useMemo(() => buildTree(sessionData.counts ?? {}), [sessionData.counts])
105
- useEffect(() => {
106
- if (scope?.cwd) setCwd(scope.cwd)
107
- }, [scope?.cwd])
108
- useSessionLoader(visible, sessionId, scope, dataStore, setCwd, setError)
109
- // Switching conversations closes any floating preview left open by the
110
- // previous session (preview is shared UI state; session data never
111
- // crosses sessions anymore).
112
- useEffect(() => {
113
- dataStore.set({ preview: null })
114
- }, [sessionId, dataStore])
115
- const toggleDir = (path) => setCollapsedDirs((prev) => toggleInSet(prev, path))
116
- const openPreview = (path) => dataStore.set({ preview: { abs: path, name: basenameOf(path) } })
117
- const closePreview = () => dataStore.set({ preview: null })
118
- const onClear = () => clearSessionData(dataStore, sessionId)
119
- const onRefresh = () => refreshSessionData(dataStore, sessionId, setCwd, setError)
120
- const recent = sessionData.recent ?? []
121
- return createElement('div', { className: 'dfa' },
122
- renderError(error),
123
- renderRecentSection(recent, recentOpen, () => setRecentOpen((v) => !v), onRefresh, onClear, openPreview),
124
- renderStatsSection(tree, collapsedDirs, toggleDir, openPreview),
125
- data.preview
126
- ? createElement(FloatingPreview, { ctx, store, scope, preview: data.preview, onClose: closePreview })
127
- : null,
128
- )
129
- }
136
+ /**
137
+ * The file-activity tab. Each session renders only its own store bucket:
138
+ * a fresh conversation shows an empty list immediately, with no residue
139
+ * from the previous session. Clicking any file opens a FLOATING preview
140
+ * that reuses the sidebar's NATIVE viewer via matchFileViewer.
141
+ */
142
+ function FileActivityView({ ctx, store, scope, visible, dataStore }) {
143
+ const data = useSyncExternalStore(dataStore.subscribe, dataStore.getSnapshot)
144
+ const [cwd, setCwd] = useState(scope?.cwd || '')
145
+ const [error, setError] = useState(false)
146
+ const [recentOpen, setRecentOpen] = useState(true)
147
+ const [collapsedDirs, setCollapsedDirs] = useState(() => new Set())
148
+ const sessionId = scope?.sessionId ?? ''
149
+ const sessionData = (data.bySession ?? {})[sessionId] ?? EMPTY_SESSION
150
+ const tree = useMemo(() => buildTree(sessionData.counts ?? {}), [sessionData.counts])
151
+ useEffect(() => {
152
+ if (scope?.cwd) setCwd(scope.cwd)
153
+ }, [scope?.cwd])
154
+ useSessionLoader(visible, sessionId, scope, dataStore, setCwd, setError)
155
+ // Switching conversations closes any floating preview left open by the
156
+ // previous session (preview is shared UI state; session data never
157
+ // crosses sessions anymore).
158
+ useEffect(() => {
159
+ dataStore.set({ preview: null })
160
+ }, [sessionId, dataStore])
161
+ const toggleDir = (path) => setCollapsedDirs((prev) => toggleInSet(prev, path))
162
+ const openPreview = (path) => dataStore.set({ preview: { abs: path, name: basenameOf(path) } })
163
+ const closePreview = () => dataStore.set({ preview: null })
164
+ const onClear = () => clearSessionData(dataStore, sessionId)
165
+ const onRefresh = () => refreshSessionData(dataStore, sessionId, setCwd, setError)
166
+ const recent = sessionData.recent ?? []
167
+ return createElement(
168
+ 'div',
169
+ { className: 'dfa' },
170
+ renderError(error),
171
+ renderRecentSection(recent, recentOpen, () => setRecentOpen((v) => !v), onRefresh, onClear, openPreview),
172
+ renderStatsSection(tree, collapsedDirs, toggleDir, openPreview),
173
+ data.preview
174
+ ? createElement(FloatingPreview, {
175
+ ctx,
176
+ store,
177
+ scope,
178
+ preview: data.preview,
179
+ onClose: closePreview,
180
+ })
181
+ : null,
182
+ )
183
+ }
package/lib/state.js CHANGED
@@ -38,13 +38,17 @@ export async function loadState(file) {
38
38
  /** Map a raw operation kind (tool name or client op) to 'read' | 'write' | 'edit' | 'delete'. */
39
39
  export function mapOp(op) {
40
40
  switch (op) {
41
- case 'write': return 'write'
41
+ case 'write':
42
+ return 'write'
42
43
  case 'edit':
43
- case 'str_replace_editor': return 'edit'
44
- case 'delete': return 'delete'
44
+ case 'str_replace_editor':
45
+ return 'edit'
46
+ case 'delete':
47
+ return 'delete'
45
48
  case 'read':
46
49
  case 'read_image':
47
- default: return 'read'
50
+ default:
51
+ return 'read'
48
52
  }
49
53
  }
50
54
 
@@ -91,8 +95,9 @@ function pushRecent(recent, path, op, time) {
91
95
 
92
96
  /** A record target is valid when both ids are non-empty strings (no NUL). */
93
97
  function isValidRecordTarget(sessionId, path) {
94
- return typeof sessionId === 'string' && sessionId !== ''
95
- && typeof path === 'string' && path !== '' && !path.includes('\0')
98
+ return (
99
+ typeof sessionId === 'string' && sessionId !== '' && typeof path === 'string' && path !== '' && !path.includes('\0')
100
+ )
96
101
  }
97
102
 
98
103
  /** 'write' → create/modify by the known-file registry; 'edit' → modify; else read. */
@@ -119,7 +124,8 @@ function bumpCount(counts, finalOp, firstSeen, timestamp) {
119
124
  * Returns { state, trimmed } where trimmed reports whether anything changed.
120
125
  */
121
126
  export function trimLoadedState(loaded) {
122
- if (loaded.sessions === undefined || loaded.sessions === null || typeof loaded.sessions !== 'object') loaded.sessions = {}
127
+ if (loaded.sessions === undefined || loaded.sessions === null || typeof loaded.sessions !== 'object')
128
+ loaded.sessions = {}
123
129
  let trimmed = false
124
130
  for (const session of Object.values(loaded.sessions)) {
125
131
  if (Array.isArray(session.recent)) {
@@ -150,7 +156,8 @@ export function trimLoadedState(loaded) {
150
156
  export function isRecordedPath(state, sessionId, path) {
151
157
  const session = state.sessions[sessionId]
152
158
  if (session === undefined) return false
153
- if (session.counts !== undefined && typeof session.counts[path] === 'object' && session.counts[path] !== null) return true
159
+ if (session.counts !== undefined && typeof session.counts[path] === 'object' && session.counts[path] !== null)
160
+ return true
154
161
  // Deleted files no longer exist on disk — a delete history entry must not
155
162
  // authorize media preview for them.
156
163
  if (Array.isArray(session.recent)) return session.recent.some((entry) => entry.path === path && entry.op !== 'delete')
package/lib/store.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * (tmp+rename) and a teardown flush. The exposed store object carries a live
5
5
  * `state` reference so route handlers always read the current document.
6
6
  */
7
- import { rename, writeFile } from 'node:fs/promises'
7
+ import { atomicWriteJson } from 'dsh-shared'
8
8
  import { applyRecord, createState, loadState, mapOp, stateFile, trimLoadedState } from './state.js'
9
9
 
10
10
  /** Build the per-apply store: { state, record, schedulePersist, dispose }. */
@@ -30,16 +30,9 @@ export function createStore(ctx) {
30
30
 
31
31
  /** Write the current state atomically (serialized through dirtyChain). */
32
32
  function persistNow(handle) {
33
- const snapshot = JSON.stringify(handle.store.state)
34
- const tmp = `${handle.file}.tmp-${process.pid}`
35
- handle.dirtyChain = handle.dirtyChain.then(async () => {
36
- try {
37
- await writeFile(tmp, snapshot, 'utf8')
38
- await rename(tmp, handle.file)
39
- } catch (error) {
40
- handle.ctx.logger?.warn(`[dsh-file-activity] persist failed: ${error instanceof Error ? error.message : String(error)}`)
41
- }
42
- }).catch(() => {})
33
+ handle.dirtyChain = handle.dirtyChain
34
+ .then(() => atomicWriteJson(handle.file, handle.store.state, handle.ctx.logger, '[dsh-file-activity]'))
35
+ .catch(() => {})
43
36
  }
44
37
 
45
38
  /** Debounced (500ms) schedule of a persist. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-file-activity",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "DSH 侧边栏文件活动插件:按 LRU 记录文件读取/新增/修改事件(最近访问),按绝对路径树形统计(文件统计),点击文件浮窗预览(代码高亮/Markdown/图片/PDF)。DSH web plugin: file activity tracker — LRU recent-access list plus per-file read/create/modify counts in a folder tree, with floating preview.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -46,7 +46,8 @@
46
46
  "peerDependencies": {
47
47
  "cordis": "^4.0.0-rc.8",
48
48
  "dsh-better-sidebar": "^0.14.0",
49
- "react": "^18.2.0"
49
+ "react": "^18.2.0 || ^19.2.0",
50
+ "dsh-shared": "^0.1.0"
50
51
  },
51
52
  "peerDependenciesMeta": {
52
53
  "dsh-better-sidebar": {
package/lib/fence.js DELETED
@@ -1,57 +0,0 @@
1
- /**
2
- * Trust fence for /file-activity routes: only loopback / explicitly trusted
3
- * hosts may call the plugin's HTTP endpoints (same behavioral contract as the
4
- * /api gateway). 403-refuses everything else, including cross-site requests.
5
- */
6
- function header(headers, name) {
7
- const value = headers[name]
8
- return typeof value === 'string' ? value : undefined
9
- }
10
-
11
- function parseAuthority(authority) {
12
- try {
13
- return new URL(`http://${authority}`)
14
- } catch {
15
- return undefined
16
- }
17
- }
18
-
19
- function isLoopbackHostname(hostname) {
20
- if (hostname === 'localhost' || hostname === '[::1]') return true
21
- const parts = hostname.split('.')
22
- return parts.length === 4
23
- && parts[0] === '127'
24
- && parts.every((part) => /^\d{1,3}$/.test(part) && Number(part) <= 255)
25
- }
26
-
27
- function canonicalAuthority(entry, entryUrl) {
28
- const port = entryUrl.port !== '' ? entryUrl.port : new URL(`https://${entry}`).port
29
- return port === '' ? entryUrl.hostname : `${entryUrl.hostname}:${port}`
30
- }
31
-
32
- function isTrustedAuthority(hostUrl, trustedHosts) {
33
- return trustedHosts.some((entry) => {
34
- const entryUrl = parseAuthority(entry)
35
- if (entryUrl === undefined) return false
36
- return canonicalAuthority(entry, entryUrl) === entryUrl.hostname
37
- ? entryUrl.hostname === hostUrl.hostname
38
- : entryUrl.host === hostUrl.host
39
- })
40
- }
41
-
42
- /** Host-header trust fence (same behavioral contract as the /api gateway). */
43
- export function isTrustedApiRequest(request, trustedHosts) {
44
- const host = header(request.headers, 'host')
45
- if (host === undefined) return false
46
- const hostUrl = parseAuthority(host)
47
- if (hostUrl === undefined) return false
48
- if (!isLoopbackHostname(hostUrl.hostname) && !isTrustedAuthority(hostUrl, trustedHosts)) return false
49
- if (header(request.headers, 'sec-fetch-site') === 'cross-site') return false
50
- const origin = header(request.headers, 'origin')
51
- if (origin === undefined) return true
52
- try {
53
- return new URL(origin).host === hostUrl.host
54
- } catch {
55
- return false
56
- }
57
- }
package/lib/http.js DELETED
@@ -1,34 +0,0 @@
1
- /**
2
- * HTTP helpers shared by the /file-activity routes: bounded JSON body
3
- * reading, JSON responses, and session working-directory resolution.
4
- */
5
-
6
- /** Read a JSON request body (bounded). */
7
- export async function readJsonBody(request) {
8
- let body = ''
9
- for await (const chunk of request) {
10
- body += chunk
11
- if (body.length > 1_000_000) throw new Error('request body too large')
12
- }
13
- if (body === '') return {}
14
- return JSON.parse(body)
15
- }
16
-
17
- export function writeJson(response, status, value) {
18
- const payload = JSON.stringify(value)
19
- response.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-cache' })
20
- response.end(payload)
21
- }
22
-
23
- export function writeError(response, error) {
24
- const message = error instanceof Error ? error.message : String(error)
25
- writeJson(response, 400, { ok: false, error: { message } })
26
- }
27
-
28
- /** Session working directory, mirroring better-sidebar's resolution. */
29
- export function sessionCwdOf(ctx, sessionId) {
30
- const session = ctx.sessions.get(sessionId)
31
- const headerCwd = session?.header?.cwd
32
- if (typeof headerCwd === 'string' && headerCwd !== '') return headerCwd
33
- return process.cwd()
34
- }
@@ -1,48 +0,0 @@
1
- // ── icons (inline, stroke=currentColor, matching better-sidebar) ──────
2
- const ICON_STROKE = 1.8
3
- const iconSvg = (children, size) =>
4
- createElement('svg', {
5
- width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
6
- stroke: 'currentColor', strokeWidth: ICON_STROKE, strokeLinecap: 'round', strokeLinejoin: 'round',
7
- 'aria-hidden': 'true',
8
- }, children.map((child, i) => (child === null || child === undefined || typeof child === 'boolean')
9
- ? child
10
- : createElement(child.type, { key: i, ...child.props })))
11
-
12
- const icon = {
13
- clock: (size = 16) => iconSvg([
14
- createElement('circle', { cx: 12, cy: 12, r: 9 }),
15
- createElement('path', { d: 'M12 7v5l3 2' }),
16
- ], size),
17
- refresh: (size = 16) => iconSvg([
18
- createElement('path', { d: 'M21 12a9 9 0 1 1-2.64-6.36' }),
19
- createElement('polyline', { points: '21 3 21 9 15 9' }),
20
- ], size),
21
- trash: (size = 16) => iconSvg([
22
- createElement('path', { d: 'M3 6h18' }),
23
- createElement('path', { d: 'M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6' }),
24
- createElement('path', { d: 'M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2' }),
25
- ], size),
26
- chevronRight: (size = 14) => iconSvg([
27
- createElement('polyline', { points: '9 6 15 12 9 18' }),
28
- ], size),
29
- chevronDown: (size = 14) => iconSvg([
30
- createElement('polyline', { points: '6 9 12 15 18 9' }),
31
- ], size),
32
- file: (size = 16) => iconSvg([
33
- createElement('path', { d: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z' }),
34
- createElement('path', { d: 'M14 2v6h6' }),
35
- ], size),
36
- folder: (size = 16) => iconSvg([
37
- createElement('path', { d: 'M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z' }),
38
- ], size),
39
- external: (size = 15) => iconSvg([
40
- createElement('path', { d: 'M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6' }),
41
- createElement('polyline', { points: '15 3 21 3 21 9' }),
42
- createElement('line', { x1: 10, y1: 14, x2: 21, y2: 3 }),
43
- ], size),
44
- close: (size = 15) => iconSvg([
45
- createElement('line', { x1: 18, y1: 6, x2: 6, y2: 18 }),
46
- createElement('line', { x1: 6, y1: 6, x2: 18, y2: 18 }),
47
- ], size),
48
- }