@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,14 @@
1
+ /** Fixed eye-care themes built from semantic DSH Web tokens. */
2
+ import type { ThemeDefinition } from '@deepseek-ai/dsh-client-ui-theme/client';
3
+ import type { EyeCareIntensity } from '../shared.ts';
4
+ /** Theme id prefix reserved by this package. */
5
+ export declare const EYE_CARE_THEME_PREFIX = "eye-care-";
6
+ type Scheme = 'light' | 'dark';
7
+ /** Build the stable id for one concrete eye-care theme. */
8
+ export declare function eyeCareThemeId(scheme: Scheme, intensity: EyeCareIntensity): string;
9
+ /** Six concrete themes registered for the two schemes and three warmth levels. */
10
+ export declare const EYE_CARE_THEMES: readonly ThemeDefinition[];
11
+ /** Whether a theme id is one of this package's registered concrete themes. */
12
+ export declare function isEyeCareThemeId(id: string): boolean;
13
+ export {};
14
+ //# sourceMappingURL=themes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"themes.d.ts","sourceRoot":"","sources":["../../../src/client/themes.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAEhE,OAAO,KAAK,EAAE,eAAe,EAAe,MAAM,yCAAyC,CAAA;AAC3F,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEpD,gDAAgD;AAChD,eAAO,MAAM,qBAAqB,cAAc,CAAA;AAEhD,KAAK,MAAM,GAAG,OAAO,GAAG,MAAM,CAAA;AAsJ9B,2DAA2D;AAC3D,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,gBAAgB,GAAG,MAAM,CAElF;AA6ED,kFAAkF;AAClF,eAAO,MAAM,eAAe,EAAE,SAAS,eAAe,EAOrD,CAAA;AAID,8EAA8E;AAC9E,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAEpD"}
@@ -0,0 +1,10 @@
1
+ /** DSH eye-care Host plugin: settings ownership and loopback RPC transport. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ export { DEFAULT_EYE_CARE_SETTINGS, EYE_CARE_INTENSITIES, EYE_CARE_MODES, EYE_CARE_SETTINGS_NAMESPACE, EYE_CARE_RPC_CHANNEL, EYE_CARE_RPC_READ, EYE_CARE_RPC_SAVE, type EyeCareIntensity, type EyeCareMode, type EyeCareSettings, } from './shared.ts';
4
+ export { EYE_CARE_SETTINGS_NS, EyeCareSettingsSchema } from './settings.ts';
5
+ export { EyeCareRpcBackend } from './rpc.ts';
6
+ /** Required Host capability. */
7
+ export declare const inject: string[];
8
+ /** Register the durable namespace and the optional loopback browser transport. */
9
+ export declare function apply(ctx: Context): void;
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAE/E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAKlD,OAAO,EACL,yBAAyB,EACzB,oBAAoB,EACpB,cAAc,EACd,2BAA2B,EAC3B,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,eAAe,GACrB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAA;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAA;AAE5C,gCAAgC;AAChC,eAAO,MAAM,MAAM,UAAe,CAAA;AAElC,kFAAkF;AAClF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAGxC"}
@@ -0,0 +1,17 @@
1
+ /** Loopback-only Connection RPC for durable eye-care preferences. */
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection';
4
+ import { type EyeCareSettingsSnapshot } from './shared.ts';
5
+ /** Host backend used by the loopback-only Connection RPC registration. */
6
+ export declare class EyeCareRpcBackend {
7
+ private readonly ctx;
8
+ /** Create a backend over the active Settings service. */
9
+ constructor(ctx: Context);
10
+ /** Read the current browser-safe settings snapshot. */
11
+ snapshot(): EyeCareSettingsSnapshot;
12
+ /** Handle one Connection RPC endpoint. */
13
+ readonly handle: ConnectionRpcHandler;
14
+ }
15
+ /** Register the RPC channel when the Web Connection Host is composed. */
16
+ export declare function installEyeCareRpc(ctx: Context): void;
17
+ //# sourceMappingURL=rpc.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../../src/rpc.ts"],"names":[],"mappings":"AAAA,qEAAqE;AAErE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAA;AAG9E,OAAO,EAML,KAAK,uBAAuB,EAE7B,MAAM,aAAa,CAAA;AAiCpB,0EAA0E;AAC1E,qBAAa,iBAAiB;IAEhB,OAAO,CAAC,QAAQ,CAAC,GAAG;IADhC,yDAAyD;gBAC5B,GAAG,EAAE,OAAO;IAEzC,uDAAuD;IACvD,QAAQ,IAAI,uBAAuB;IAenC,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAuDpC;CACF;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CASpD"}
@@ -0,0 +1,8 @@
1
+ /** Host-only schema and branded namespace for eye-care settings. */
2
+ import z from '@deepseek-ai/schemastery';
3
+ import { type EyeCareSettings } from './shared.ts';
4
+ /** Branded settings namespace used by the Host service. */
5
+ export declare const EYE_CARE_SETTINGS_NS: import("@deepseek-ai/dsh-settings").SettingsNamespace;
6
+ /** Host schema for the eye-care settings section. */
7
+ export declare const EyeCareSettingsSchema: z<EyeCareSettings>;
8
+ //# sourceMappingURL=settings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/settings.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,OAAO,CAAC,MAAM,0BAA0B,CAAA;AAExC,OAAO,EAOL,KAAK,eAAe,EACrB,MAAM,aAAa,CAAA;AAEpB,2DAA2D;AAC3D,eAAO,MAAM,oBAAoB,uDAAiD,CAAA;AAElF,qDAAqD;AACrD,eAAO,MAAM,qBAAqB,EAAE,CAAC,CAAC,eAAe,CAGnD,CAAA"}
@@ -0,0 +1,59 @@
1
+ /** Browser-safe eye-care settings and RPC wire values. */
2
+ /** User-selectable eye-care operating modes. */
3
+ export declare const EYE_CARE_MODES: readonly ["off", "auto", "light", "dark"];
4
+ /** Warmth levels shared by light and dark palettes. */
5
+ export declare const EYE_CARE_INTENSITIES: readonly ["soft", "balanced", "warm"];
6
+ /** Eye-care operating mode. */
7
+ export type EyeCareMode = typeof EYE_CARE_MODES[number];
8
+ /** Eye-care warmth level. */
9
+ export type EyeCareIntensity = typeof EYE_CARE_INTENSITIES[number];
10
+ /** Settings namespace owned by this package. */
11
+ export declare const EYE_CARE_SETTINGS_NAMESPACE = "eye-care";
12
+ /** Loopback-only Connection RPC channel owned by this package. */
13
+ export declare const EYE_CARE_RPC_CHANNEL = "/eye-care";
14
+ /** RPC endpoint that reads the current settings snapshot. */
15
+ export declare const EYE_CARE_RPC_READ = "settings.read";
16
+ /** RPC endpoint that replaces the settings section. */
17
+ export declare const EYE_CARE_RPC_SAVE = "settings.save";
18
+ /** Settings field carrying the selected mode. */
19
+ export declare const MODE_FIELD = "mode";
20
+ /** Settings field carrying the selected warmth. */
21
+ export declare const INTENSITY_FIELD = "intensity";
22
+ /** Defaults used when no user section exists. */
23
+ export declare const DEFAULT_EYE_CARE_SETTINGS: Readonly<EyeCareSettings>;
24
+ /** Persisted eye-care preferences. */
25
+ export interface EyeCareSettings {
26
+ /** Whether eye care is disabled, system-aware, light, or dark. */
27
+ mode: EyeCareMode;
28
+ /** Warmth applied by the active eye-care palette. */
29
+ intensity: EyeCareIntensity;
30
+ }
31
+ /** Browser-safe Host snapshot returned through the loopback RPC channel. */
32
+ export interface EyeCareSettingsSnapshot {
33
+ /** Wire schema version for this dedicated endpoint. */
34
+ schemaVersion: 1;
35
+ /** Whether the active settings provider accepts writes. */
36
+ writable: boolean;
37
+ /** Resolved settings and the revision fencing the next write. */
38
+ settings: {
39
+ value: EyeCareSettings;
40
+ revision: number;
41
+ applies: 'live';
42
+ };
43
+ /** Durable built-in Appearance preference used to fence startup convergence. */
44
+ baseThemePreference?: 'light' | 'dark' | 'system';
45
+ }
46
+ /** Revision-fenced write request. */
47
+ export interface EyeCareSettingsWriteRequest {
48
+ expectedRevision: number;
49
+ value: EyeCareSettings;
50
+ }
51
+ /** Narrow an unknown value to one supported mode. */
52
+ export declare function isEyeCareMode(value: unknown): value is EyeCareMode;
53
+ /** Narrow an unknown value to one supported warmth. */
54
+ export declare function isEyeCareIntensity(value: unknown): value is EyeCareIntensity;
55
+ /** Decode a complete settings value from the RPC boundary. */
56
+ export declare function decodeEyeCareSettings(value: unknown): EyeCareSettings | undefined;
57
+ /** Decode one complete Host snapshot from the RPC boundary. */
58
+ export declare function decodeEyeCareSnapshot(value: unknown): EyeCareSettingsSnapshot | undefined;
59
+ //# sourceMappingURL=shared.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../src/shared.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAE1D,gDAAgD;AAChD,eAAO,MAAM,cAAc,2CAA4C,CAAA;AAEvE,uDAAuD;AACvD,eAAO,MAAM,oBAAoB,uCAAwC,CAAA;AAEzE,+BAA+B;AAC/B,MAAM,MAAM,WAAW,GAAG,OAAO,cAAc,CAAC,MAAM,CAAC,CAAA;AAEvD,6BAA6B;AAC7B,MAAM,MAAM,gBAAgB,GAAG,OAAO,oBAAoB,CAAC,MAAM,CAAC,CAAA;AAElE,gDAAgD;AAChD,eAAO,MAAM,2BAA2B,aAAa,CAAA;AAErD,kEAAkE;AAClE,eAAO,MAAM,oBAAoB,cAAc,CAAA;AAE/C,6DAA6D;AAC7D,eAAO,MAAM,iBAAiB,kBAAkB,CAAA;AAEhD,uDAAuD;AACvD,eAAO,MAAM,iBAAiB,kBAAkB,CAAA;AAEhD,iDAAiD;AACjD,eAAO,MAAM,UAAU,SAAS,CAAA;AAEhC,mDAAmD;AACnD,eAAO,MAAM,eAAe,cAAc,CAAA;AAE1C,iDAAiD;AACjD,eAAO,MAAM,yBAAyB,EAAE,QAAQ,CAAC,eAAe,CAG9D,CAAA;AAEF,sCAAsC;AACtC,MAAM,WAAW,eAAe;IAC9B,kEAAkE;IAClE,IAAI,EAAE,WAAW,CAAA;IACjB,qDAAqD;IACrD,SAAS,EAAE,gBAAgB,CAAA;CAC5B;AAED,4EAA4E;AAC5E,MAAM,WAAW,uBAAuB;IACtC,uDAAuD;IACvD,aAAa,EAAE,CAAC,CAAA;IAChB,2DAA2D;IAC3D,QAAQ,EAAE,OAAO,CAAA;IACjB,iEAAiE;IACjE,QAAQ,EAAE;QACR,KAAK,EAAE,eAAe,CAAA;QACtB,QAAQ,EAAE,MAAM,CAAA;QAChB,OAAO,EAAE,MAAM,CAAA;KAChB,CAAA;IACD,gFAAgF;IAChF,mBAAmB,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAA;CAClD;AAED,qCAAqC;AACrC,MAAM,WAAW,2BAA2B;IAC1C,gBAAgB,EAAE,MAAM,CAAA;IACxB,KAAK,EAAE,eAAe,CAAA;CACvB;AAED,qDAAqD;AACrD,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAElE;AAED,uDAAuD;AACvD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAE5E;AAED,8DAA8D;AAC9D,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,GAAG,SAAS,CAKjF;AAED,+DAA+D;AAC/D,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,uBAAuB,GAAG,SAAS,CAwBzF"}
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "@anionex/dsh-eye-care",
3
+ "version": "0.1.0",
4
+ "description": "Warm light, warm dark, and system-aware eye-care themes for DSH Web",
5
+ "keywords": [
6
+ "deepseek",
7
+ "deepseek-harness",
8
+ "eye-care",
9
+ "theme",
10
+ "dark-mode",
11
+ "accessibility"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/Anionex/dsh-eye-care.git"
16
+ },
17
+ "bugs": {
18
+ "url": "https://github.com/Anionex/dsh-eye-care/issues"
19
+ },
20
+ "type": "module",
21
+ "packageManager": "pnpm@11.7.0",
22
+ "engines": {
23
+ "node": "^22.19.0 || >=24.0.0"
24
+ },
25
+ "main": "./lib/index.js",
26
+ "types": "./lib/types/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./lib/types/index.d.ts",
30
+ "default": "./lib/index.js"
31
+ },
32
+ "./client": {
33
+ "types": "./lib/types/client/index.d.ts",
34
+ "default": "./lib/client.js"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "files": [
39
+ "lib",
40
+ "src",
41
+ "README.md",
42
+ "README.zh.md",
43
+ "README.i18n.yaml",
44
+ "assets",
45
+ "cordis.patch.yml",
46
+ "LICENSE"
47
+ ],
48
+ "scripts": {
49
+ "build": "node --input-type=module -e \"import { rmSync } from 'node:fs'; for (const path of ['lib', '.client-build']) rmSync(path, { recursive: true, force: true })\" && tsc -p tsconfig.json && tsc -p tsconfig.client.json && tsdown -c tsdown.config.mjs",
50
+ "check": "tsc -p tsconfig.json --noEmit --pretty false && tsc -p tsconfig.client.json --noEmit --pretty false",
51
+ "prepack": "pnpm run build",
52
+ "test": "pnpm run build && vitest run"
53
+ },
54
+ "dsh": {
55
+ "bundle": {
56
+ "patch": "./cordis.patch.yml"
57
+ },
58
+ "client": {
59
+ "platform": "web",
60
+ "inject": [
61
+ "@deepseek-ai/dsh-client-connection",
62
+ "@deepseek-ai/dsh-client-runtime",
63
+ "@deepseek-ai/dsh-client-locale",
64
+ "@deepseek-ai/dsh-client-ui-settings",
65
+ "@deepseek-ai/dsh-api-remotes",
66
+ "@deepseek-ai/dsh-client-ui-theme"
67
+ ]
68
+ }
69
+ },
70
+ "peerDependencies": {
71
+ "@deepseek-ai/dsh-api-remotes": "^0.1.0-rc.6",
72
+ "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
73
+ "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
74
+ "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
75
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
76
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
77
+ "@deepseek-ai/dsh-client-ui-theme": "^0.1.0-rc.6",
78
+ "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
79
+ "@deepseek-ai/cordis": "^4.0.1",
80
+ "react": "^18.2.0",
81
+ "@deepseek-ai/schemastery": "^3.18.1"
82
+ },
83
+ "devDependencies": {
84
+ "@testing-library/react": "16.3.2",
85
+ "@testing-library/dom": "10.4.1",
86
+ "@types/node": "22.20.0",
87
+ "@types/react": "18.3.31",
88
+ "@types/react-dom": "18.3.7",
89
+ "jsdom": "29.1.1",
90
+ "lightningcss": "1.32.0",
91
+ "react": "18.3.1",
92
+ "react-dom": "18.3.1",
93
+ "tsdown": "0.22.2",
94
+ "typescript": "6.0.3",
95
+ "vite-tsconfig-paths": "6.1.1",
96
+ "vitest": "4.1.8"
97
+ },
98
+ "license": "MIT"
99
+ }
@@ -0,0 +1,137 @@
1
+ /* Eye-care row: compact controls that remain usable in the narrow General panel. */
2
+
3
+ .row {
4
+ display: grid;
5
+ grid-template-columns: minmax(170px, 0.8fr) minmax(360px, 1.2fr);
6
+ gap: 20px;
7
+ padding: 16px 0;
8
+ border-bottom: 1px solid var(--dsw-alias-border-l2);
9
+ }
10
+
11
+ .copy {
12
+ min-width: 0;
13
+ }
14
+
15
+ .title {
16
+ margin: 0;
17
+ color: var(--dsw-alias-label-primary);
18
+ font-size: 14px;
19
+ font-weight: 500;
20
+ line-height: 22px;
21
+ }
22
+
23
+ .description {
24
+ margin: 4px 0 0;
25
+ color: var(--dsw-alias-label-tertiary);
26
+ font-size: 12px;
27
+ line-height: 18px;
28
+ }
29
+
30
+ .controls {
31
+ display: grid;
32
+ gap: 8px;
33
+ align-content: start;
34
+ }
35
+
36
+ .controlGroup {
37
+ display: flex;
38
+ flex-wrap: wrap;
39
+ gap: 6px;
40
+ }
41
+
42
+ .option,
43
+ .smallOption {
44
+ border: 1px solid var(--dsw-alias-border-l2);
45
+ background: var(--dsw-alias-bg-layer-1);
46
+ color: var(--dsw-alias-label-secondary);
47
+ font: inherit;
48
+ cursor: pointer;
49
+ }
50
+
51
+ .option:focus-visible,
52
+ .smallOption:focus-visible {
53
+ outline: 2px solid var(--dsw-alias-button-info-fill);
54
+ outline-offset: 2px;
55
+ }
56
+
57
+ .option {
58
+ min-width: 68px;
59
+ padding: 7px 11px;
60
+ border-radius: 9px;
61
+ font-size: 12px;
62
+ line-height: 18px;
63
+ }
64
+
65
+ .smallOption {
66
+ padding: 4px 9px;
67
+ border-radius: 7px;
68
+ font-size: 11px;
69
+ line-height: 16px;
70
+ }
71
+
72
+ .option:hover:not(:disabled),
73
+ .smallOption:hover:not(:disabled) {
74
+ background: var(--dsw-alias-interactive-bg-hover-solid);
75
+ }
76
+
77
+ .selected {
78
+ border-color: var(--dsw-alias-button-info-fill);
79
+ background: var(--dsw-alias-interactive-bg-active);
80
+ color: var(--dsw-alias-label-primary);
81
+ }
82
+
83
+ .option:disabled,
84
+ .smallOption:disabled {
85
+ cursor: wait;
86
+ opacity: 0.65;
87
+ }
88
+
89
+ .primaryLine,
90
+ .settingLine {
91
+ display: flex;
92
+ align-items: center;
93
+ flex-wrap: wrap;
94
+ gap: 8px;
95
+ }
96
+
97
+ .settingLabel,
98
+ .status {
99
+ color: var(--dsw-alias-label-tertiary);
100
+ font-size: 11px;
101
+ line-height: 16px;
102
+ }
103
+
104
+ .settingLabel {
105
+ min-width: 56px;
106
+ }
107
+
108
+ .advanced {
109
+ border: 1px solid var(--dsw-alias-border-l2);
110
+ border-radius: 9px;
111
+ overflow: hidden;
112
+ }
113
+
114
+ .advanced > summary {
115
+ padding: 8px 10px;
116
+ color: var(--dsw-alias-label-secondary);
117
+ font-size: 11px;
118
+ line-height: 16px;
119
+ cursor: pointer;
120
+ }
121
+
122
+ .advancedBody {
123
+ display: grid;
124
+ gap: 8px;
125
+ padding: 0 10px 10px;
126
+ }
127
+
128
+ .status {
129
+ min-height: 16px;
130
+ }
131
+
132
+ @media (max-width: 640px) {
133
+ .row {
134
+ grid-template-columns: 1fr;
135
+ gap: 10px;
136
+ }
137
+ }
@@ -0,0 +1,122 @@
1
+ /** General-settings row for eye-care mode and warmth controls. */
2
+
3
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'
4
+ import type { EyeCareController } from './controller.ts'
5
+ import type { EyeCareKey } from './locales.ts'
6
+ import type { EyeCareIntensity, EyeCareMode } from '../shared.ts'
7
+ import css from './EyeCareRow.module.css'
8
+
9
+ /** Injected business face used by the row. */
10
+ export interface EyeCareRowInjected {
11
+ /** State source. */
12
+ hooks: { eyeCare: EyeCareController['store'] }
13
+ /** Set the selected mode. */
14
+ setMode: (mode: EyeCareMode) => Promise<void>
15
+ /** Set the selected warmth. */
16
+ setIntensity: (intensity: EyeCareIntensity) => Promise<void>
17
+ }
18
+
19
+ /** Full composed props for the settings slot. */
20
+ export type EyeCareRowProps = PropsRuntime<'settings.general.item'>
21
+ & PropsLocale<'settings.eyeCare'>
22
+ & InjectFace<EyeCareRowInjected>
23
+
24
+ const MODES: readonly { id: EyeCareMode; label: EyeCareKey }[] = [
25
+ { id: 'off', label: 'mode.off' },
26
+ { id: 'auto', label: 'mode.auto' },
27
+ { id: 'light', label: 'mode.light' },
28
+ { id: 'dark', label: 'mode.dark' },
29
+ ]
30
+
31
+ const INTENSITIES: readonly { id: EyeCareIntensity; label: EyeCareKey }[] = [
32
+ { id: 'soft', label: 'intensity.soft' },
33
+ { id: 'balanced', label: 'intensity.balanced' },
34
+ { id: 'warm', label: 'intensity.warm' },
35
+ ]
36
+
37
+ function statusKey(mode: EyeCareMode): EyeCareKey {
38
+ return `status.${mode}` as EyeCareKey
39
+ }
40
+
41
+ /** Render the feature-owned General settings row. */
42
+ export function EyeCareRow({ t, useEyeCare, setMode, setIntensity }: EyeCareRowProps) {
43
+ const state = useEyeCare(value => value)
44
+ const busy = state.status === 'loading' || state.status === 'saving'
45
+ const enabled = state.settings.mode !== 'off'
46
+ const status = state.status === 'error' && state.error !== null ? state.error : t(statusKey(state.settings.mode))
47
+ const statusText = state.status === 'unavailable' ? `${status} · ${t('status.local')}` : status
48
+ return (
49
+ <section className={css.row} aria-labelledby="dsh-eye-care-title">
50
+ <div className={css.copy}>
51
+ <h3 id="dsh-eye-care-title" className={css.title}>{t('title')}</h3>
52
+ <p className={css.description}>{t('description')}</p>
53
+ </div>
54
+ <div className={css.controls}>
55
+ <div className={css.primaryLine}>
56
+ <div className={css.controlGroup} role="group" aria-label={t('enabled.title')}>
57
+ <button
58
+ type="button"
59
+ className={`${css.option}${!enabled ? ` ${css.selected}` : ''}`}
60
+ aria-pressed={!enabled}
61
+ disabled={busy}
62
+ onClick={() => { void setMode('off') }}
63
+ >
64
+ {t('enabled.off')}
65
+ </button>
66
+ <button
67
+ type="button"
68
+ className={`${css.option}${enabled ? ` ${css.selected}` : ''}`}
69
+ aria-pressed={enabled}
70
+ disabled={busy}
71
+ onClick={() => { void setMode(state.settings.mode === 'off' ? 'auto' : state.settings.mode) }}
72
+ >
73
+ {t('enabled.on')}
74
+ </button>
75
+ </div>
76
+ </div>
77
+ {enabled ? <details className={css.advanced}>
78
+ <summary>{t('advanced.title')}</summary>
79
+ <div className={css.advancedBody}>
80
+ <div className={css.settingLine}>
81
+ <span className={css.settingLabel}>{t('mode.title')}</span>
82
+ <div className={css.controlGroup} role="group" aria-label={t('mode.title')}>
83
+ {MODES.filter(({ id }) => id !== 'off').map(({ id, label }) => (
84
+ <button
85
+ key={id}
86
+ type="button"
87
+ className={`${css.smallOption}${state.settings.mode === id ? ` ${css.selected}` : ''}`}
88
+ aria-pressed={state.settings.mode === id}
89
+ disabled={busy}
90
+ onClick={() => { void setMode(id) }}
91
+ >
92
+ {t(label)}
93
+ </button>
94
+ ))}
95
+ </div>
96
+ </div>
97
+ <div className={css.settingLine}>
98
+ <span className={css.settingLabel}>{t('intensity.title')}</span>
99
+ <div className={css.controlGroup} role="group" aria-label={t('intensity.title')}>
100
+ {INTENSITIES.map(({ id, label }) => (
101
+ <button
102
+ key={id}
103
+ type="button"
104
+ className={`${css.smallOption}${state.settings.intensity === id ? ` ${css.selected}` : ''}`}
105
+ aria-pressed={state.settings.intensity === id}
106
+ disabled={busy}
107
+ onClick={() => { void setIntensity(id) }}
108
+ >
109
+ {t(label)}
110
+ </button>
111
+ ))}
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </details> : null}
116
+ <div className={css.status} aria-live="polite">
117
+ {state.status === 'saving' ? t('status.saving') : statusText}
118
+ </div>
119
+ </div>
120
+ </section>
121
+ )
122
+ }