@luziyang2026/dsh-question-nav 0.7.2 → 0.7.4
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/README.md +12 -4
- package/README.zh.md +10 -3
- package/lib/client.js +128 -57
- package/lib/client.js.map +1 -1
- package/lib/index.js +30 -5
- package/lib/types/client/QuestionNavSettingsTab.d.ts +4 -3
- package/lib/types/client/QuestionNavStrip.d.ts +7 -2
- package/lib/types/client/locales.d.ts +8 -4
- package/lib/types/client/settings.d.ts +13 -7
- package/lib/types/core/focus.d.ts +3 -16
- package/lib/types/core/page-size.d.ts +16 -0
- package/lib/types/settings.d.ts +8 -4
- package/package.json +1 -1
- package/src/client/QuestionNavSettingsTab.tsx +24 -5
- package/src/client/QuestionNavStrip.tsx +82 -47
- package/src/client/index.ts +3 -1
- package/src/client/locales.ts +8 -4
- package/src/client/question-nav.module.css +61 -17
- package/src/client/settings.ts +36 -13
- package/src/core/focus.ts +3 -34
- package/src/core/page-size.ts +20 -0
- package/src/settings.ts +12 -4
package/src/client/settings.ts
CHANGED
|
@@ -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
|
|
4
|
-
* the user's
|
|
5
|
-
* registered by the host half
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
}
|
package/src/core/focus.ts
CHANGED
|
@@ -75,42 +75,11 @@ export function magnificationWindow(total: number, selected: number, radius: num
|
|
|
75
75
|
return out
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
/** Clearance (px) kept around the focused dot when scrolling it into view, so
|
|
79
|
-
* its two magnified neighbors on each side stay inside the band's clear area
|
|
80
|
-
* (2 dot rows ≈ 28px + the 22px fade zone). */
|
|
81
|
-
export const FOCUS_NEIGHBOR_CLEARANCE = 50
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Minimal "scroll into view" for the focused dot, in the spirit of
|
|
85
|
-
* scrollIntoView({ block: 'nearest' }): returns the scrollTop that brings the
|
|
86
|
-
* dot — plus `clearance` room for its magnified neighbors — inside the
|
|
87
|
-
* visible band with the smallest possible movement, or null when the dot is
|
|
88
|
-
* already fully visible. Unlike unconditional centering this never shifts the
|
|
89
|
-
* band while the user browses dot-by-dot: only a clipped dot is scrolled.
|
|
90
|
-
*/
|
|
91
|
-
export function minimalScrollIntoView(
|
|
92
|
-
scrollTop: number,
|
|
93
|
-
clientHeight: number,
|
|
94
|
-
scrollHeight: number,
|
|
95
|
-
dotTop: number,
|
|
96
|
-
dotHeight: number,
|
|
97
|
-
clearance: number = FOCUS_NEIGHBOR_CLEARANCE,
|
|
98
|
-
): number | null {
|
|
99
|
-
const margin = Math.min(clearance, clientHeight / 4)
|
|
100
|
-
const viewTop = scrollTop + margin
|
|
101
|
-
const viewBottom = scrollTop + clientHeight - margin
|
|
102
|
-
if (dotTop >= viewTop && dotTop + dotHeight <= viewBottom) return null
|
|
103
|
-
const next = dotTop < viewTop
|
|
104
|
-
? dotTop - margin
|
|
105
|
-
: dotTop + dotHeight + margin - clientHeight
|
|
106
|
-
return Math.max(0, Math.min(next, Math.max(0, scrollHeight - clientHeight)))
|
|
107
|
-
}
|
|
108
|
-
|
|
109
78
|
/**
|
|
110
79
|
* Paging model for the dot band. Overflowing dots are not auto-scrolled by
|
|
111
|
-
* hovering the edges: instead the user pages them
|
|
112
|
-
* (▲ above the dot queue, ▼ below it), each click
|
|
113
|
-
* hidden dots. Pure arithmetic — no React, no DOM.
|
|
80
|
+
* hovering the edges or hovering a clipped dot: instead the user pages them
|
|
81
|
+
* with two triangle buttons (▲ above the dot queue, ▼ below it), each click
|
|
82
|
+
* revealing `DOT_PAGE_ROWS` hidden dots. Pure arithmetic — no React, no DOM.
|
|
114
83
|
*/
|
|
115
84
|
|
|
116
85
|
/** How many hidden dots one click of a paging triangle reveals. */
|
|
@@ -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.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|