dsh-file-activity 0.5.10 → 0.5.12

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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  本文件记录 dsh-file-activity 的所有版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
4
4
 
5
+ ## [0.5.12] - 2026-09-18
6
+
7
+ ### 变更
8
+
9
+ - feat(file-activity): 右侧边栏默认宽度配置(默认 20%)
10
+
11
+ ## [0.5.11] - 2026-09-18
12
+
13
+ ### 变更
14
+
15
+ - feat(file-activity): 右侧边栏默认宽度配置(默认 20%)
16
+
5
17
  ## [0.5.10] - 2026-09-16
6
18
 
7
19
  ### 变更
package/README.md CHANGED
@@ -52,6 +52,9 @@ dsh plugin --profile web add link:<仓库路径>/plugins/dsh-file-activity
52
52
  | 配置项 | 默认 | 作用 |
53
53
  | ------------------------------------------------ | ---- | -------------------------------------------------------------------------- |
54
54
  | 会话开始时自动打开(**设置 → 插件 → 文件活动**) | 开 | 每个会话首次打开时自动显示本页;关闭后仍可从侧边栏「新标签页」菜单手动打开 |
55
+ | 右侧边栏默认宽度(**设置 → 插件 → 文件活动**) | 20% | 启动时按此比例展开右侧边栏(10%–70%);手动拖拽过宽度后本次运行不再覆盖 |
56
+
57
+ > 宽度偏好只在你**尚未拖拽**时生效(宿主自己给的 45% 默认值也算「已拖拽」之外的空档)。宿主右侧栏下限为 300px,窗口窄于约 1500px 时 20% 会被抬高到 300px——这是宿主 clamp,非本插件行为。
55
58
 
56
59
  ## 相关文档
57
60
 
package/lib/client.js CHANGED
@@ -46,6 +46,8 @@ window.__ModuleLoader__.load({
46
46
  const PREVIEW_ID = 'dsh-file-activity-preview'
47
47
  const AUTO_OPEN_KEY = 'dsh-file-activity:auto-opened:'
48
48
  const AUTO_OPEN_PREF_KEY = 'dsh-file-activity:autoOpen'
49
+ // Right column default width ratio in percent (issue #384).
50
+ const RIGHTBAR_PREF_KEY = 'dsh-file-activity:rightbarWidth'
49
51
  const POLL_MS = 6000
50
52
 
51
53
  // ── parts (injected by scripts/build.mjs; keep this exact order — the
@@ -104,6 +106,11 @@ const strings = {
104
106
  isZh()
105
107
  ? '每个会话首次打开时自动显示本页;关闭后仍可从侧边栏右上角的「新标签页」手动打开。'
106
108
  : 'Shows this page once per session. When off, open it from the sidebar new-tab control.',
109
+ rightbarWidthLabel: () => (isZh() ? '右侧边栏默认宽度' : 'Right sidebar default width'),
110
+ rightbarWidthHint: () =>
111
+ isZh()
112
+ ? '启动时右侧边栏按此比例展开(10%–70%)。手动拖拽过宽度后不再覆盖;宿主最小宽度 300px,窄窗口下实际会更宽。'
113
+ : 'Opens the right sidebar at this ratio (10%–70%). A manual drag wins for the rest of the run; the host floor is 300px, so narrow windows land wider.',
107
114
  tabUnavailable: () => (isZh() ? '侧边栏扩展点不可用' : 'Sidebar extension point unavailable'),
108
115
  tabUnavailableHint: () =>
109
116
  isZh()
@@ -986,7 +993,7 @@ const STYLES = `
986
993
  border:1px solid var(--dsw-alias-state-error-primary); background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent); }
987
994
  .dfa-degraded-title { font:var(--dsw-font-xs-13); color:var(--dsw-alias-state-error-primary); }
988
995
  .dfa-degraded-hint { margin-top:2px; font:var(--dsw-font-xxs-12); opacity:.8; }
989
- /* 设置页(settings.plugins.tab):开关行。 */
996
+ /* 设置页(settings.plugins.tab):开关行 + 百分比输入行(issue #384)。 */
990
997
  .dfa-set { display:flex; flex-direction:column; gap:10px; padding:10px 2px; }
991
998
  .dfa-set-title { font:var(--dsw-font-sm-14); }
992
999
  .dfa-set-row { display:flex; align-items:center; justify-content:space-between; gap:12px; cursor:pointer; }
@@ -994,6 +1001,12 @@ const STYLES = `
994
1001
  .dfa-set-label { font:var(--dsw-font-xs-13); }
995
1002
  .dfa-set-hint { font:var(--dsw-font-xxs-12); opacity:.75; }
996
1003
  .dfa-set-switch { flex:none; width:16px; height:16px; accent-color:var(--dsw-alias-accent); cursor:pointer; }
1004
+ .dfa-set-row-static { cursor:default; }
1005
+ .dfa-set-field { flex:none; display:inline-flex; align-items:center; gap:4px; cursor:default; }
1006
+ .dfa-set-number { box-sizing:border-box; width:60px; padding:4px 6px; text-align:right; color:inherit;
1007
+ background:transparent; border:1px solid var(--dsw-alias-border-l1); border-radius:6px; font:var(--dsw-font-xs-13); }
1008
+ .dfa-set-number:focus { outline:none; border-color:var(--dsw-alias-accent); }
1009
+ .dfa-set-unit { font:var(--dsw-font-xs-13); opacity:.75; }
997
1010
  /* 浮窗正文:Markdown / 代码 / 纯文本 / 图片四种渲染体。 */
998
1011
  .dfa-fp-md { height:100%; overflow:auto; padding:2px 4px; }
999
1012
  .dfa-fp-code { height:100%; overflow:auto; }
@@ -1798,6 +1811,183 @@ function PdfPreview({ src, download, title }) {
1798
1811
  )
1799
1812
  }
1800
1813
 
1814
+ 'use strict'
1815
+ // ── right column default width (issue #384) ───────────────────────────
1816
+ //
1817
+ // Why this half exists: the host hardcodes the right column's first-open width
1818
+ // at 45% of the frame (ui-layout columns.ts RIGHTBAR_DEFAULT_RATIO) and
1819
+ // persists nothing, and it exposes NO width face on ctx.layout or
1820
+ // ctx.sidebarRight. The one reachable channel is the layout entry's declared
1821
+ // store seat: ctx.slots.entries('root') yields the ui-layout registration,
1822
+ // whose store.create() returns the very instance AppFrame subscribes to (the
1823
+ // framework itself goes through handle.create()).
1824
+ //
1825
+ // Semantics = DEFAULT, not override: the host fills layoutInfo.rightbar with
1826
+ // the 45% value on the panel's first opening (stores.ts rightbar ??= …), so
1827
+ // `rightbar === null` means "nothing opened or dragged it in this run". We
1828
+ // write ONLY then; a value the user dragged is never touched.
1829
+ //
1830
+ // Every contract mismatch (no entry, factory-shaped store, no create(), a
1831
+ // throwing create()/getSnapshot(), no setRightbar, an unexpected snapshot) is
1832
+ // a SILENT skip with at most one debug line. Breaking the tab, the auto-open
1833
+ // or the preview over a nicety like this would be far worse than doing
1834
+ // nothing, so the whole path is detect-and-degrade.
1835
+ /** Allowed ratio range in percent; the ceiling matches the host's own cap (70%). */
1836
+ const RIGHTBAR_RATIO_MIN = 10
1837
+ const RIGHTBAR_RATIO_MAX = 70
1838
+ /** Ratio used when nothing valid is stored: the issue #384 default. */
1839
+ const RIGHTBAR_RATIO_DEFAULT = 20
1840
+ /** One-shot skip log gate, so a churning root seat cannot flood the console. */
1841
+ let rightbarSkipLogged = false
1842
+ /** Debug-only skip report; never throws, never warns (degradation is silent). */
1843
+ function logRightbarSkip(reason, error) {
1844
+ if (rightbarSkipLogged) return
1845
+ rightbarSkipLogged = true
1846
+ try {
1847
+ if (typeof console !== 'undefined' && typeof console.debug === 'function') {
1848
+ console.debug('[dsh-file-activity] 右侧边栏默认宽度未应用: ' + reason, error ?? '')
1849
+ }
1850
+ } catch {
1851
+ // logging must never break activation
1852
+ }
1853
+ }
1854
+ /** Whether a raw value is a usable percent ratio (finite, inside the range). */
1855
+ function rightbarRatioValid(value) {
1856
+ const ratio = Number(value)
1857
+ return Number.isFinite(ratio) && ratio >= RIGHTBAR_RATIO_MIN && ratio <= RIGHTBAR_RATIO_MAX
1858
+ }
1859
+ /** Persisted ratio, falling back to the default for missing/illegal values. */
1860
+ function storedRightbarRatio() {
1861
+ let raw
1862
+ try {
1863
+ raw = window.localStorage.getItem(RIGHTBAR_PREF_KEY)
1864
+ } catch {
1865
+ return RIGHTBAR_RATIO_DEFAULT
1866
+ }
1867
+ if (raw === null || !rightbarRatioValid(raw)) return RIGHTBAR_RATIO_DEFAULT
1868
+ return Number(raw)
1869
+ }
1870
+ /** Persist a ratio; an illegal value is rejected instead of poisoning storage. */
1871
+ function saveRightbarRatio(value) {
1872
+ if (!rightbarRatioValid(value)) return false
1873
+ try {
1874
+ window.localStorage.setItem(RIGHTBAR_PREF_KEY, String(Number(value)))
1875
+ } catch {
1876
+ // storage unavailable (private mode): the control still shows the value
1877
+ }
1878
+ return true
1879
+ }
1880
+ /**
1881
+ * The entry's store seat in HANDLE form (create() present), or null.
1882
+ * A factory-shaped seat is deliberately not guessed at: calling it would build
1883
+ * a second, unsubscribed instance instead of the one AppFrame renders.
1884
+ * @param entry - one 'root' seat snapshot.
1885
+ * @returns the handle-shaped seat, or null.
1886
+ */
1887
+ function rightbarStoreSeat(entry) {
1888
+ const store = entry && entry.store
1889
+ if (!store || typeof store !== 'object') return null
1890
+ if (typeof store.create !== 'function') return null
1891
+ return store
1892
+ }
1893
+ /**
1894
+ * The instance's layoutInfo when it is writable, or null.
1895
+ * @param instance - handle-created store instance.
1896
+ * @returns layoutInfo, or null when the shape is foreign or a width exists.
1897
+ */
1898
+ function rightbarWritableInfo(instance) {
1899
+ if (typeof instance.getSnapshot !== 'function') return null
1900
+ const snapshot = instance.getSnapshot()
1901
+ const info = snapshot && snapshot.layoutInfo
1902
+ if (!info || typeof info !== 'object') return null
1903
+ // NULL is the only green light: any number means this run already owns a
1904
+ // width (the host's own 45% first-open default, or a user drag).
1905
+ if (info.rightbar !== null) return null
1906
+ return info
1907
+ }
1908
+ /**
1909
+ * Turn a handle-created instance into a write target, or null.
1910
+ * @param instance - handle-created store instance.
1911
+ * @returns the write target, or null.
1912
+ */
1913
+ function rightbarTargetFromInstance(instance) {
1914
+ if (!instance) return null
1915
+ const info = rightbarWritableInfo(instance)
1916
+ if (!info) return null
1917
+ if (!instance.actions || typeof instance.actions.setRightbar !== 'function') return null
1918
+ return { setRightbar: instance.actions.setRightbar, info }
1919
+ }
1920
+ /**
1921
+ * Resolve the ui-layout instance behind the 'root' seat, or null when the host
1922
+ * contract does not match. The snapshot shape is the test: writing the width
1923
+ * goes through exactly that shape, so a shape we cannot read is a channel we
1924
+ * must not use.
1925
+ * @param ctx - client root context.
1926
+ * @returns the write target, or null (caller stays silent).
1927
+ */
1928
+ function rightbarLayoutTarget(ctx) {
1929
+ const slots = ctx && ctx.slots
1930
+ if (!slots || typeof slots.entries !== 'function') {
1931
+ logRightbarSkip('ctx.slots.entries 不可用')
1932
+ return null
1933
+ }
1934
+ const entries = slots.entries('root') || []
1935
+ for (const entry of entries) {
1936
+ try {
1937
+ const target = rightbarTargetFromInstance(rightbarStoreSeat(entry)?.create?.())
1938
+ if (target) return target
1939
+ } catch (error) {
1940
+ logRightbarSkip('读取 root 席位 store 失败', error)
1941
+ }
1942
+ }
1943
+ logRightbarSkip('未找到可写的 ui-layout root 席位')
1944
+ return null
1945
+ }
1946
+ /**
1947
+ * Apply the configured default width once, if the panel has no width yet.
1948
+ * @param ctx - client root context.
1949
+ */
1950
+ function applyRightbarDefaultWidth(ctx) {
1951
+ try {
1952
+ const target = rightbarLayoutTarget(ctx)
1953
+ if (!target) return
1954
+ // The snapshot's own viewportWidth is the value the host's clamp uses, so
1955
+ // prefer it; window.innerWidth only covers a snapshot that omits it.
1956
+ const viewport = Number.isFinite(target.info.viewportWidth) ? target.info.viewportWidth : window.innerWidth
1957
+ if (!Number.isFinite(viewport)) {
1958
+ logRightbarSkip('viewportWidth 不可用')
1959
+ return
1960
+ }
1961
+ const px = Math.round(viewport * (storedRightbarRatio() / 100))
1962
+ if (!Number.isFinite(px) || px <= 0) {
1963
+ logRightbarSkip('计算出的宽度非法')
1964
+ return
1965
+ }
1966
+ target.setRightbar(px)
1967
+ } catch (error) {
1968
+ logRightbarSkip('应用宽度失败', error)
1969
+ }
1970
+ }
1971
+ /**
1972
+ * Install the preference: apply once at activation, then re-resolve the
1973
+ * instance after a 'root' seat change (HMR / remount must not keep a stale
1974
+ * instance). The returned disposer releases the subscription with the fiber.
1975
+ * @param ctx - client root context.
1976
+ * @returns the subscription disposer, when one was taken.
1977
+ */
1978
+ function installRightbarDefaultWidth(ctx) {
1979
+ applyRightbarDefaultWidth(ctx)
1980
+ const slots = ctx && ctx.slots
1981
+ if (!slots || typeof slots.subscribe !== 'function') return undefined
1982
+ try {
1983
+ const dispose = slots.subscribe('root', () => applyRightbarDefaultWidth(ctx))
1984
+ return typeof dispose === 'function' ? () => dispose() : undefined
1985
+ } catch (error) {
1986
+ logRightbarSkip('订阅 root 席位失败', error)
1987
+ return undefined
1988
+ }
1989
+ }
1990
+
1801
1991
  'use strict'
1802
1992
  // ── settings tab (replaces 迁移前的 settings.pluginToggles) ────
1803
1993
  //
@@ -1843,9 +2033,40 @@ function settingsRow(label, hint, checked, onChange) {
1843
2033
  }),
1844
2034
  )
1845
2035
  }
1846
- /** Settings panel body: the auto-open switch plus its explanatory hint. */
2036
+ /** One labeled percent row (issue #384: the right column's default width). */
2037
+ function settingsPercentRow(label, hint, value, onCommit) {
2038
+ return createElement(
2039
+ 'div',
2040
+ { className: 'dfa-set-row dfa-set-row-static' },
2041
+ createElement(
2042
+ 'span',
2043
+ { className: 'dfa-set-text' },
2044
+ createElement('span', { className: 'dfa-set-label' }, label),
2045
+ createElement('span', { className: 'dfa-set-hint' }, hint),
2046
+ ),
2047
+ createElement(
2048
+ 'span',
2049
+ { className: 'dfa-set-field' },
2050
+ createElement('input', {
2051
+ type: 'number',
2052
+ className: 'dfa-set-number',
2053
+ min: RIGHTBAR_RATIO_MIN,
2054
+ max: RIGHTBAR_RATIO_MAX,
2055
+ step: 1,
2056
+ value,
2057
+ onChange: (event) => onCommit(event?.target?.value),
2058
+ }),
2059
+ createElement('span', { className: 'dfa-set-unit' }, '%'),
2060
+ ),
2061
+ )
2062
+ }
2063
+ /**
2064
+ * Settings panel body: the auto-open switch, the right-sidebar default width,
2065
+ * and their explanatory hints.
2066
+ */
1847
2067
  function FileActivitySettings() {
1848
2068
  const [enabled, setEnabled] = useState(autoOpenEnabled)
2069
+ const [ratio, setRatio] = useState(storedRightbarRatio)
1849
2070
  return createElement(
1850
2071
  'div',
1851
2072
  { className: 'dfa-set', 'data-dfa-settings': '1' },
@@ -1854,6 +2075,12 @@ function FileActivitySettings() {
1854
2075
  setAutoOpenEnabled(next)
1855
2076
  setEnabled(next)
1856
2077
  }),
2078
+ settingsPercentRow(strings.rightbarWidthLabel(), strings.rightbarWidthHint(), ratio, (raw) => {
2079
+ // An illegal / out-of-range edit is rejected outright: the control keeps
2080
+ // showing the stored preference instead of a value we would not apply.
2081
+ if (!saveRightbarRatio(raw)) return
2082
+ setRatio(Number(raw))
2083
+ }),
1857
2084
  )
1858
2085
  }
1859
2086
  /** Register the settings tab into the settings section's list seat. */
@@ -1967,6 +2194,12 @@ exports.apply = function apply(ctx) {
1967
2194
  registerPreviewOverlay(ctx, dataStore)
1968
2195
  registerSettingsTab(ctx)
1969
2196
  guarded('previews', () => registerDocumentPreviews(ctx))
2197
+ // Right column default width (issue #384) BEFORE the auto-open below: the
2198
+ // host fills its own 45% default the moment the panel first opens
2199
+ // (stores.ts `rightbar ??= …`), so whoever writes first wins. Auto-opening
2200
+ // our tab opens that panel — applying the width afterwards would always
2201
+ // find a non-null preference and never take effect.
2202
+ ctx.effect(() => installRightbarDefaultWidth(ctx), 'dsh-file-activity: rightbar default width')
1970
2203
  // sidebar operations → host record route
1971
2204
  ctx.effect(() => installFetchInterceptor(), 'dsh-file-activity: sidebar fetch observation')
1972
2205
  // auto-open once per session (default on)
package/lib/client.src.js CHANGED
@@ -46,6 +46,8 @@ window.__ModuleLoader__.load({
46
46
  const PREVIEW_ID = 'dsh-file-activity-preview'
47
47
  const AUTO_OPEN_KEY = 'dsh-file-activity:auto-opened:'
48
48
  const AUTO_OPEN_PREF_KEY = 'dsh-file-activity:autoOpen'
49
+ // Right column default width ratio in percent (issue #384).
50
+ const RIGHTBAR_PREF_KEY = 'dsh-file-activity:rightbarWidth'
49
51
  const POLL_MS = 6000
50
52
 
51
53
  // ── parts (injected by scripts/build.mjs; keep this exact order — the
@@ -64,6 +66,7 @@ window.__ModuleLoader__.load({
64
66
  __PART_VIEW__
65
67
  __PART_PREVIEW_DATA__
66
68
  __PART_PREVIEW__
69
+ __PART_RIGHTBAR_WIDTH__
67
70
  __PART_SETTINGS__
68
71
  __PART_APPLY__
69
72
 
@@ -95,6 +95,12 @@ exports.apply = function apply(ctx) {
95
95
  registerPreviewOverlay(ctx, dataStore)
96
96
  registerSettingsTab(ctx)
97
97
  guarded('previews', () => registerDocumentPreviews(ctx))
98
+ // Right column default width (issue #384) BEFORE the auto-open below: the
99
+ // host fills its own 45% default the moment the panel first opens
100
+ // (stores.ts `rightbar ??= …`), so whoever writes first wins. Auto-opening
101
+ // our tab opens that panel — applying the width afterwards would always
102
+ // find a non-null preference and never take effect.
103
+ ctx.effect(() => installRightbarDefaultWidth(ctx), 'dsh-file-activity: rightbar default width')
98
104
  // sidebar operations → host record route
99
105
  ctx.effect(() => installFetchInterceptor(), 'dsh-file-activity: sidebar fetch observation')
100
106
  // auto-open once per session (default on)
@@ -52,6 +52,11 @@ const strings = {
52
52
  isZh()
53
53
  ? '每个会话首次打开时自动显示本页;关闭后仍可从侧边栏右上角的「新标签页」手动打开。'
54
54
  : 'Shows this page once per session. When off, open it from the sidebar new-tab control.',
55
+ rightbarWidthLabel: () => (isZh() ? '右侧边栏默认宽度' : 'Right sidebar default width'),
56
+ rightbarWidthHint: () =>
57
+ isZh()
58
+ ? '启动时右侧边栏按此比例展开(10%–70%)。手动拖拽过宽度后不再覆盖;宿主最小宽度 300px,窄窗口下实际会更宽。'
59
+ : 'Opens the right sidebar at this ratio (10%–70%). A manual drag wins for the rest of the run; the host floor is 300px, so narrow windows land wider.',
55
60
  tabUnavailable: () => (isZh() ? '侧边栏扩展点不可用' : 'Sidebar extension point unavailable'),
56
61
  tabUnavailableHint: () =>
57
62
  isZh()
@@ -0,0 +1,176 @@
1
+ 'use strict'
2
+ // ── right column default width (issue #384) ───────────────────────────
3
+ //
4
+ // Why this half exists: the host hardcodes the right column's first-open width
5
+ // at 45% of the frame (ui-layout columns.ts RIGHTBAR_DEFAULT_RATIO) and
6
+ // persists nothing, and it exposes NO width face on ctx.layout or
7
+ // ctx.sidebarRight. The one reachable channel is the layout entry's declared
8
+ // store seat: ctx.slots.entries('root') yields the ui-layout registration,
9
+ // whose store.create() returns the very instance AppFrame subscribes to (the
10
+ // framework itself goes through handle.create()).
11
+ //
12
+ // Semantics = DEFAULT, not override: the host fills layoutInfo.rightbar with
13
+ // the 45% value on the panel's first opening (stores.ts rightbar ??= …), so
14
+ // `rightbar === null` means "nothing opened or dragged it in this run". We
15
+ // write ONLY then; a value the user dragged is never touched.
16
+ //
17
+ // Every contract mismatch (no entry, factory-shaped store, no create(), a
18
+ // throwing create()/getSnapshot(), no setRightbar, an unexpected snapshot) is
19
+ // a SILENT skip with at most one debug line. Breaking the tab, the auto-open
20
+ // or the preview over a nicety like this would be far worse than doing
21
+ // nothing, so the whole path is detect-and-degrade.
22
+ /** Allowed ratio range in percent; the ceiling matches the host's own cap (70%). */
23
+ const RIGHTBAR_RATIO_MIN = 10
24
+ const RIGHTBAR_RATIO_MAX = 70
25
+ /** Ratio used when nothing valid is stored: the issue #384 default. */
26
+ const RIGHTBAR_RATIO_DEFAULT = 20
27
+ /** One-shot skip log gate, so a churning root seat cannot flood the console. */
28
+ let rightbarSkipLogged = false
29
+ /** Debug-only skip report; never throws, never warns (degradation is silent). */
30
+ function logRightbarSkip(reason, error) {
31
+ if (rightbarSkipLogged) return
32
+ rightbarSkipLogged = true
33
+ try {
34
+ if (typeof console !== 'undefined' && typeof console.debug === 'function') {
35
+ console.debug('[dsh-file-activity] 右侧边栏默认宽度未应用: ' + reason, error ?? '')
36
+ }
37
+ } catch {
38
+ // logging must never break activation
39
+ }
40
+ }
41
+ /** Whether a raw value is a usable percent ratio (finite, inside the range). */
42
+ function rightbarRatioValid(value) {
43
+ const ratio = Number(value)
44
+ return Number.isFinite(ratio) && ratio >= RIGHTBAR_RATIO_MIN && ratio <= RIGHTBAR_RATIO_MAX
45
+ }
46
+ /** Persisted ratio, falling back to the default for missing/illegal values. */
47
+ function storedRightbarRatio() {
48
+ let raw
49
+ try {
50
+ raw = window.localStorage.getItem(RIGHTBAR_PREF_KEY)
51
+ } catch {
52
+ return RIGHTBAR_RATIO_DEFAULT
53
+ }
54
+ if (raw === null || !rightbarRatioValid(raw)) return RIGHTBAR_RATIO_DEFAULT
55
+ return Number(raw)
56
+ }
57
+ /** Persist a ratio; an illegal value is rejected instead of poisoning storage. */
58
+ function saveRightbarRatio(value) {
59
+ if (!rightbarRatioValid(value)) return false
60
+ try {
61
+ window.localStorage.setItem(RIGHTBAR_PREF_KEY, String(Number(value)))
62
+ } catch {
63
+ // storage unavailable (private mode): the control still shows the value
64
+ }
65
+ return true
66
+ }
67
+ /**
68
+ * The entry's store seat in HANDLE form (create() present), or null.
69
+ * A factory-shaped seat is deliberately not guessed at: calling it would build
70
+ * a second, unsubscribed instance instead of the one AppFrame renders.
71
+ * @param entry - one 'root' seat snapshot.
72
+ * @returns the handle-shaped seat, or null.
73
+ */
74
+ function rightbarStoreSeat(entry) {
75
+ const store = entry && entry.store
76
+ if (!store || typeof store !== 'object') return null
77
+ if (typeof store.create !== 'function') return null
78
+ return store
79
+ }
80
+ /**
81
+ * The instance's layoutInfo when it is writable, or null.
82
+ * @param instance - handle-created store instance.
83
+ * @returns layoutInfo, or null when the shape is foreign or a width exists.
84
+ */
85
+ function rightbarWritableInfo(instance) {
86
+ if (typeof instance.getSnapshot !== 'function') return null
87
+ const snapshot = instance.getSnapshot()
88
+ const info = snapshot && snapshot.layoutInfo
89
+ if (!info || typeof info !== 'object') return null
90
+ // NULL is the only green light: any number means this run already owns a
91
+ // width (the host's own 45% first-open default, or a user drag).
92
+ if (info.rightbar !== null) return null
93
+ return info
94
+ }
95
+ /**
96
+ * Turn a handle-created instance into a write target, or null.
97
+ * @param instance - handle-created store instance.
98
+ * @returns the write target, or null.
99
+ */
100
+ function rightbarTargetFromInstance(instance) {
101
+ if (!instance) return null
102
+ const info = rightbarWritableInfo(instance)
103
+ if (!info) return null
104
+ if (!instance.actions || typeof instance.actions.setRightbar !== 'function') return null
105
+ return { setRightbar: instance.actions.setRightbar, info }
106
+ }
107
+ /**
108
+ * Resolve the ui-layout instance behind the 'root' seat, or null when the host
109
+ * contract does not match. The snapshot shape is the test: writing the width
110
+ * goes through exactly that shape, so a shape we cannot read is a channel we
111
+ * must not use.
112
+ * @param ctx - client root context.
113
+ * @returns the write target, or null (caller stays silent).
114
+ */
115
+ function rightbarLayoutTarget(ctx) {
116
+ const slots = ctx && ctx.slots
117
+ if (!slots || typeof slots.entries !== 'function') {
118
+ logRightbarSkip('ctx.slots.entries 不可用')
119
+ return null
120
+ }
121
+ const entries = slots.entries('root') || []
122
+ for (const entry of entries) {
123
+ try {
124
+ const target = rightbarTargetFromInstance(rightbarStoreSeat(entry)?.create?.())
125
+ if (target) return target
126
+ } catch (error) {
127
+ logRightbarSkip('读取 root 席位 store 失败', error)
128
+ }
129
+ }
130
+ logRightbarSkip('未找到可写的 ui-layout root 席位')
131
+ return null
132
+ }
133
+ /**
134
+ * Apply the configured default width once, if the panel has no width yet.
135
+ * @param ctx - client root context.
136
+ */
137
+ function applyRightbarDefaultWidth(ctx) {
138
+ try {
139
+ const target = rightbarLayoutTarget(ctx)
140
+ if (!target) return
141
+ // The snapshot's own viewportWidth is the value the host's clamp uses, so
142
+ // prefer it; window.innerWidth only covers a snapshot that omits it.
143
+ const viewport = Number.isFinite(target.info.viewportWidth) ? target.info.viewportWidth : window.innerWidth
144
+ if (!Number.isFinite(viewport)) {
145
+ logRightbarSkip('viewportWidth 不可用')
146
+ return
147
+ }
148
+ const px = Math.round(viewport * (storedRightbarRatio() / 100))
149
+ if (!Number.isFinite(px) || px <= 0) {
150
+ logRightbarSkip('计算出的宽度非法')
151
+ return
152
+ }
153
+ target.setRightbar(px)
154
+ } catch (error) {
155
+ logRightbarSkip('应用宽度失败', error)
156
+ }
157
+ }
158
+ /**
159
+ * Install the preference: apply once at activation, then re-resolve the
160
+ * instance after a 'root' seat change (HMR / remount must not keep a stale
161
+ * instance). The returned disposer releases the subscription with the fiber.
162
+ * @param ctx - client root context.
163
+ * @returns the subscription disposer, when one was taken.
164
+ */
165
+ function installRightbarDefaultWidth(ctx) {
166
+ applyRightbarDefaultWidth(ctx)
167
+ const slots = ctx && ctx.slots
168
+ if (!slots || typeof slots.subscribe !== 'function') return undefined
169
+ try {
170
+ const dispose = slots.subscribe('root', () => applyRightbarDefaultWidth(ctx))
171
+ return typeof dispose === 'function' ? () => dispose() : undefined
172
+ } catch (error) {
173
+ logRightbarSkip('订阅 root 席位失败', error)
174
+ return undefined
175
+ }
176
+ }
@@ -43,9 +43,40 @@ function settingsRow(label, hint, checked, onChange) {
43
43
  }),
44
44
  )
45
45
  }
46
- /** Settings panel body: the auto-open switch plus its explanatory hint. */
46
+ /** One labeled percent row (issue #384: the right column's default width). */
47
+ function settingsPercentRow(label, hint, value, onCommit) {
48
+ return createElement(
49
+ 'div',
50
+ { className: 'dfa-set-row dfa-set-row-static' },
51
+ createElement(
52
+ 'span',
53
+ { className: 'dfa-set-text' },
54
+ createElement('span', { className: 'dfa-set-label' }, label),
55
+ createElement('span', { className: 'dfa-set-hint' }, hint),
56
+ ),
57
+ createElement(
58
+ 'span',
59
+ { className: 'dfa-set-field' },
60
+ createElement('input', {
61
+ type: 'number',
62
+ className: 'dfa-set-number',
63
+ min: RIGHTBAR_RATIO_MIN,
64
+ max: RIGHTBAR_RATIO_MAX,
65
+ step: 1,
66
+ value,
67
+ onChange: (event) => onCommit(event?.target?.value),
68
+ }),
69
+ createElement('span', { className: 'dfa-set-unit' }, '%'),
70
+ ),
71
+ )
72
+ }
73
+ /**
74
+ * Settings panel body: the auto-open switch, the right-sidebar default width,
75
+ * and their explanatory hints.
76
+ */
47
77
  function FileActivitySettings() {
48
78
  const [enabled, setEnabled] = useState(autoOpenEnabled)
79
+ const [ratio, setRatio] = useState(storedRightbarRatio)
49
80
  return createElement(
50
81
  'div',
51
82
  { className: 'dfa-set', 'data-dfa-settings': '1' },
@@ -54,6 +85,12 @@ function FileActivitySettings() {
54
85
  setAutoOpenEnabled(next)
55
86
  setEnabled(next)
56
87
  }),
88
+ settingsPercentRow(strings.rightbarWidthLabel(), strings.rightbarWidthHint(), ratio, (raw) => {
89
+ // An illegal / out-of-range edit is rejected outright: the control keeps
90
+ // showing the stored preference instead of a value we would not apply.
91
+ if (!saveRightbarRatio(raw)) return
92
+ setRatio(Number(raw))
93
+ }),
57
94
  )
58
95
  }
59
96
  /** Register the settings tab into the settings section's list seat. */
@@ -116,7 +116,7 @@ const STYLES = `
116
116
  border:1px solid var(--dsw-alias-state-error-primary); background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 8%, transparent); }
117
117
  .dfa-degraded-title { font:var(--dsw-font-xs-13); color:var(--dsw-alias-state-error-primary); }
118
118
  .dfa-degraded-hint { margin-top:2px; font:var(--dsw-font-xxs-12); opacity:.8; }
119
- /* 设置页(settings.plugins.tab):开关行。 */
119
+ /* 设置页(settings.plugins.tab):开关行 + 百分比输入行(issue #384)。 */
120
120
  .dfa-set { display:flex; flex-direction:column; gap:10px; padding:10px 2px; }
121
121
  .dfa-set-title { font:var(--dsw-font-sm-14); }
122
122
  .dfa-set-row { display:flex; align-items:center; justify-content:space-between; gap:12px; cursor:pointer; }
@@ -124,6 +124,12 @@ const STYLES = `
124
124
  .dfa-set-label { font:var(--dsw-font-xs-13); }
125
125
  .dfa-set-hint { font:var(--dsw-font-xxs-12); opacity:.75; }
126
126
  .dfa-set-switch { flex:none; width:16px; height:16px; accent-color:var(--dsw-alias-accent); cursor:pointer; }
127
+ .dfa-set-row-static { cursor:default; }
128
+ .dfa-set-field { flex:none; display:inline-flex; align-items:center; gap:4px; cursor:default; }
129
+ .dfa-set-number { box-sizing:border-box; width:60px; padding:4px 6px; text-align:right; color:inherit;
130
+ background:transparent; border:1px solid var(--dsw-alias-border-l1); border-radius:6px; font:var(--dsw-font-xs-13); }
131
+ .dfa-set-number:focus { outline:none; border-color:var(--dsw-alias-accent); }
132
+ .dfa-set-unit { font:var(--dsw-font-xs-13); opacity:.75; }
127
133
  /* 浮窗正文:Markdown / 代码 / 纯文本 / 图片四种渲染体。 */
128
134
  .dfa-fp-md { height:100%; overflow:auto; padding:2px 4px; }
129
135
  .dfa-fp-code { height:100%; overflow:auto; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-file-activity",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
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",