@anionex/dsh-eye-care 0.1.0

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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +6 -0
  3. package/README.md +86 -0
  4. package/README.zh.md +86 -0
  5. package/assets/eye-care-dark.png +0 -0
  6. package/assets/eye-care-light.png +0 -0
  7. package/cordis.patch.yml +3 -0
  8. package/lib/client.js +1106 -0
  9. package/lib/client.js.map +1 -0
  10. package/lib/index.js +14 -0
  11. package/lib/index.js.map +1 -0
  12. package/lib/rpc.js +123 -0
  13. package/lib/rpc.js.map +1 -0
  14. package/lib/settings.js +12 -0
  15. package/lib/settings.js.map +1 -0
  16. package/lib/shared.js +70 -0
  17. package/lib/shared.js.map +1 -0
  18. package/lib/types/client/EyeCareRow.d.ts +20 -0
  19. package/lib/types/client/EyeCareRow.d.ts.map +1 -0
  20. package/lib/types/client/controller.d.ts +96 -0
  21. package/lib/types/client/controller.d.ts.map +1 -0
  22. package/lib/types/client/index.d.ts +23 -0
  23. package/lib/types/client/index.d.ts.map +1 -0
  24. package/lib/types/client/locales.d.ts +54 -0
  25. package/lib/types/client/locales.d.ts.map +1 -0
  26. package/lib/types/client/themes.d.ts +14 -0
  27. package/lib/types/client/themes.d.ts.map +1 -0
  28. package/lib/types/index.d.ts +10 -0
  29. package/lib/types/index.d.ts.map +1 -0
  30. package/lib/types/rpc.d.ts +17 -0
  31. package/lib/types/rpc.d.ts.map +1 -0
  32. package/lib/types/settings.d.ts +8 -0
  33. package/lib/types/settings.d.ts.map +1 -0
  34. package/lib/types/shared.d.ts +59 -0
  35. package/lib/types/shared.d.ts.map +1 -0
  36. package/package.json +99 -0
  37. package/src/client/EyeCareRow.module.css +137 -0
  38. package/src/client/EyeCareRow.tsx +122 -0
  39. package/src/client/controller.ts +435 -0
  40. package/src/client/css-modules.d.ts +5 -0
  41. package/src/client/index.tsx +72 -0
  42. package/src/client/locales.ts +56 -0
  43. package/src/client/themes.ts +254 -0
  44. package/src/index.ts +30 -0
  45. package/src/rpc.ts +138 -0
  46. package/src/settings.ts +22 -0
  47. package/src/shared.ts +112 -0
@@ -0,0 +1,254 @@
1
+ /** Fixed eye-care themes built from semantic DSH Web tokens. */
2
+
3
+ import type { ThemeDefinition, ThemeTokens } from '@deepseek-ai/dsh-client-ui-theme/client'
4
+ import type { EyeCareIntensity } from '../shared.ts'
5
+
6
+ /** Theme id prefix reserved by this package. */
7
+ export const EYE_CARE_THEME_PREFIX = 'eye-care-'
8
+
9
+ type Scheme = 'light' | 'dark'
10
+
11
+ interface Palette {
12
+ bgBase: string
13
+ bgLayer1: string
14
+ bgLayer2: string
15
+ bgLayer3: string
16
+ bgModule: string
17
+ bgOverlay: string
18
+ textPrimary: string
19
+ textSecondary: string
20
+ textTertiary: string
21
+ textCaption: string
22
+ border1: string
23
+ border2: string
24
+ border3: string
25
+ hover: string
26
+ hoverSolid: string
27
+ active: string
28
+ primaryFill: string
29
+ primaryHover: string
30
+ primaryDimmed: string
31
+ bubble: string
32
+ bubbleHighlight: string
33
+ input: string
34
+ selector: string
35
+ sidebar: string
36
+ sidebarHover: string
37
+ sidebarActive: string
38
+ sidebarAccent: string
39
+ code: string
40
+ codeBanner: string
41
+ inlineCode: string
42
+ scrollbar: string
43
+ scrollbarHover: string
44
+ shiki: {
45
+ constant: string
46
+ string: string
47
+ comment: string
48
+ keyword: string
49
+ parameter: string
50
+ function: string
51
+ stringExpression: string
52
+ punctuation: string
53
+ link: string
54
+ }
55
+ }
56
+
57
+ const LIGHT: Record<EyeCareIntensity, Palette> = {
58
+ soft: {
59
+ bgBase: '#fbf8ef', bgLayer1: '#fffdf6', bgLayer2: '#f8f3e8', bgLayer3: '#f2ecde',
60
+ bgModule: '#f7f2e7', bgOverlay: '#ece4d5', textPrimary: '#29271f', textSecondary: '#575247',
61
+ textTertiary: '#716a5d', textCaption: '#958b7a', border1: 'rgba(85, 72, 46, 0.08)',
62
+ border2: 'rgba(85, 72, 46, 0.15)', border3: 'rgba(85, 72, 46, 0.21)',
63
+ hover: 'rgba(126, 101, 53, 0.08)', hoverSolid: '#f1eadc', active: 'rgba(126, 101, 53, 0.14)',
64
+ primaryFill: '#44583d', primaryHover: '#536a49', primaryDimmed: '#dfe6d9',
65
+ bubble: '#eef3df', bubbleHighlight: '#e0e9cb', input: '#fffdf6', selector: '#f2ecde',
66
+ sidebar: '#f4efe3', sidebarHover: '#ebe4d5', sidebarActive: '#e2dac9', sidebarAccent: '#d6e0c1',
67
+ code: '#f2eee4', codeBanner: '#ebe5d8', inlineCode: '#eee8db', scrollbar: '#d1c6b4',
68
+ scrollbarHover: '#baad98',
69
+ shiki: {
70
+ constant: '#176f88', string: '#34783c', comment: '#817768', keyword: '#a03e58',
71
+ parameter: '#a15b1f', function: '#6950a6', stringExpression: '#2f7040', punctuation: '#5a554d', link: '#246d9a',
72
+ },
73
+ },
74
+ balanced: {
75
+ bgBase: '#faf5e8', bgLayer1: '#fffaf0', bgLayer2: '#f5eddd', bgLayer3: '#eee3d0',
76
+ bgModule: '#f3ead9', bgOverlay: '#e7dbc7', textPrimary: '#2b281f', textSecondary: '#5b5345',
77
+ textTertiary: '#766d5b', textCaption: '#9b8e77', border1: 'rgba(93, 72, 36, 0.09)',
78
+ border2: 'rgba(93, 72, 36, 0.17)', border3: 'rgba(93, 72, 36, 0.24)',
79
+ hover: 'rgba(139, 100, 37, 0.09)', hoverSolid: '#eee3d1', active: 'rgba(139, 100, 37, 0.16)',
80
+ primaryFill: '#425b3c', primaryHover: '#52704a', primaryDimmed: '#dce7d3',
81
+ bubble: '#eaf1d5', bubbleHighlight: '#dce7bc', input: '#fffaf0', selector: '#eee3d0',
82
+ sidebar: '#f1e8d7', sidebarHover: '#e7dcc8', sidebarActive: '#ddd0b9', sidebarAccent: '#d2dfb3',
83
+ code: '#efe9dc', codeBanner: '#e6dece', inlineCode: '#ebe2d1', scrollbar: '#ccbda6',
84
+ scrollbarHover: '#b3a188',
85
+ shiki: {
86
+ constant: '#146d87', string: '#39753b', comment: '#837664', keyword: '#a13e57',
87
+ parameter: '#a1581c', function: '#684ba3', stringExpression: '#34703d', punctuation: '#5d5549', link: '#206c98',
88
+ },
89
+ },
90
+ warm: {
91
+ bgBase: '#f8efda', bgLayer1: '#fff7e7', bgLayer2: '#f2e6d0', bgLayer3: '#eadcc4',
92
+ bgModule: '#f0e3cc', bgOverlay: '#e1d0b5', textPrimary: '#30291f', textSecondary: '#615343',
93
+ textTertiary: '#7c6a56', textCaption: '#a08b70', border1: 'rgba(105, 73, 30, 0.10)',
94
+ border2: 'rgba(105, 73, 30, 0.19)', border3: 'rgba(105, 73, 30, 0.27)',
95
+ hover: 'rgba(151, 97, 27, 0.10)', hoverSolid: '#eadbc2', active: 'rgba(151, 97, 27, 0.18)',
96
+ primaryFill: '#3f5c39', primaryHover: '#506f46', primaryDimmed: '#d8e5ce',
97
+ bubble: '#e6efc9', bubbleHighlight: '#d6e3ac', input: '#fff7e7', selector: '#eadcc4',
98
+ sidebar: '#eee1ca', sidebarHover: '#e3d3b9', sidebarActive: '#d7c4a7', sidebarAccent: '#cedeaa',
99
+ code: '#ece2d1', codeBanner: '#e2d5c0', inlineCode: '#e7dac4', scrollbar: '#c7b398',
100
+ scrollbarHover: '#ad9779',
101
+ shiki: {
102
+ constant: '#116b83', string: '#3e7439', comment: '#85735e', keyword: '#a33b54',
103
+ parameter: '#a25418', function: '#67459e', stringExpression: '#386d39', punctuation: '#625344', link: '#1c6b94',
104
+ },
105
+ },
106
+ }
107
+
108
+ const DARK: Record<EyeCareIntensity, Palette> = {
109
+ soft: {
110
+ bgBase: '#20211c', bgLayer1: '#25261f', bgLayer2: '#2a2b23', bgLayer3: '#313128',
111
+ bgModule: '#2d2e26', bgOverlay: '#3a392e', textPrimary: '#ece9dc', textSecondary: '#c7c1b0',
112
+ textTertiary: '#aaa28f', textCaption: '#7f786a', border1: 'rgba(245, 233, 201, 0.07)',
113
+ border2: 'rgba(245, 233, 201, 0.13)', border3: 'rgba(245, 233, 201, 0.19)',
114
+ hover: 'rgba(244, 227, 182, 0.08)', hoverSolid: '#36362c', active: 'rgba(244, 227, 182, 0.14)',
115
+ primaryFill: '#dfe9cf', primaryHover: '#f1f5e7', primaryDimmed: '#4d5143',
116
+ bubble: '#30372a', bubbleHighlight: '#3d4933', input: '#2b2d25', selector: '#37382e',
117
+ sidebar: '#252820', sidebarHover: '#30342a', sidebarActive: '#3a3e32', sidebarAccent: '#3f4b35',
118
+ code: '#1c1e1b', codeBanner: '#252821', inlineCode: '#292c24', scrollbar: '#57584d',
119
+ scrollbarHover: '#707064',
120
+ shiki: {
121
+ constant: '#68b7d2', string: '#91c88b', comment: '#9d988d', keyword: '#e39aaf',
122
+ parameter: '#e6ad72', function: '#b9a5df', stringExpression: '#9bcf91', punctuation: '#c7c2b7', link: '#77b8d9',
123
+ },
124
+ },
125
+ balanced: {
126
+ bgBase: '#211f19', bgLayer1: '#27231c', bgLayer2: '#2d2921', bgLayer3: '#353027',
127
+ bgModule: '#302b22', bgOverlay: '#40382b', textPrimary: '#efe7d6', textSecondary: '#cbc0aa',
128
+ textTertiary: '#aea087', textCaption: '#837662', border1: 'rgba(250, 230, 190, 0.08)',
129
+ border2: 'rgba(250, 230, 190, 0.15)', border3: 'rgba(250, 230, 190, 0.21)',
130
+ hover: 'rgba(247, 216, 157, 0.09)', hoverSolid: '#3b352b', active: 'rgba(247, 216, 157, 0.16)',
131
+ primaryFill: '#e0ebcf', primaryHover: '#f2f7e6', primaryDimmed: '#505243',
132
+ bubble: '#32392a', bubbleHighlight: '#404c32', input: '#302b23', selector: '#3b352a',
133
+ sidebar: '#282820', sidebarHover: '#33342a', sidebarActive: '#3d3e32', sidebarAccent: '#435036',
134
+ code: '#1d1d19', codeBanner: '#28251f', inlineCode: '#2d2922', scrollbar: '#5b584b',
135
+ scrollbarHover: '#757062',
136
+ shiki: {
137
+ constant: '#69b8d0', string: '#94c985', comment: '#a19a8d', keyword: '#e49aad',
138
+ parameter: '#e7aa6c', function: '#bca2df', stringExpression: '#9dcc89', punctuation: '#cbc2b2', link: '#79b9d5',
139
+ },
140
+ },
141
+ warm: {
142
+ bgBase: '#231e17', bgLayer1: '#2a231b', bgLayer2: '#31291f', bgLayer3: '#3a3025',
143
+ bgModule: '#342b21', bgOverlay: '#46392b', textPrimary: '#f1e4cf', textSecondary: '#cdbb9f',
144
+ textTertiary: '#b09a7e', textCaption: '#857058', border1: 'rgba(255, 225, 177, 0.09)',
145
+ border2: 'rgba(255, 225, 177, 0.17)', border3: 'rgba(255, 225, 177, 0.24)',
146
+ hover: 'rgba(252, 207, 136, 0.10)', hoverSolid: '#403529', active: 'rgba(252, 207, 136, 0.18)',
147
+ primaryFill: '#e1ebcc', primaryHover: '#f3f7e4', primaryDimmed: '#535143',
148
+ bubble: '#353a28', bubbleHighlight: '#444d30', input: '#342a20', selector: '#403428',
149
+ sidebar: '#2b271e', sidebarHover: '#363127', sidebarActive: '#413a2e', sidebarAccent: '#485235',
150
+ code: '#1f1b17', codeBanner: '#2b241d', inlineCode: '#31281f', scrollbar: '#605548',
151
+ scrollbarHover: '#7a6b5b',
152
+ shiki: {
153
+ constant: '#6ab9ce', string: '#98c87e', comment: '#a59a89', keyword: '#e696a8',
154
+ parameter: '#e8a765', function: '#bea0dc', stringExpression: '#a0cb82', punctuation: '#cec0ad', link: '#7bb9d1',
155
+ },
156
+ },
157
+ }
158
+
159
+ /** Build the stable id for one concrete eye-care theme. */
160
+ export function eyeCareThemeId(scheme: Scheme, intensity: EyeCareIntensity): string {
161
+ return `${EYE_CARE_THEME_PREFIX}${scheme}-${intensity}`
162
+ }
163
+
164
+ function tokensOf(palette: Palette): ThemeTokens {
165
+ return Object.freeze({
166
+ '--dsw-alias-bg-base': palette.bgBase,
167
+ '--dsw-alias-bg-layer-1': palette.bgLayer1,
168
+ '--dsw-alias-bg-layer-2': palette.bgLayer2,
169
+ '--dsw-alias-bg-layer-3': palette.bgLayer3,
170
+ '--dsw-alias-bg-module-platform': palette.bgModule,
171
+ '--dsw-alias-bg-multi-select': palette.bgModule,
172
+ '--dsw-alias-bg-overlay': palette.bgOverlay,
173
+ '--dsw-alias-bg-skeleton': palette.hover,
174
+ '--dsw-alias-border-l1': palette.border1,
175
+ '--dsw-alias-border-l2-darkmode-thin': palette.border2,
176
+ '--dsw-alias-border-l2': palette.border2,
177
+ '--dsw-alias-border-l3': palette.border3,
178
+ '--dsw-alias-border-l4': palette.border3,
179
+ '--dsw-alias-brand-primary': palette.textPrimary,
180
+ '--dsw-alias-brand-text': palette.textPrimary,
181
+ '--dsw-alias-button-contrast-fill': palette.textSecondary,
182
+ '--dsw-alias-button-elevated-fill': palette.bgLayer3,
183
+ '--dsw-alias-button-floating-fill': palette.bgLayer2,
184
+ '--dsw-alias-button-floating-hover': palette.hoverSolid,
185
+ '--dsw-alias-button-ghost-active-border': palette.textCaption,
186
+ '--dsw-alias-button-ghost-active-fill': palette.active,
187
+ '--dsw-alias-button-ghost-active-hover': palette.hoverSolid,
188
+ '--dsw-alias-button-primary-fill': palette.primaryFill,
189
+ '--dsw-alias-button-primary-hover': palette.primaryHover,
190
+ '--dsw-alias-button-primary-dimmed': palette.primaryDimmed,
191
+ '--dsw-alias-interactive-bg-active': palette.active,
192
+ '--dsw-alias-interactive-bg-hover-accent': palette.active,
193
+ '--dsw-alias-interactive-bg-hover': palette.hover,
194
+ '--dsw-alias-interactive-bg-hover-solid': palette.hoverSolid,
195
+ '--dsw-alias-label-caption': palette.textCaption,
196
+ '--dsw-alias-label-dimmed': palette.border3,
197
+ '--dsw-alias-label-primary-bluish': palette.textPrimary,
198
+ '--dsw-alias-label-primary-dimmed': palette.textPrimary,
199
+ '--dsw-alias-label-primary': palette.textPrimary,
200
+ '--dsw-alias-label-secondary': palette.textSecondary,
201
+ '--dsw-alias-label-tertiary': palette.textTertiary,
202
+ '--dsw-alias-markdown-citation': palette.hoverSolid,
203
+ '--dsw-alias-markdown-code-block-banner': palette.codeBanner,
204
+ '--dsw-alias-markdown-code-block': palette.code,
205
+ '--dsw-alias-markdown-code-segment-selected': palette.bgLayer3,
206
+ '--dsw-alias-markdown-code-segment-unselected': palette.code,
207
+ '--dsw-alias-markdown-inline-code': palette.inlineCode,
208
+ '--dsw-alias-markdown-placeholder': palette.bgModule,
209
+ '--dsw-alias-markdown-tag': palette.hoverSolid,
210
+ '--dsw-alias-scrollbar-bg-l1': palette.scrollbar,
211
+ '--dsw-alias-scrollbar-bg-l2': palette.scrollbar,
212
+ '--dsw-alias-scrollbar-hover-l1': palette.scrollbarHover,
213
+ '--dsw-alias-scrollbar-hover-l2': palette.scrollbarHover,
214
+ '--dsw-specific-bubble-highlight': palette.bubbleHighlight,
215
+ '--dsw-specific-bubble': palette.bubble,
216
+ '--dsw-specific-input-major': palette.input,
217
+ '--dsw-specific-login-input': palette.bgLayer2,
218
+ '--dsw-specific-menu': palette.bgLayer3,
219
+ '--dsw-specific-selector': palette.selector,
220
+ '--dsw-specific-sidebar-fill': palette.sidebar,
221
+ '--dsw-specific-sidebar-nav-item-active-accent': palette.sidebarAccent,
222
+ '--dsw-specific-sidebar-nav-item-active': palette.sidebarActive,
223
+ '--dsw-specific-sidebar-nav-item-hover': palette.sidebarHover,
224
+ '--dsw-specific-tip': palette.bgModule,
225
+ '--shiki-foreground': palette.textPrimary,
226
+ '--shiki-background': palette.code,
227
+ '--shiki-token-constant': palette.shiki.constant,
228
+ '--shiki-token-string': palette.shiki.string,
229
+ '--shiki-token-comment': palette.shiki.comment,
230
+ '--shiki-token-keyword': palette.shiki.keyword,
231
+ '--shiki-token-parameter': palette.shiki.parameter,
232
+ '--shiki-token-function': palette.shiki.function,
233
+ '--shiki-token-string-expression': palette.shiki.stringExpression,
234
+ '--shiki-token-punctuation': palette.shiki.punctuation,
235
+ '--shiki-token-link': palette.shiki.link,
236
+ })
237
+ }
238
+
239
+ /** Six concrete themes registered for the two schemes and three warmth levels. */
240
+ export const EYE_CARE_THEMES: readonly ThemeDefinition[] = Object.freeze(
241
+ (['light', 'dark'] as const).flatMap(scheme =>
242
+ (['soft', 'balanced', 'warm'] as const).map(intensity => Object.freeze({
243
+ id: eyeCareThemeId(scheme, intensity),
244
+ colorScheme: scheme,
245
+ tokens: tokensOf((scheme === 'light' ? LIGHT : DARK)[intensity]),
246
+ }))),
247
+ )
248
+
249
+ const EYE_CARE_THEME_IDS = new Set(EYE_CARE_THEMES.map(theme => theme.id))
250
+
251
+ /** Whether a theme id is one of this package's registered concrete themes. */
252
+ export function isEyeCareThemeId(id: string): boolean {
253
+ return EYE_CARE_THEME_IDS.has(id)
254
+ }
package/src/index.ts ADDED
@@ -0,0 +1,30 @@
1
+ /** DSH eye-care Host plugin: settings ownership and loopback RPC transport. */
2
+
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import type {} from '@deepseek-ai/dsh-settings'
5
+ import { installEyeCareRpc } from './rpc.ts'
6
+ import { EYE_CARE_SETTINGS_NS, EyeCareSettingsSchema } from './settings.ts'
7
+
8
+ export {
9
+ DEFAULT_EYE_CARE_SETTINGS,
10
+ EYE_CARE_INTENSITIES,
11
+ EYE_CARE_MODES,
12
+ EYE_CARE_SETTINGS_NAMESPACE,
13
+ EYE_CARE_RPC_CHANNEL,
14
+ EYE_CARE_RPC_READ,
15
+ EYE_CARE_RPC_SAVE,
16
+ type EyeCareIntensity,
17
+ type EyeCareMode,
18
+ type EyeCareSettings,
19
+ } from './shared.ts'
20
+ export { EYE_CARE_SETTINGS_NS, EyeCareSettingsSchema } from './settings.ts'
21
+ export { EyeCareRpcBackend } from './rpc.ts'
22
+
23
+ /** Required Host capability. */
24
+ export const inject = ['settings']
25
+
26
+ /** Register the durable namespace and the optional loopback browser transport. */
27
+ export function apply(ctx: Context): void {
28
+ ctx.settings.register(EYE_CARE_SETTINGS_NS, EyeCareSettingsSchema)
29
+ installEyeCareRpc(ctx)
30
+ }
package/src/rpc.ts ADDED
@@ -0,0 +1,138 @@
1
+ /** Loopback-only Connection RPC for durable eye-care preferences. */
2
+
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection'
5
+ import { SettingsConflictError, type SettingsDescriptor } from '@deepseek-ai/dsh-settings'
6
+ import type {} from '@deepseek-ai/dsh-client-connection'
7
+ import {
8
+ decodeEyeCareSettings,
9
+ EYE_CARE_RPC_CHANNEL,
10
+ EYE_CARE_RPC_READ,
11
+ EYE_CARE_RPC_SAVE,
12
+ type EyeCareSettings,
13
+ type EyeCareSettingsSnapshot,
14
+ type EyeCareSettingsWriteRequest,
15
+ } from './shared.ts'
16
+ import { EYE_CARE_SETTINGS_NS } from './settings.ts'
17
+
18
+ function descriptorOf(ctx: Context): SettingsDescriptor {
19
+ const descriptor = ctx.settings.describe().find(row => row.ns === EYE_CARE_SETTINGS_NS)
20
+ if (descriptor === undefined) throw new Error('eye-care settings namespace is not registered')
21
+ return descriptor
22
+ }
23
+
24
+ function baseThemePreferenceOf(ctx: Context): 'light' | 'dark' | 'system' | undefined {
25
+ const descriptor = ctx.settings.describe().find(row => String(row.ns) === 'ui-theme')
26
+ if (typeof descriptor?.value !== 'object' || descriptor.value === null || Array.isArray(descriptor.value)) return undefined
27
+ const preference = (descriptor.value as Record<string, unknown>)['preference']
28
+ return preference === 'light' || preference === 'dark' || preference === 'system' ? preference : undefined
29
+ }
30
+
31
+ function parseWriteRequest(value: unknown): EyeCareSettingsWriteRequest {
32
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
33
+ throw new TypeError('request must be an object')
34
+ }
35
+ const record = value as Record<string, unknown>
36
+ const settings = decodeEyeCareSettings(record['value'])
37
+ const revision = record['expectedRevision']
38
+ if (!Number.isSafeInteger(revision) || (revision as number) < 0 || settings === undefined) {
39
+ throw new TypeError('save requires a non-negative expectedRevision and a valid eye-care value')
40
+ }
41
+ return { expectedRevision: revision as number, value: settings }
42
+ }
43
+
44
+ function publicMessage(error: unknown): string {
45
+ return (error instanceof Error ? error.message : String(error)).slice(0, 1000)
46
+ }
47
+
48
+ /** Host backend used by the loopback-only Connection RPC registration. */
49
+ export class EyeCareRpcBackend {
50
+ /** Create a backend over the active Settings service. */
51
+ constructor(private readonly ctx: Context) {}
52
+
53
+ /** Read the current browser-safe settings snapshot. */
54
+ snapshot(): EyeCareSettingsSnapshot {
55
+ const descriptor = descriptorOf(this.ctx)
56
+ const baseThemePreference = baseThemePreferenceOf(this.ctx)
57
+ return {
58
+ schemaVersion: 1,
59
+ writable: this.ctx.settings.writable,
60
+ settings: {
61
+ value: descriptor.value as EyeCareSettings,
62
+ revision: descriptor.revision,
63
+ applies: 'live',
64
+ },
65
+ ...(baseThemePreference === undefined ? {} : { baseThemePreference }),
66
+ }
67
+ }
68
+
69
+ /** Handle one Connection RPC endpoint. */
70
+ readonly handle: ConnectionRpcHandler = async (endpoint, payload) => {
71
+ if (endpoint === EYE_CARE_RPC_READ) {
72
+ return { ok: true, value: this.snapshot() }
73
+ }
74
+ if (endpoint !== EYE_CARE_RPC_SAVE) {
75
+ return {
76
+ ok: false,
77
+ error: {
78
+ code: 'bad-request',
79
+ message: `unknown eye-care endpoint: ${endpoint}`,
80
+ details: { issues: [] },
81
+ },
82
+ }
83
+ }
84
+ let request: EyeCareSettingsWriteRequest
85
+ try {
86
+ request = parseWriteRequest(payload)
87
+ } catch (error) {
88
+ return {
89
+ ok: false,
90
+ error: {
91
+ code: 'bad-request',
92
+ message: publicMessage(error),
93
+ details: { issues: [] },
94
+ },
95
+ }
96
+ }
97
+ try {
98
+ if (!this.ctx.settings.writable) throw new Error('settings provider is read-only')
99
+ await this.ctx.settings.replace(EYE_CARE_SETTINGS_NS, request.value, request.expectedRevision)
100
+ return { ok: true, value: this.snapshot() }
101
+ } catch (error) {
102
+ if (error instanceof SettingsConflictError) {
103
+ return {
104
+ ok: false,
105
+ error: {
106
+ code: 'settings-conflict',
107
+ message: publicMessage(error),
108
+ details: {
109
+ ns: String(EYE_CARE_SETTINGS_NS),
110
+ expected: error.expected,
111
+ actual: error.actual,
112
+ },
113
+ },
114
+ }
115
+ }
116
+ return {
117
+ ok: false,
118
+ error: {
119
+ code: 'settings-rejected',
120
+ message: publicMessage(error),
121
+ details: { ns: String(EYE_CARE_SETTINGS_NS) },
122
+ },
123
+ }
124
+ }
125
+ }
126
+ }
127
+
128
+ /** Register the RPC channel when the Web Connection Host is composed. */
129
+ export function installEyeCareRpc(ctx: Context): void {
130
+ const backend = new EyeCareRpcBackend(ctx)
131
+ ctx.inject(['connection'], (connectionCtx) => {
132
+ connectionCtx.connection.rpc.handle(
133
+ EYE_CARE_RPC_CHANNEL,
134
+ backend.handle,
135
+ { authority: 'loopback' },
136
+ )
137
+ })
138
+ }
@@ -0,0 +1,22 @@
1
+ /** Host-only schema and branded namespace for eye-care settings. */
2
+
3
+ import z from '@deepseek-ai/schemastery'
4
+ import { settingsNamespace } from '@deepseek-ai/dsh-settings'
5
+ import {
6
+ DEFAULT_EYE_CARE_SETTINGS,
7
+ EYE_CARE_INTENSITIES,
8
+ EYE_CARE_MODES,
9
+ EYE_CARE_SETTINGS_NAMESPACE,
10
+ INTENSITY_FIELD,
11
+ MODE_FIELD,
12
+ type EyeCareSettings,
13
+ } from './shared.ts'
14
+
15
+ /** Branded settings namespace used by the Host service. */
16
+ export const EYE_CARE_SETTINGS_NS = settingsNamespace(EYE_CARE_SETTINGS_NAMESPACE)
17
+
18
+ /** Host schema for the eye-care settings section. */
19
+ export const EyeCareSettingsSchema: z<EyeCareSettings> = z.object({
20
+ [MODE_FIELD]: z.union([...EYE_CARE_MODES]).default(DEFAULT_EYE_CARE_SETTINGS.mode),
21
+ [INTENSITY_FIELD]: z.union([...EYE_CARE_INTENSITIES]).default(DEFAULT_EYE_CARE_SETTINGS.intensity),
22
+ })
package/src/shared.ts ADDED
@@ -0,0 +1,112 @@
1
+ /** Browser-safe eye-care settings and RPC wire values. */
2
+
3
+ /** User-selectable eye-care operating modes. */
4
+ export const EYE_CARE_MODES = ['off', 'auto', 'light', 'dark'] as const
5
+
6
+ /** Warmth levels shared by light and dark palettes. */
7
+ export const EYE_CARE_INTENSITIES = ['soft', 'balanced', 'warm'] as const
8
+
9
+ /** Eye-care operating mode. */
10
+ export type EyeCareMode = typeof EYE_CARE_MODES[number]
11
+
12
+ /** Eye-care warmth level. */
13
+ export type EyeCareIntensity = typeof EYE_CARE_INTENSITIES[number]
14
+
15
+ /** Settings namespace owned by this package. */
16
+ export const EYE_CARE_SETTINGS_NAMESPACE = 'eye-care'
17
+
18
+ /** Loopback-only Connection RPC channel owned by this package. */
19
+ export const EYE_CARE_RPC_CHANNEL = '/eye-care'
20
+
21
+ /** RPC endpoint that reads the current settings snapshot. */
22
+ export const EYE_CARE_RPC_READ = 'settings.read'
23
+
24
+ /** RPC endpoint that replaces the settings section. */
25
+ export const EYE_CARE_RPC_SAVE = 'settings.save'
26
+
27
+ /** Settings field carrying the selected mode. */
28
+ export const MODE_FIELD = 'mode'
29
+
30
+ /** Settings field carrying the selected warmth. */
31
+ export const INTENSITY_FIELD = 'intensity'
32
+
33
+ /** Defaults used when no user section exists. */
34
+ export const DEFAULT_EYE_CARE_SETTINGS: Readonly<EyeCareSettings> = Object.freeze({
35
+ mode: 'off',
36
+ intensity: 'balanced',
37
+ })
38
+
39
+ /** Persisted eye-care preferences. */
40
+ export interface EyeCareSettings {
41
+ /** Whether eye care is disabled, system-aware, light, or dark. */
42
+ mode: EyeCareMode
43
+ /** Warmth applied by the active eye-care palette. */
44
+ intensity: EyeCareIntensity
45
+ }
46
+
47
+ /** Browser-safe Host snapshot returned through the loopback RPC channel. */
48
+ export interface EyeCareSettingsSnapshot {
49
+ /** Wire schema version for this dedicated endpoint. */
50
+ schemaVersion: 1
51
+ /** Whether the active settings provider accepts writes. */
52
+ writable: boolean
53
+ /** Resolved settings and the revision fencing the next write. */
54
+ settings: {
55
+ value: EyeCareSettings
56
+ revision: number
57
+ applies: 'live'
58
+ }
59
+ /** Durable built-in Appearance preference used to fence startup convergence. */
60
+ baseThemePreference?: 'light' | 'dark' | 'system'
61
+ }
62
+
63
+ /** Revision-fenced write request. */
64
+ export interface EyeCareSettingsWriteRequest {
65
+ expectedRevision: number
66
+ value: EyeCareSettings
67
+ }
68
+
69
+ /** Narrow an unknown value to one supported mode. */
70
+ export function isEyeCareMode(value: unknown): value is EyeCareMode {
71
+ return EYE_CARE_MODES.some(mode => mode === value)
72
+ }
73
+
74
+ /** Narrow an unknown value to one supported warmth. */
75
+ export function isEyeCareIntensity(value: unknown): value is EyeCareIntensity {
76
+ return EYE_CARE_INTENSITIES.some(intensity => intensity === value)
77
+ }
78
+
79
+ /** Decode a complete settings value from the RPC boundary. */
80
+ export function decodeEyeCareSettings(value: unknown): EyeCareSettings | undefined {
81
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined
82
+ const record = value as Record<string, unknown>
83
+ if (!isEyeCareMode(record[MODE_FIELD]) || !isEyeCareIntensity(record[INTENSITY_FIELD])) return undefined
84
+ return { mode: record[MODE_FIELD], intensity: record[INTENSITY_FIELD] }
85
+ }
86
+
87
+ /** Decode one complete Host snapshot from the RPC boundary. */
88
+ export function decodeEyeCareSnapshot(value: unknown): EyeCareSettingsSnapshot | undefined {
89
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) return undefined
90
+ const record = value as Record<string, unknown>
91
+ const settingsEnvelope = record['settings']
92
+ if (record['schemaVersion'] !== 1 || typeof record['writable'] !== 'boolean'
93
+ || typeof settingsEnvelope !== 'object' || settingsEnvelope === null || Array.isArray(settingsEnvelope)) return undefined
94
+ const settingsRecord = settingsEnvelope as Record<string, unknown>
95
+ const settings = decodeEyeCareSettings(settingsRecord['value'])
96
+ const revision = settingsRecord['revision']
97
+ if (settings === undefined || !Number.isSafeInteger(revision) || (revision as number) < 0
98
+ || settingsRecord['applies'] !== 'live') return undefined
99
+ const baseThemePreference = record['baseThemePreference']
100
+ if (baseThemePreference !== undefined
101
+ && baseThemePreference !== 'light' && baseThemePreference !== 'dark' && baseThemePreference !== 'system') return undefined
102
+ return {
103
+ schemaVersion: 1,
104
+ writable: record['writable'],
105
+ settings: {
106
+ value: settings,
107
+ revision: revision as number,
108
+ applies: 'live',
109
+ },
110
+ ...(baseThemePreference === undefined ? {} : { baseThemePreference }),
111
+ }
112
+ }