dsh-custom-theme 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/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "dsh-custom-theme",
3
+ "version": "0.1.0",
4
+ "description": "User-editable CSS themes and background images for the DSH Web GUI and Desktop app, with pickers in Settings",
5
+ "license": "MIT",
6
+ "author": "Sparrived",
7
+ "type": "module",
8
+ "main": "./src/index.mjs",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Sparrived/dsh-custom-theme.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/Sparrived/dsh-custom-theme/issues"
15
+ },
16
+ "homepage": "https://github.com/Sparrived/dsh-custom-theme#readme",
17
+ "exports": {
18
+ ".": "./src/index.mjs",
19
+ "./client": "./lib/client.js",
20
+ "./package.json": "./package.json"
21
+ },
22
+ "dsh": {
23
+ "client": {
24
+ "platform": "web",
25
+ "inject": [
26
+ "@deepseek-ai/dsh-client-ui-settings"
27
+ ]
28
+ },
29
+ "bundle": {
30
+ "patch": "./cordis.patch.yml"
31
+ }
32
+ },
33
+ "files": [
34
+ "src",
35
+ "lib/client.js",
36
+ "themes",
37
+ "cordis.patch.yml",
38
+ "README.md"
39
+ ],
40
+ "scripts": {
41
+ "test": "node --test \"test/**/*.test.mjs\"",
42
+ "test:browser": "node test/browser/appearance.mjs"
43
+ },
44
+ "keywords": [
45
+ "dsh",
46
+ "cordis",
47
+ "plugin",
48
+ "theme"
49
+ ]
50
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,289 @@
1
+ /**
2
+ * dsh-custom-theme — Host half.
3
+ *
4
+ * Owns a user-editable CSS theme directory (`$DSH_HOME/themes`) and serves it to
5
+ * the browser half over a prefix route on the Web Host. Bundled themes are
6
+ * seeded once, and a user who drops an extra `.css` file into the directory gets
7
+ * a new entry in the Appearance picker without touching any configuration.
8
+ *
9
+ * Imports no third-party modules: the row must stay loadable by a profile that
10
+ * links this package directly, with no peer resolution for a Schemastery `Config`
11
+ * schema yet. `config` is therefore read defensively.
12
+ */
13
+
14
+ import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
15
+ import { homedir } from 'node:os'
16
+ import { join } from 'node:path'
17
+ import { fileURLToPath } from 'node:url'
18
+
19
+ import {
20
+ BUNDLED_THEME_IDS,
21
+ MAX_BACKGROUND_BYTES,
22
+ MAX_THEME_BYTES,
23
+ SEED_VERSION,
24
+ backgroundContentType,
25
+ backgroundsDirectory,
26
+ isBackgroundName,
27
+ isBundledThemeId,
28
+ isThemeId,
29
+ orderThemeIds,
30
+ themesDirectory,
31
+ } from './themes.mjs'
32
+
33
+ /**
34
+ * Prefix route owned by this plugin. `dsh-client-ui-theme` and the SPA dist use
35
+ * other paths.
36
+ *
37
+ * No trailing slash: the webserver matches a prefix `p` against `p` itself and
38
+ * `p/<anything>`, so registering `'/dsh-custom-theme/'` would only ever match
39
+ * `'/dsh-custom-theme//…'` and every real request would fall through.
40
+ */
41
+ const ROUTE_PATH = '/dsh-custom-theme'
42
+
43
+ /** Seeded stylesheets live beside the package root, one level above this module. */
44
+ const SEED_ROOT = fileURLToPath(new URL('../themes/', import.meta.url))
45
+
46
+ export const name = 'dsh-custom-theme'
47
+
48
+ /**
49
+ * Resolve the DSH home, honouring the environment override the runtime sets.
50
+ * @returns The home directory whose `themes` directory this plugin owns.
51
+ */
52
+ function dshHome() {
53
+ const configured = process.env.DSH_HOME
54
+ return typeof configured === 'string' && configured.trim() !== '' ? configured : join(homedir(), '.dsh')
55
+ }
56
+
57
+ /**
58
+ * Copy each bundled stylesheet into the theme directory.
59
+ *
60
+ * Within one seed generation an existing file is left alone, so a user edit
61
+ * survives every restart and a deletion is only repaired. When the generation
62
+ * advances the bundled stylesheets are written again, because a fixed or extended
63
+ * bundled palette has to reach a machine that already holds the older copy. Only
64
+ * bundled ids are ever written; anything else in the directory is the user's.
65
+ *
66
+ * The theme directory is owned by this plugin, and `themes/*.css` documents that
67
+ * a customised palette belongs in a copy under its own name rather than in an
68
+ * edited bundled file.
69
+ * @param directory - Theme directory, created when missing.
70
+ * @returns Ids written by this call, for the startup log.
71
+ */
72
+ async function seedThemes(directory) {
73
+ await mkdir(directory, { recursive: true })
74
+ const marker = join(directory, '.seed-version')
75
+ let generation = null
76
+ try {
77
+ generation = Number.parseInt(await readFile(marker, 'utf8'), 10)
78
+ } catch (error) {
79
+ if (error.code !== 'ENOENT') throw error
80
+ }
81
+ // A missing marker is a directory this plugin has not seeded, so every bundled
82
+ // file is written even if an unrelated file of the same name is already there.
83
+ const stale = generation !== SEED_VERSION
84
+ const seeded = []
85
+ for (const id of BUNDLED_THEME_IDS) {
86
+ const target = join(directory, `${id}.css`)
87
+ if (!stale) {
88
+ try {
89
+ await readFile(target)
90
+ continue
91
+ } catch (error) {
92
+ if (error.code !== 'ENOENT') throw error
93
+ }
94
+ }
95
+ await writeFile(target, await readFile(join(SEED_ROOT, `${id}.css`), 'utf8'), 'utf8')
96
+ seeded.push(id)
97
+ }
98
+ if (generation !== SEED_VERSION) await writeFile(marker, String(SEED_VERSION), 'utf8')
99
+ return seeded
100
+ }
101
+
102
+ /**
103
+ * List the theme ids present in the directory.
104
+ * @param directory - Theme directory.
105
+ * @returns Ordered ids without the `.css` suffix.
106
+ */
107
+ async function scanThemes(directory) {
108
+ const entries = await readdir(directory, { withFileTypes: true })
109
+ const ids = entries
110
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.css') && !entry.name.startsWith('.'))
111
+ .map((entry) => entry.name.slice(0, -'.css'.length))
112
+ return orderThemeIds(ids)
113
+ }
114
+
115
+ /**
116
+ * Write one response. Handlers own the raw `ServerResponse`.
117
+ * @param res - Response owned by this handler.
118
+ * @param status - HTTP status code.
119
+ * @param contentType - Response media type.
120
+ * @param body - Response body as text or bytes; empty for error statuses.
121
+ */
122
+ function send(res, status, contentType, body = '') {
123
+ const payload = Buffer.isBuffer(body) ? body : Buffer.from(body)
124
+ res.writeHead(status, {
125
+ 'content-type': contentType,
126
+ 'cache-control': 'no-store',
127
+ 'content-length': payload.length,
128
+ })
129
+ res.end(payload)
130
+ }
131
+
132
+ /**
133
+ * Percent-decode a request path.
134
+ * @param value - Raw path below the route prefix.
135
+ * @returns Decoded text, or `undefined` when the escape sequence is malformed.
136
+ */
137
+ function decodePath(value) {
138
+ try {
139
+ return decodeURIComponent(value)
140
+ } catch {
141
+ return undefined
142
+ }
143
+ }
144
+
145
+ /**
146
+ * List the background images present in the directory.
147
+ * @param directory - Background directory.
148
+ * @returns File names in alphabet order; an absent directory lists nothing.
149
+ */
150
+ async function scanBackgrounds(directory) {
151
+ let entries
152
+ try {
153
+ entries = await readdir(directory, { withFileTypes: true })
154
+ } catch (error) {
155
+ if (error.code === 'ENOENT') return []
156
+ throw error
157
+ }
158
+ return entries
159
+ .filter((entry) => entry.isFile() && !entry.name.startsWith('.') && isBackgroundName(entry.name))
160
+ .map((entry) => entry.name)
161
+ .sort((left, right) => left.localeCompare(right))
162
+ }
163
+
164
+ /**
165
+ * Read one background image under the file-name whitelist.
166
+ * @param directory - Background directory.
167
+ * @param name - Candidate file name from the request path.
168
+ * @returns The image bytes, or `undefined` when no such image exists or it is oversized.
169
+ */
170
+ async function readBackground(directory, name) {
171
+ if (!isBackgroundName(name)) return undefined
172
+ let bytes
173
+ try {
174
+ bytes = await readFile(join(directory, name))
175
+ } catch (error) {
176
+ if (error.code === 'ENOENT' || error.code === 'EISDIR') return undefined
177
+ throw error
178
+ }
179
+ return bytes.length > MAX_BACKGROUND_BYTES ? undefined : bytes
180
+ }
181
+
182
+ /**
183
+ * Read one theme stylesheet under the id whitelist.
184
+ * @param directory - Theme directory.
185
+ * @param id - Candidate theme id from the request path.
186
+ * @returns Stylesheet text, or `undefined` when no such theme exists or it is oversized.
187
+ */
188
+ async function readTheme(directory, id) {
189
+ if (!isThemeId(id)) return undefined
190
+ let text
191
+ try {
192
+ text = await readFile(join(directory, `${id}.css`), 'utf8')
193
+ } catch (error) {
194
+ if (error.code === 'ENOENT' || error.code === 'EISDIR') return undefined
195
+ throw error
196
+ }
197
+ return Buffer.byteLength(text) > MAX_THEME_BYTES ? undefined : text
198
+ }
199
+
200
+ /**
201
+ * Answer the listing, stylesheet and background routes. Every other path under
202
+ * the prefix is a 404; the route is `prefix`, so this handler owns the whole
203
+ * subtree.
204
+ * @param req - Request from the application origin.
205
+ * @param res - Response owned by this handler.
206
+ * @param paths - `themes` and `backgrounds` directories, plus `ready`, which
207
+ * settles once the initial seeding pass finished, so a request that arrives
208
+ * during startup never scans a directory that does not exist yet.
209
+ */
210
+ async function handleRoute(req, res, paths) {
211
+ await paths.ready
212
+ const url = new URL(req.url ?? '/', 'http://localhost')
213
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
214
+ send(res, 405, 'text/plain; charset=utf-8', 'method not allowed')
215
+ return
216
+ }
217
+ const rest = decodePath(url.pathname.slice(ROUTE_PATH.length + 1))
218
+ if (rest === undefined) {
219
+ send(res, 400, 'text/plain; charset=utf-8', 'malformed path')
220
+ return
221
+ }
222
+ if (rest === 'themes') {
223
+ const themes = (await scanThemes(paths.themes)).map((id) => ({ id, bundled: isBundledThemeId(id) }))
224
+ send(res, 200, 'application/json; charset=utf-8', JSON.stringify({ themes, dir: paths.themes }))
225
+ return
226
+ }
227
+ if (rest === 'backgrounds') {
228
+ const names = await scanBackgrounds(paths.backgrounds)
229
+ const backgrounds = names.map((name) => ({ name, url: `${ROUTE_PATH}/background/${encodeURIComponent(name)}` }))
230
+ send(res, 200, 'application/json; charset=utf-8', JSON.stringify({ backgrounds, dir: paths.backgrounds }))
231
+ return
232
+ }
233
+ const theme = /^theme\/([^/]+)\.css$/u.exec(rest)
234
+ if (theme !== null) {
235
+ const text = await readTheme(paths.themes, theme[1])
236
+ if (text === undefined) {
237
+ send(res, 404, 'text/plain; charset=utf-8', 'theme not found')
238
+ return
239
+ }
240
+ send(res, 200, 'text/css; charset=utf-8', text)
241
+ return
242
+ }
243
+ const background = /^background\/([^/]+)$/u.exec(rest)
244
+ const bytes = background === null ? undefined : await readBackground(paths.backgrounds, background[1])
245
+ if (bytes === undefined) {
246
+ send(res, 404, 'text/plain; charset=utf-8', 'background not found')
247
+ return
248
+ }
249
+ send(res, 200, backgroundContentType(background[1]), bytes)
250
+ }
251
+
252
+ /**
253
+ * Seed the theme directory and expose both directories over the Web Host.
254
+ *
255
+ * The route is registered through `ctx.inject` so a profile without
256
+ * `dsh-host-webserver` still seeds the theme directory instead of failing to
257
+ * load.
258
+ * @param ctx - Host Cordis context of this row.
259
+ * @param config - Row configuration; `themesDir` and `backgroundsDir` override
260
+ * the default locations.
261
+ */
262
+ export function apply(ctx, config) {
263
+ const themes = themesDirectory(config, dshHome())
264
+ const backgrounds = backgroundsDirectory(config, dshHome())
265
+ ctx.logger.info('dsh-custom-theme: themes directory %s', themes)
266
+ ctx.logger.info('dsh-custom-theme: backgrounds directory %s', backgrounds)
267
+ const ready = Promise.all([
268
+ seedThemes(themes).then((ids) => {
269
+ if (ids.length > 0) ctx.logger.info('dsh-custom-theme: seeded %s', ids.join(', '))
270
+ }),
271
+ // The background directory holds only user-supplied images, so it is created
272
+ // but never seeded; the card lists it as empty until something is dropped in.
273
+ mkdir(backgrounds, { recursive: true }),
274
+ ]).then(() => {}, (error) => {
275
+ ctx.logger.warn('dsh-custom-theme: preparing directories failed: %s', error.message)
276
+ })
277
+
278
+ ctx.inject(['webServer'], (child) => {
279
+ child.effect(() => child.webServer.register({
280
+ kind: 'prefix',
281
+ path: ROUTE_PATH,
282
+ handler: (req, res) => handleRoute(req, res, { themes, backgrounds, ready }).catch((error) => {
283
+ child.logger.warn('dsh-custom-theme: %s failed: %s', req.url, error.message)
284
+ if (!res.headersSent) send(res, 500, 'text/plain; charset=utf-8', 'theme read failed')
285
+ }),
286
+ }), 'dsh-custom-theme: route')
287
+ child.logger.info('dsh-custom-theme: serving %s', ROUTE_PATH)
288
+ })
289
+ }
package/src/themes.mjs ADDED
@@ -0,0 +1,121 @@
1
+ // Pure theme-directory model: id validation, picker ordering, directory
2
+ // resolution. No filesystem and no Cordis, so `node --test` drives it directly.
3
+
4
+ import { join } from 'node:path'
5
+
6
+ /** Distinct bundle themes, in picker order. Mirrors the ids of `themes/*.css`. */
7
+ export const BUNDLED_THEME_IDS = ['gov', 'monokai-pro', 'one-dark']
8
+
9
+ /**
10
+ * Seed generation.
11
+ *
12
+ * Bumping it re-syncs the bundled stylesheets on the next start, because a
13
+ * corrected or extended bundled palette has to reach a machine that already has
14
+ * the older copy. Files whose id is not bundled are never touched, and
15
+ * `themes/*.css` is documented as managed so a customised palette belongs in a
16
+ * copy under its own name.
17
+ */
18
+ export const SEED_VERSION = 2
19
+
20
+ /** Refuse to serve or read a stylesheet larger than this; a theme is a few KiB. */
21
+ export const MAX_THEME_BYTES = 1024 * 1024
22
+
23
+ /**
24
+ * Theme ids that may reach the filesystem. Must start alphanumeric, so a name
25
+ * can never be `.`, `..`, or a dotfile, and contains no separator.
26
+ */
27
+ const THEME_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u
28
+
29
+ /**
30
+ * Whether a request-supplied id may be turned into a path.
31
+ * @param value - Candidate id from a request path or a directory entry.
32
+ * @returns True when the id is safe to join under the theme directory.
33
+ */
34
+ export function isThemeId(value) {
35
+ return typeof value === 'string' && THEME_ID.test(value)
36
+ }
37
+
38
+ /**
39
+ * Resolve the theme directory: an explicit `themesDir` config value wins, then
40
+ * `<home>/themes`.
41
+ * @param config - Row configuration from the profile patch; every field is optional.
42
+ * @param home - The DSH home directory, resolved by the caller.
43
+ * @returns Theme directory path.
44
+ */
45
+ export function themesDirectory(config, home) {
46
+ if (typeof config?.themesDir === 'string' && config.themesDir.trim() !== '') return config.themesDir
47
+ return join(home, 'themes')
48
+ }
49
+
50
+ /**
51
+ * Order the ids found in the theme directory: bundled ids first in their fixed
52
+ * order, then every other id in alphabet order.
53
+ * @param ids - Ids present in the directory, in any order and possibly with duplicates.
54
+ * @returns Ordered, de-duplicated ids.
55
+ */
56
+ export function orderThemeIds(ids) {
57
+ const unique = [...new Set(ids)].filter(isThemeId)
58
+ const bundled = BUNDLED_THEME_IDS.filter((id) => unique.includes(id))
59
+ const extra = unique
60
+ .filter((id) => !BUNDLED_THEME_IDS.includes(id))
61
+ .sort((left, right) => left.localeCompare(right))
62
+ return [...bundled, ...extra]
63
+ }
64
+
65
+ /**
66
+ * Whether an id is one of the themes this package seeds.
67
+ * @param id - Candidate id.
68
+ * @returns True for a bundled id.
69
+ */
70
+ export function isBundledThemeId(id) {
71
+ return BUNDLED_THEME_IDS.includes(id)
72
+ }
73
+
74
+ /** Background image extensions this plugin serves, with their media types. */
75
+ const BACKGROUND_TYPES = new Map([
76
+ ['.png', 'image/png'],
77
+ ['.jpg', 'image/jpeg'],
78
+ ['.jpeg', 'image/jpeg'],
79
+ ['.webp', 'image/webp'],
80
+ ['.gif', 'image/gif'],
81
+ ['.avif', 'image/avif'],
82
+ ['.bmp', 'image/bmp'],
83
+ ])
84
+
85
+ /** Refuse to serve an image larger than this. */
86
+ export const MAX_BACKGROUND_BYTES = 16 * 1024 * 1024
87
+
88
+ /**
89
+ * Whether a request-supplied name may be turned into a path under the
90
+ * background directory: a valid id plus one served image extension.
91
+ * @param value - Candidate file name from a request path or a directory entry.
92
+ * @returns True when the name is safe to join under the background directory.
93
+ */
94
+ export function isBackgroundName(value) {
95
+ if (typeof value !== 'string') return false
96
+ const dot = value.lastIndexOf('.')
97
+ if (dot <= 0) return false
98
+ return isThemeId(value.slice(0, dot)) && BACKGROUND_TYPES.has(value.slice(dot).toLowerCase())
99
+ }
100
+
101
+ /**
102
+ * Media type for a background file name.
103
+ * @param name - File name already accepted by {@link isBackgroundName}.
104
+ * @returns The media type; `application/octet-stream` when the extension is unknown.
105
+ */
106
+ export function backgroundContentType(name) {
107
+ return BACKGROUND_TYPES.get(name.slice(name.lastIndexOf('.')).toLowerCase()) ?? 'application/octet-stream'
108
+ }
109
+
110
+ /**
111
+ * Resolve the background directory: an explicit `backgroundsDir` config value
112
+ * wins, then `<home>/backgrounds`. Nothing is seeded here; the directory only
113
+ * holds images the user put there.
114
+ * @param config - Row configuration from the profile patch; every field is optional.
115
+ * @param home - The DSH home directory, resolved by the caller.
116
+ * @returns Background directory path.
117
+ */
118
+ export function backgroundsDirectory(config, home) {
119
+ if (typeof config?.backgroundsDir === 'string' && config.backgroundsDir.trim() !== '') return config.backgroundsDir
120
+ return join(home, 'backgrounds')
121
+ }
package/themes/gov.css ADDED
@@ -0,0 +1,53 @@
1
+ /*
2
+ * 政务主题 — 宣纸底色、中国红品牌色、国徽金强调色。
3
+ *
4
+ * Ported from Deeptop's `themes/gov.css`, which defines both palettes in one file
5
+ * exactly as this one does: `:root` is the light set and `:root[data-theme="dark"]`
6
+ * the dark set. The plugin reads each set separately and hands them to the
7
+ * official theme runtime, so the appearance switch moves between them.
8
+ *
9
+ * Deeptop's own variable names map onto the official `--dsw-*` alias tokens:
10
+ * --surface → bg-base, --surface-raised → bg-layer-1, --surface-soft → bg-layer-2,
11
+ * --line → border-l1, --line-strong → border-l2, --accent → brand-primary,
12
+ * --ink → label-primary, --ink-muted → label-secondary, --danger/--good/--warning
13
+ * → the state colours, --rail → the sidebar fill. `color-scheme` is deliberately
14
+ * not carried over: the shell and the appearance preference own it.
15
+ */
16
+
17
+ :root {
18
+ --dsw-alias-bg-base: #f4eee3;
19
+ --dsw-alias-bg-layer-1: #fbf6ec;
20
+ --dsw-alias-bg-layer-2: #eae2d2;
21
+ --dsw-alias-bg-overlay: #fbf6ec;
22
+ --dsw-alias-border-l1: #d6cdb8;
23
+ --dsw-alias-border-l2: #b8ac92;
24
+ --dsw-alias-brand-primary: #c8161d;
25
+ --dsw-alias-label-primary: #1a1714;
26
+ --dsw-alias-label-secondary: #5a544a;
27
+ --dsw-alias-state-error-primary: #a02a2a;
28
+ --dsw-alias-state-idle-primary: #8a8270;
29
+ --dsw-alias-state-success-primary: #5a7d3d;
30
+ --dsw-alias-state-warn-primary: #b6812a;
31
+ --dsw-specific-sidebar-fill: #e6ddc9;
32
+ }
33
+
34
+ :root[data-theme="dark"] {
35
+ --dsw-alias-bg-base: #1f1a14;
36
+ --dsw-alias-bg-layer-1: #2a231a;
37
+ --dsw-alias-bg-layer-2: #3b3324;
38
+ --dsw-alias-bg-overlay: #2a231a;
39
+ --dsw-alias-border-l1: #3b3324;
40
+ --dsw-alias-border-l2: #54483a;
41
+ --dsw-alias-brand-primary: #e64c50;
42
+ --dsw-alias-label-primary: #f0e6d2;
43
+ --dsw-alias-label-secondary: #a89a82;
44
+ --dsw-alias-state-error-primary: #e64c50;
45
+ --dsw-alias-state-idle-primary: #7a6f5c;
46
+ --dsw-alias-state-success-primary: #82ac5c;
47
+ --dsw-alias-state-warn-primary: #d4a24c;
48
+ --dsw-specific-sidebar-fill: #1f1a14;
49
+ }
50
+
51
+ body {
52
+ font-family: "Source Han Serif SC", "Noto Serif SC", "Songti SC", SimSun, serif;
53
+ }
@@ -0,0 +1,39 @@
1
+ /*
2
+ * Monokai Pro — both palettes in one file, ported from Deeptop's
3
+ * `themes/monokai-pro.css`. Mapping onto the official `--dsw-*` alias tokens is
4
+ * documented in `gov.css`.
5
+ */
6
+
7
+ :root {
8
+ --dsw-alias-bg-base: #fbfaf9;
9
+ --dsw-alias-bg-layer-1: #ffffff;
10
+ --dsw-alias-bg-layer-2: #f1efed;
11
+ --dsw-alias-bg-overlay: #ffffff;
12
+ --dsw-alias-border-l1: #ddd9d7;
13
+ --dsw-alias-border-l2: #cbc6c4;
14
+ --dsw-alias-brand-primary: #2d9bb3;
15
+ --dsw-alias-label-primary: #2d2a2e;
16
+ --dsw-alias-label-secondary: #5e5a60;
17
+ --dsw-alias-state-error-primary: #d6455f;
18
+ --dsw-alias-state-idle-primary: #8a868b;
19
+ --dsw-alias-state-success-primary: #3f9d5c;
20
+ --dsw-alias-state-warn-primary: #b37a1e;
21
+ --dsw-specific-sidebar-fill: #efedeb;
22
+ }
23
+
24
+ :root[data-theme="dark"] {
25
+ --dsw-alias-bg-base: #2d2a2e;
26
+ --dsw-alias-bg-layer-1: #373438;
27
+ --dsw-alias-bg-layer-2: #3f3c41;
28
+ --dsw-alias-bg-overlay: #373438;
29
+ --dsw-alias-border-l1: #3a373b;
30
+ --dsw-alias-border-l2: #4d4a4f;
31
+ --dsw-alias-brand-primary: #78dce8;
32
+ --dsw-alias-label-primary: #fcfcfa;
33
+ --dsw-alias-label-secondary: #c5c2c7;
34
+ --dsw-alias-state-error-primary: #ff6188;
35
+ --dsw-alias-state-idle-primary: #9a979d;
36
+ --dsw-alias-state-success-primary: #a9dc76;
37
+ --dsw-alias-state-warn-primary: #ffd866;
38
+ --dsw-specific-sidebar-fill: #262326;
39
+ }
@@ -0,0 +1,39 @@
1
+ /*
2
+ * One Dark — both palettes in one file, ported from Deeptop's
3
+ * `themes/one-dark.css`. Mapping onto the official `--dsw-*` alias tokens is
4
+ * documented in `gov.css`.
5
+ */
6
+
7
+ :root {
8
+ --dsw-alias-bg-base: #f6f8fb;
9
+ --dsw-alias-bg-layer-1: #ffffff;
10
+ --dsw-alias-bg-layer-2: #eef2f6;
11
+ --dsw-alias-bg-overlay: #ffffff;
12
+ --dsw-alias-border-l1: #d9e0e8;
13
+ --dsw-alias-border-l2: #c3cedb;
14
+ --dsw-alias-brand-primary: #4078c0;
15
+ --dsw-alias-label-primary: #282c34;
16
+ --dsw-alias-label-secondary: #5f6672;
17
+ --dsw-alias-state-error-primary: #d73a49;
18
+ --dsw-alias-state-idle-primary: #8b93a1;
19
+ --dsw-alias-state-success-primary: #22863a;
20
+ --dsw-alias-state-warn-primary: #b08800;
21
+ --dsw-specific-sidebar-fill: #edf1f6;
22
+ }
23
+
24
+ :root[data-theme="dark"] {
25
+ --dsw-alias-bg-base: #282c34;
26
+ --dsw-alias-bg-layer-1: #2c313a;
27
+ --dsw-alias-bg-layer-2: #323842;
28
+ --dsw-alias-bg-overlay: #2c313a;
29
+ --dsw-alias-border-l1: #3a404c;
30
+ --dsw-alias-border-l2: #4b5263;
31
+ --dsw-alias-brand-primary: #61afef;
32
+ --dsw-alias-label-primary: #abb2bf;
33
+ --dsw-alias-label-secondary: #9da5b4;
34
+ --dsw-alias-state-error-primary: #e06c75;
35
+ --dsw-alias-state-idle-primary: #5c6370;
36
+ --dsw-alias-state-success-primary: #98c379;
37
+ --dsw-alias-state-warn-primary: #e5c07b;
38
+ --dsw-specific-sidebar-fill: #21252b;
39
+ }