@luziyang2026/dsh-question-nav 0.7.3 → 0.7.5

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,13 +1,14 @@
1
1
  /**
2
2
  * Browser mirror of the `question-nav` settings namespace: reads the rail
3
- * anchor edge from the settings scope (`ctx.settingsScope.bind`) and routes
4
- * the user's choice back through `scope.set`. The namespace itself is
5
- * registered by the host half (src/settings.ts).
3
+ * anchor edge and the ▲/▼ page size from the settings scope
4
+ * (`ctx.settingsScope.bind`) and routes the user's choices back through
5
+ * `scope.set`. The namespace itself is registered by the host half
6
+ * (src/settings.ts).
6
7
  *
7
8
  * The settings surface is optional and may apply after this plugin, so the
8
- * controller starts unbound and degrades to the default alignment until
9
- * {@link attach} binds the scope (called from a fiber that injects
10
- * `settingsScope`). The plugin keeps working everywhere it already did.
9
+ * controller starts unbound and degrades to the defaults until {@link attach}
10
+ * binds the scope (called from a fiber that injects `settingsScope`). The
11
+ * plugin keeps working everywhere it already did.
11
12
  *
12
13
  * @module dsh-question-nav/client/settings
13
14
  */
@@ -17,12 +18,21 @@ import {
17
18
  ALIGN_FIELD, ALIGN_OPTIONS, DEFAULT_ALIGN, QUESTION_NAV_SETTINGS_NS,
18
19
  type AlignPreference,
19
20
  } from '../core/align.ts'
21
+ import {
22
+ DEFAULT_PAGE_SIZE, PAGE_SIZE_FIELD, PAGE_SIZE_OPTIONS,
23
+ type PageSize,
24
+ } from '../core/page-size.ts'
20
25
 
21
- /** Narrow a raw section to the anchor field. */
26
+ /** Narrow a raw section value to the anchor field. */
22
27
  function isAlignPreference(value: unknown): value is AlignPreference {
23
28
  return ALIGN_OPTIONS.some((option) => option === value)
24
29
  }
25
30
 
31
+ /** Narrow a raw section value to the page-size field. */
32
+ function isPageSize(value: unknown): value is PageSize {
33
+ return (PAGE_SIZE_OPTIONS as readonly unknown[]).includes(value)
34
+ }
35
+
26
36
  /** The minimal face of the settings scope service this controller needs.
27
37
  * Kept structural (bind only) so the controller stays decoupled from the
28
38
  * full service and is unit-testable with a stub binder. */
@@ -30,15 +40,18 @@ export interface SettingsScopeBinderLike {
30
40
  bind<T>(spec: SettingsScopeSpec<T>): SettingsScope<T>
31
41
  }
32
42
 
33
- /** Wire section this plugin owns (the Host schema's `align` field). */
43
+ /** Wire section this plugin owns (the Host schema's fields). */
34
44
  interface QuestionNavSection {
35
45
  align?: unknown
46
+ pageSize?: unknown
36
47
  }
37
48
 
38
- /** Snapshot consumed by the strip and the settings row. */
49
+ /** Snapshot consumed by the strip and the settings rows. */
39
50
  export interface QuestionNavSettingsState {
40
51
  /** Last accepted anchor edge (default while the scope is absent/loading). */
41
52
  align: AlignPreference
53
+ /** Last accepted page size (default while the scope is absent/loading). */
54
+ pageSize: PageSize
42
55
  /** Whether the user layer overrides the composition default. */
43
56
  overridden: boolean
44
57
  }
@@ -48,7 +61,7 @@ export class QuestionNavSettingsController {
48
61
  private scope: SettingsScope<QuestionNavSection> | undefined
49
62
  private readonly listeners = new Set<() => void>()
50
63
  private unsubscribe: () => void = () => {}
51
- private state: QuestionNavSettingsState = { align: DEFAULT_ALIGN, overridden: false }
64
+ private state: QuestionNavSettingsState = { align: DEFAULT_ALIGN, pageSize: DEFAULT_PAGE_SIZE, overridden: false }
52
65
 
53
66
  /**
54
67
  * Bind the namespace scope once the settings surface is present. Called
@@ -64,19 +77,23 @@ export class QuestionNavSettingsController {
64
77
  this.unsubscribe = this.scope.subscribe(() => {
65
78
  if (this.scope === undefined) return
66
79
  const next = this.derive(this.scope.getSnapshot())
67
- if (next.align === this.state.align && next.overridden === this.state.overridden) return
80
+ if (next.align === this.state.align && next.pageSize === this.state.pageSize
81
+ && next.overridden === this.state.overridden) return
68
82
  this.state = next
69
83
  for (const listener of this.listeners) listener()
70
84
  })
71
85
  }
72
86
 
73
87
  private derive(snapshot: SettingsScopeSnapshot<QuestionNavSection>): QuestionNavSettingsState {
74
- const user = snapshot.user as { align?: unknown } | undefined
88
+ const user = snapshot.user as QuestionNavSection | undefined
75
89
  return {
76
90
  align: snapshot.status === 'ready' && isAlignPreference(snapshot.value?.align)
77
91
  ? snapshot.value.align
78
92
  : DEFAULT_ALIGN,
79
- overridden: user !== undefined && user.align !== undefined,
93
+ pageSize: snapshot.status === 'ready' && isPageSize(snapshot.value?.pageSize)
94
+ ? snapshot.value.pageSize
95
+ : DEFAULT_PAGE_SIZE,
96
+ overridden: user !== undefined && (user.align !== undefined || user.pageSize !== undefined),
80
97
  }
81
98
  }
82
99
 
@@ -102,4 +119,10 @@ export class QuestionNavSettingsController {
102
119
  if (this.scope === undefined) return
103
120
  void this.scope.set(ALIGN_FIELD, align)
104
121
  }
122
+
123
+ /** Route the user's page-size choice to the Host document. */
124
+ setPageSize(pageSize: PageSize): void {
125
+ if (this.scope === undefined) return
126
+ void this.scope.set(PAGE_SIZE_FIELD, pageSize)
127
+ }
105
128
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Page-size constants for the ▲/▼ paging buttons, shared by the host schema
3
+ * and the browser settings scope. Pure data: no DSH imports, so the client
4
+ * bundle may inline this module (a Host import here would leak into the
5
+ * browser half).
6
+ *
7
+ * @module dsh-question-nav/page-size
8
+ */
9
+
10
+ /** Selectable page sizes: how many hidden dots one ▲/▼ click reveals. */
11
+ export const PAGE_SIZE_OPTIONS = [3, 5, 8, 10] as const
12
+
13
+ /** Page-size preference (dots revealed per paging-button click). */
14
+ export type PageSize = typeof PAGE_SIZE_OPTIONS[number]
15
+
16
+ /** Default page size when the user-settings document has no override. */
17
+ export const DEFAULT_PAGE_SIZE: PageSize = 5
18
+
19
+ /** Field carrying the selected page size. */
20
+ export const PAGE_SIZE_FIELD = 'pageSize'
package/src/settings.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * Host-side durable settings for the question-nav plugin, registered into the
3
- * DSH user-settings document. Currently one field: which edge of the
4
- * conversation column the rail anchors to (`align`). The browser half reads
5
- * the same namespace through the settings scope (`ctx.settingsScope.bind`)
6
- * and routes the user's choice back through `scope.set`.
3
+ * DSH user-settings document. Two fields: which edge of the conversation
4
+ * column the rail anchors to (`align`) and how many hidden dots one ▲/▼
5
+ * paging-button click reveals (`pageSize`). The browser half reads the same
6
+ * namespace through the settings scope (`ctx.settingsScope.bind`) and routes
7
+ * the user's choice back through `scope.set`.
7
8
  *
8
9
  * @module dsh-question-nav/settings
9
10
  */
@@ -14,16 +15,23 @@ import {
14
15
  ALIGN_FIELD, ALIGN_OPTIONS, DEFAULT_ALIGN, QUESTION_NAV_SETTINGS_NS,
15
16
  type AlignPreference,
16
17
  } from './core/align.ts'
18
+ import {
19
+ DEFAULT_PAGE_SIZE, PAGE_SIZE_FIELD, PAGE_SIZE_OPTIONS,
20
+ type PageSize,
21
+ } from './core/page-size.ts'
17
22
 
18
23
  /** Durable settings section shared by the Host schema and the browser scope. */
19
24
  export interface QuestionNavSettings {
20
25
  /** Anchor edge of the rail. */
21
26
  align: AlignPreference
27
+ /** Dots revealed per ▲/▼ paging-button click. */
28
+ pageSize: PageSize
22
29
  }
23
30
 
24
31
  /** Durable settings schema; also the wire envelope the browser scope validates against. */
25
32
  export const QuestionNavSettingsSchema: z<QuestionNavSettings> = z.object({
26
33
  [ALIGN_FIELD]: z.union([...ALIGN_OPTIONS]).default(DEFAULT_ALIGN),
34
+ [PAGE_SIZE_FIELD]: z.union([...PAGE_SIZE_OPTIONS]).default(DEFAULT_PAGE_SIZE),
27
35
  })
28
36
 
29
37
  /** The settings namespace this plugin owns, branded for the Host registry.