@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.
- package/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +86 -0
- package/README.zh.md +86 -0
- package/assets/eye-care-dark.png +0 -0
- package/assets/eye-care-light.png +0 -0
- package/cordis.patch.yml +3 -0
- package/lib/client.js +1106 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +14 -0
- package/lib/index.js.map +1 -0
- package/lib/rpc.js +123 -0
- package/lib/rpc.js.map +1 -0
- package/lib/settings.js +12 -0
- package/lib/settings.js.map +1 -0
- package/lib/shared.js +70 -0
- package/lib/shared.js.map +1 -0
- package/lib/types/client/EyeCareRow.d.ts +20 -0
- package/lib/types/client/EyeCareRow.d.ts.map +1 -0
- package/lib/types/client/controller.d.ts +96 -0
- package/lib/types/client/controller.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +23 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +54 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/themes.d.ts +14 -0
- package/lib/types/client/themes.d.ts.map +1 -0
- package/lib/types/index.d.ts +10 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/rpc.d.ts +17 -0
- package/lib/types/rpc.d.ts.map +1 -0
- package/lib/types/settings.d.ts +8 -0
- package/lib/types/settings.d.ts.map +1 -0
- package/lib/types/shared.d.ts +59 -0
- package/lib/types/shared.d.ts.map +1 -0
- package/package.json +99 -0
- package/src/client/EyeCareRow.module.css +137 -0
- package/src/client/EyeCareRow.tsx +122 -0
- package/src/client/controller.ts +435 -0
- package/src/client/css-modules.d.ts +5 -0
- package/src/client/index.tsx +72 -0
- package/src/client/locales.ts +56 -0
- package/src/client/themes.ts +254 -0
- package/src/index.ts +30 -0
- package/src/rpc.ts +138 -0
- package/src/settings.ts +22 -0
- 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
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -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
|
+
}
|