pluidr 0.7.1 → 0.7.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pluidr",
3
- "version": "0.7.1",
3
+ "version": "0.7.2",
4
4
  "description": "Opinionated Engineering Workflow for OpenCode — Explore, Plan, Build, Debug, and Security-Audit.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -1,11 +1,18 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs"
2
2
  import { join } from "node:path"
3
- import { getConfigDir, getConfigPath, getPromptsDir } from "../../core/paths.js"
3
+ import { getConfigDir, getConfigPath, getPromptsDir, getThemesDir } from "../../core/paths.js"
4
4
  import { findSqueezePath } from "../../core/squeezeInstaller.js"
5
5
 
6
6
  const EXPECTED_PROMPT_COUNT = 18
7
7
  const PLUGIN_FILES = ["pluidr-flow.js", "pluidr-squeeze.js"]
8
8
 
9
+ export function cleanJsonc(content) {
10
+ return content.replace(
11
+ /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)|(,\s*(?=[\]}]))/g,
12
+ (m, g1, g2) => g1 ? "" : g2 ? "" : m
13
+ )
14
+ }
15
+
9
16
  export function collectChecks() {
10
17
  const configDir = getConfigDir()
11
18
  const configPath = getConfigPath()
@@ -13,6 +20,7 @@ export function collectChecks() {
13
20
  const pluginsDir = join(configDir, "plugins")
14
21
  const packageJsonPath = join(configDir, "package.json")
15
22
  const binDir = join(configDir, "bin")
23
+ const themesDir = getThemesDir()
16
24
 
17
25
  const checks = []
18
26
 
@@ -39,6 +47,10 @@ export function collectChecks() {
39
47
  }
40
48
  checks.push({ component: "plugin files", pass: pluginsFound === PLUGIN_FILES.length, detail: `${pluginsFound}/${PLUGIN_FILES.length}` })
41
49
 
50
+ // 3b. Custom theme exists
51
+ const themeExists = existsSync(join(themesDir, "pluidr-contrast.json"))
52
+ checks.push({ component: "pluidr-contrast theme file", pass: themeExists })
53
+
42
54
  // 4. package.json with @opencode-ai/plugin dependency
43
55
  let pkgValid = false
44
56
  if (existsSync(packageJsonPath)) {
@@ -67,7 +79,7 @@ export function collectChecks() {
67
79
  if (configExists) {
68
80
  try {
69
81
  const content = readFileSync(configPath, "utf-8")
70
- const cleaned = content.replace(/\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m)
82
+ const cleaned = cleanJsonc(content)
71
83
  JSON.parse(cleaned)
72
84
  configValid = true
73
85
  } catch {
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs"
1
+ import { readFileSync, writeFileSync } from "node:fs"
2
2
  import { resolve, dirname } from "node:path"
3
3
  import { fileURLToPath } from "node:url"
4
4
  import { confirm, isCancel } from "@clack/prompts"
@@ -8,8 +8,10 @@ import { buildConfig } from "../../core/configBuilder.js"
8
8
  import { writeConfig } from "../../core/configWriter.js"
9
9
  import { writeAgentPrompts } from "../../core/agentPromptWriter.js"
10
10
  import { writePluginBundle, writePluginPackageJson } from "../../core/pluginWriter.js"
11
+ import { writeThemes } from "../../core/themeWriter.js"
12
+ import { writeTuiConfig } from "../../core/tuiConfigWriter.js"
11
13
  import { installSqueeze } from "../../core/squeezeInstaller.js"
12
- import { getConfigPath } from "../../core/paths.js"
14
+ import { getConfigPath, getConfigDir } from "../../core/paths.js"
13
15
 
14
16
  const __dirname = dirname(fileURLToPath(import.meta.url))
15
17
  const TEMPLATES_DIR = resolve(__dirname, "../../templates")
@@ -32,6 +34,8 @@ export async function runInit() {
32
34
  const configObject = buildConfig(tierChoices, TEMPLATES_DIR)
33
35
  writeConfig(configObject)
34
36
  writeAgentPrompts(TEMPLATES_DIR)
37
+ writeThemes(TEMPLATES_DIR)
38
+ writeTuiConfig("pluidr-contrast")
35
39
  writePluginBundle()
36
40
  writePluginPackageJson()
37
41
  if (installSqueezePlugin) {
@@ -31,7 +31,7 @@ export async function runLaunch() {
31
31
  }
32
32
 
33
33
  // 3. Launch opencode
34
- console.log("Launching opencode...")
34
+ console.log("Launching pluidr...")
35
35
  const child = process.platform === "win32"
36
36
  ? spawn("opencode", { stdio: "inherit", shell: true })
37
37
  : spawn("opencode", [], { stdio: "inherit" })
@@ -16,6 +16,20 @@ function findLatestBackup() {
16
16
  return backups.length > 0 ? join(dir, backups[0]) : null
17
17
  }
18
18
 
19
+ function findLatestTuiBackup() {
20
+ const dir = getConfigDir()
21
+ let backups = []
22
+ try {
23
+ backups = readdirSync(dir)
24
+ .filter((f) => f.startsWith("tui.json.bak."))
25
+ .sort()
26
+ .reverse()
27
+ } catch {
28
+ return null
29
+ }
30
+ return backups.length > 0 ? join(dir, backups[0]) : null
31
+ }
32
+
19
33
  export async function runUninstall() {
20
34
  const configDir = getConfigDir()
21
35
  const configPath = getConfigPath()
@@ -32,6 +46,14 @@ export async function runUninstall() {
32
46
  summary.restored = latestBackup
33
47
  }
34
48
 
49
+ const latestTuiBackup = findLatestTuiBackup()
50
+ const tuiConfigPath = join(configDir, "tui.json")
51
+ if (latestTuiBackup) {
52
+ copyFileSync(latestTuiBackup, tuiConfigPath)
53
+ }
54
+
55
+ const themesDir = join(configDir, "themes")
56
+
35
57
  // Remove Pluidr-installed artifacts
36
58
  if (existsSync(promptsDir)) {
37
59
  rmSync(promptsDir, { recursive: true, force: true })
@@ -45,6 +67,11 @@ export async function runUninstall() {
45
67
  rmSync(binDir, { recursive: true, force: true })
46
68
  summary.removed.push("bin/")
47
69
  }
70
+ const customThemePath = join(themesDir, "pluidr-contrast.json")
71
+ if (existsSync(customThemePath)) {
72
+ rmSync(customThemePath, { force: true })
73
+ summary.removed.push("themes/pluidr-contrast.json")
74
+ }
48
75
 
49
76
  // Print summary
50
77
  console.log("Uninstall complete:")
@@ -1,6 +1,6 @@
1
1
  import { existsSync, copyFileSync, readdirSync, unlinkSync } from "node:fs"
2
2
  import { join, dirname } from "node:path"
3
- import { getConfigPath } from "./paths.js"
3
+ import { getConfigPath, getTuiConfigPath } from "./paths.js"
4
4
 
5
5
  const MAX_BACKUPS = 5
6
6
 
@@ -32,9 +32,15 @@ function rotateBackups(configPath) {
32
32
 
33
33
  export function backupExistingConfig(configPath) {
34
34
  const resolvedPath = configPath || getConfigPath()
35
- if (!existsSync(resolvedPath)) return
35
+ if (existsSync(resolvedPath)) {
36
+ rotateBackups(resolvedPath)
37
+ const backupPath = `${resolvedPath}.bak.${timestamp()}`
38
+ copyFileSync(resolvedPath, backupPath)
39
+ }
36
40
 
37
- rotateBackups(resolvedPath)
38
- const backupPath = `${resolvedPath}.bak.${timestamp()}`
39
- copyFileSync(resolvedPath, backupPath)
41
+ const tuiPath = getTuiConfigPath()
42
+ if (existsSync(tuiPath)) {
43
+ const backupTuiPath = `${tuiPath}.bak.${timestamp()}`
44
+ copyFileSync(tuiPath, backupTuiPath)
45
+ }
40
46
  }
package/src/core/paths.js CHANGED
@@ -5,5 +5,7 @@ const BASE = join(homedir(), ".config", "opencode")
5
5
 
6
6
  export const getConfigDir = () => BASE
7
7
  export const getConfigPath = () => join(BASE, "opencode.jsonc")
8
+ export const getTuiConfigPath = () => join(BASE, "tui.json")
8
9
  export const getPromptsDir = () => join(BASE, "prompts")
10
+ export const getThemesDir = () => join(BASE, "themes")
9
11
  // ponytail: getBackupPath removed (YAGNI)
@@ -2,7 +2,7 @@ import { describe, it } from "node:test"
2
2
  import assert from "node:assert"
3
3
  import { homedir } from "node:os"
4
4
  import { join } from "node:path"
5
- import { getConfigDir, getConfigPath, getPromptsDir } from "./paths.js"
5
+ import { getConfigDir, getConfigPath, getTuiConfigPath, getPromptsDir, getThemesDir } from "./paths.js"
6
6
 
7
7
  const BASE = join(homedir(), ".config", "opencode")
8
8
 
@@ -15,7 +15,15 @@ describe("paths", () => {
15
15
  assert.strictEqual(getConfigPath(), join(BASE, "opencode.jsonc"))
16
16
  })
17
17
 
18
+ it("getTuiConfigPath returns tui.json path", () => {
19
+ assert.strictEqual(getTuiConfigPath(), join(BASE, "tui.json"))
20
+ })
21
+
18
22
  it("getPromptsDir returns prompts path", () => {
19
23
  assert.strictEqual(getPromptsDir(), join(BASE, "prompts"))
20
24
  })
25
+
26
+ it("getThemesDir returns themes path", () => {
27
+ assert.strictEqual(getThemesDir(), join(BASE, "themes"))
28
+ })
21
29
  })
@@ -26,7 +26,7 @@ export function platformAsset() {
26
26
  }
27
27
 
28
28
  export function validatePath(path) {
29
- if (!/^[a-zA-Z0-9_\-\.\:\/\\]+$/.test(path)) {
29
+ if (!/^[a-zA-Z0-9_\-\.\:\/\\ ]+$/.test(path)) {
30
30
  throw new Error(`Invalid path: "${path}" contains unsafe characters`)
31
31
  }
32
32
  }
@@ -53,10 +53,7 @@ function extract(archivePath, destDir) {
53
53
  execFileSync("powershell", [
54
54
  "-NoProfile",
55
55
  "-Command",
56
- "Expand-Archive",
57
- "-LiteralPath", archivePath,
58
- "-DestinationPath", destDir,
59
- "-Force"
56
+ `Expand-Archive -LiteralPath '${archivePath}' -DestinationPath '${destDir}' -Force`
60
57
  ], { stdio: "ignore" })
61
58
  } else {
62
59
  execFileSync("tar", ["-xzf", archivePath, "-C", destDir], { stdio: "ignore" })
@@ -41,6 +41,10 @@ describe("validatePath", () => {
41
41
  assert.doesNotThrow(() => validatePath("C:\\Users\\test"))
42
42
  })
43
43
 
44
+ it("accepts paths with spaces", () => {
45
+ assert.doesNotThrow(() => validatePath("C:\\Users\\John Doe\\.config"))
46
+ })
47
+
44
48
  it("accepts paths with dots and dashes", () => {
45
49
  assert.doesNotThrow(() => validatePath("./dir/file-name.ext"))
46
50
  })
@@ -0,0 +1,12 @@
1
+ import { mkdirSync, copyFileSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+ import { getThemesDir } from "./paths.js"
4
+
5
+ export function writeThemes(templatesDir, destDir) {
6
+ const sourcePath = resolve(templatesDir, "themes", "pluidr-contrast.json")
7
+ const targetDir = destDir || getThemesDir()
8
+ const destPath = resolve(targetDir, "pluidr-contrast.json")
9
+
10
+ mkdirSync(targetDir, { recursive: true })
11
+ copyFileSync(sourcePath, destPath)
12
+ }
@@ -0,0 +1,29 @@
1
+ import { describe, it, after } from "node:test"
2
+ import assert from "node:assert"
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync, mkdirSync, writeFileSync } from "node:fs"
4
+ import { join } from "node:path"
5
+ import { tmpdir } from "node:os"
6
+ import { writeThemes } from "./themeWriter.js"
7
+
8
+ describe("themeWriter", () => {
9
+ const destDir = mkdtempSync(join(tmpdir(), "pluidr-theme-dest-"))
10
+
11
+ after(() => {
12
+ if (existsSync(destDir)) rmSync(destDir, { recursive: true })
13
+ })
14
+
15
+ it("copies theme file to destination directory", () => {
16
+ const tmpSrcDir = mkdtempSync(join(tmpdir(), "pluidr-theme-src-"))
17
+ const srcThemesDir = join(tmpSrcDir, "themes")
18
+ mkdirSync(srcThemesDir, { recursive: true })
19
+ writeFileSync(join(srcThemesDir, "pluidr-contrast.json"), "mock-theme-content", "utf-8")
20
+
21
+ writeThemes(tmpSrcDir, destDir)
22
+
23
+ const destPath = join(destDir, "pluidr-contrast.json")
24
+ assert.ok(existsSync(destPath), "theme file should be copied")
25
+ assert.strictEqual(readFileSync(destPath, "utf-8"), "mock-theme-content")
26
+
27
+ rmSync(tmpSrcDir, { recursive: true })
28
+ })
29
+ })
@@ -0,0 +1,22 @@
1
+ import { readFileSync, writeFileSync, existsSync } from "node:fs"
2
+ import { getTuiConfigPath } from "./paths.js"
3
+
4
+ export function writeTuiConfig(themeName, customPath) {
5
+ const path = customPath || getTuiConfigPath()
6
+ let config = {
7
+ "$schema": "https://opencode.ai/tui.json"
8
+ }
9
+
10
+ if (existsSync(path)) {
11
+ try {
12
+ const content = readFileSync(path, "utf-8")
13
+ config = JSON.parse(content)
14
+ } catch {
15
+ // If parsing fails, fall back to default structure
16
+ }
17
+ }
18
+
19
+ config.theme = themeName
20
+
21
+ writeFileSync(path, JSON.stringify(config, null, 2), "utf-8")
22
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, it, after } from "node:test"
2
+ import assert from "node:assert"
3
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
4
+ import { join } from "node:path"
5
+ import { tmpdir } from "node:os"
6
+ import { writeTuiConfig } from "./tuiConfigWriter.js"
7
+
8
+ describe("tuiConfigWriter", () => {
9
+ const destDir = mkdtempSync(join(tmpdir(), "pluidr-tui-dest-"))
10
+ const tuiPath = join(destDir, "tui.json")
11
+
12
+ after(() => {
13
+ if (existsSync(destDir)) rmSync(destDir, { recursive: true })
14
+ })
15
+
16
+ it("writes new tui.json with theme name if it does not exist", () => {
17
+ writeTuiConfig("pluidr-contrast", tuiPath)
18
+
19
+ assert.ok(existsSync(tuiPath))
20
+ const parsed = JSON.parse(readFileSync(tuiPath, "utf-8"))
21
+ assert.strictEqual(parsed.theme, "pluidr-contrast")
22
+ assert.strictEqual(parsed["$schema"], "https://opencode.ai/tui.json")
23
+ })
24
+
25
+ it("updates existing tui.json theme name while preserving other fields", () => {
26
+ writeFileSync(tuiPath, JSON.stringify({
27
+ "$schema": "https://opencode.ai/tui.json",
28
+ "theme": "ayu-dark",
29
+ "font": "monolisa"
30
+ }, null, 2), "utf-8")
31
+
32
+ writeTuiConfig("pluidr-contrast", tuiPath)
33
+
34
+ const parsed = JSON.parse(readFileSync(tuiPath, "utf-8"))
35
+ assert.strictEqual(parsed.theme, "pluidr-contrast")
36
+ assert.strictEqual(parsed.font, "monolisa")
37
+ })
38
+ })
@@ -394,8 +394,7 @@ elsewhere. Do not accept a report that is not in `docs/reports/`.
394
394
  - **Regression Awareness** — When re-triggering coder after a FAIL, pass the
395
395
  gap list in full so coder doesn't fix one thing and break something
396
396
  already confirmed as PASS.
397
- - **Clarification** — If anything is ambiguous at any phase, ask the user
398
- using multiple-choice options (2-4 short choices per question).
397
+ - **Clarification** — If anything is ambiguous at any phase, avoid excessive questioning. For minor details, make a reasonable, safe engineering assumption based on existing codebase conventions and document it. Only prompt the user using multiple-choice options (2-3 short choices per question, and list your recommended option first prefixed with '(Recommended)') if there is a critical blocking issue that directly impacts the design direction or has high regression risk.
399
398
  - **Front-End / UI Mockups** — If the user asks for a design, mockup, or anything related to the Front-End (FE), you must provide a detailed ASCII mockup of the UI structure in your output.
400
399
 
401
400
  ---
@@ -132,6 +132,7 @@ just a technical restriction.
132
132
  - **Separation of Investigation/Fix/Report** — Do not mix investigation,
133
133
  implementation, and reporting in the same delegate call. Each subagent
134
134
  has one job; the Debugger orchestrates the sequence.
135
+ - **Avoid Excessive Questioning** — Avoid excessive querying of the user for minor ambiguities. For small implementation details, make a reasonable, safe engineering assumption based on existing codebase conventions and proceed. Only prompt the user using multiple-choice options (2-3 short choices per question, and list your recommended option first prefixed with '(Recommended)') when a critical blocking issue directly prevents a safe diagnosis or fix.
135
136
 
136
137
  ## What you do NOT do
137
138
 
@@ -85,6 +85,10 @@ Do NOT remain in a completed state without offering next steps.
85
85
  - You do not patch without a GUARDRAIL GATE user confirmation after TRACE.
86
86
  - You do not loop more than 5 times on AUDIT before surfacing to the user.
87
87
 
88
+ ## Principles you apply
89
+
90
+ - **Avoid Excessive Questioning** — Avoid excessive querying of the user for minor ambiguities. For small implementation details, make a reasonable, safe engineering assumption based on existing codebase conventions and proceed. Only prompt the user using the `question` tool with multiple-choice options (2-3 short choices per question, and list your recommended option first prefixed with '(Recommended)') for critical blocking decisions (such as the Guardrail Gate or a complete audit block).
91
+
88
92
  ## Conflict Resolution
89
93
  Refer to `hierarchy.txt` (loaded globally) — you do not resolve principle
90
94
  conflicts by your own judgment outside that hierarchy.
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "$schema": "https://opencode.ai/config.json",
3
+ "theme": "pluidr-contrast",
3
4
  "instructions": ["{file:./prompts/hierarchy.txt}"],
4
5
  "permission": {
5
6
  "read": {
@@ -0,0 +1,177 @@
1
+ {
2
+ "$schema": "https://opencode.ai/theme.json",
3
+ "theme": {
4
+ "primary": {
5
+ "dark": "#ff7039",
6
+ "light": "#ff7039"
7
+ },
8
+ "secondary": {
9
+ "dark": "#99f3ff",
10
+ "light": "#99f3ff"
11
+ },
12
+ "accent": {
13
+ "dark": "#ff7039",
14
+ "light": "#ff7039"
15
+ },
16
+ "error": {
17
+ "dark": "#fc5d7c",
18
+ "light": "#fc5d7c"
19
+ },
20
+ "warning": {
21
+ "dark": "#ff669c",
22
+ "light": "#ff669c"
23
+ },
24
+ "success": {
25
+ "dark": "#00cc76",
26
+ "light": "#00cc76"
27
+ },
28
+ "info": {
29
+ "dark": "#00b4cc",
30
+ "light": "#00b4cc"
31
+ },
32
+ "text": {
33
+ "dark": "#ffffff",
34
+ "light": "#ffffff"
35
+ },
36
+ "textMuted": {
37
+ "dark": "#888888",
38
+ "light": "#888888"
39
+ },
40
+ "background": {
41
+ "dark": "#000000",
42
+ "light": "#000000"
43
+ },
44
+ "backgroundPanel": {
45
+ "dark": "#000000",
46
+ "light": "#000000"
47
+ },
48
+ "backgroundElement": {
49
+ "dark": "#000000",
50
+ "light": "#000000"
51
+ },
52
+ "border": {
53
+ "dark": "#101520",
54
+ "light": "#101520"
55
+ },
56
+ "borderActive": {
57
+ "dark": "#ff7039",
58
+ "light": "#ff7039"
59
+ },
60
+ "borderSubtle": {
61
+ "dark": "#101520",
62
+ "light": "#101520"
63
+ },
64
+ "diffAdded": {
65
+ "dark": "#00cc76",
66
+ "light": "#00cc76"
67
+ },
68
+ "diffRemoved": {
69
+ "dark": "#fc5d7c",
70
+ "light": "#fc5d7c"
71
+ },
72
+ "diffContext": {
73
+ "dark": "#888888",
74
+ "light": "#888888"
75
+ },
76
+ "diffAddedBg": {
77
+ "dark": "#1a2618",
78
+ "light": "#1a2618"
79
+ },
80
+ "diffRemovedBg": {
81
+ "dark": "#2a1a1a",
82
+ "light": "#2a1a1a"
83
+ },
84
+ "diffContextBg": {
85
+ "dark": "#000000",
86
+ "light": "#000000"
87
+ },
88
+ "markdownText": {
89
+ "dark": "#ffffff",
90
+ "light": "#ffffff"
91
+ },
92
+ "markdownHeading": {
93
+ "dark": "#ff669c",
94
+ "light": "#ff669c"
95
+ },
96
+ "markdownLink": {
97
+ "dark": "#99f3ff",
98
+ "light": "#99f3ff"
99
+ },
100
+ "markdownLinkText": {
101
+ "dark": "#ff7039",
102
+ "light": "#ff7039"
103
+ },
104
+ "markdownCode": {
105
+ "dark": "#66ffbf",
106
+ "light": "#66ffbf"
107
+ },
108
+ "markdownBlockQuote": {
109
+ "dark": "#888888",
110
+ "light": "#888888"
111
+ },
112
+ "markdownStrong": {
113
+ "dark": "#ff7039",
114
+ "light": "#ff7039"
115
+ },
116
+ "syntaxComment": {
117
+ "dark": "#888888",
118
+ "light": "#888888"
119
+ },
120
+ "syntaxKeyword": {
121
+ "dark": "#ff669c",
122
+ "light": "#ff669c"
123
+ },
124
+ "syntaxFunction": {
125
+ "dark": "#00b4cc",
126
+ "light": "#00b4cc"
127
+ },
128
+ "syntaxVariable": {
129
+ "dark": "#ffb966",
130
+ "light": "#ffb966"
131
+ },
132
+ "syntaxString": {
133
+ "dark": "#66ffbf",
134
+ "light": "#66ffbf"
135
+ },
136
+ "syntaxNumber": {
137
+ "dark": "#a000ff",
138
+ "light": "#a000ff"
139
+ },
140
+ "syntaxType": {
141
+ "dark": "#99f3ff",
142
+ "light": "#99f3ff"
143
+ },
144
+ "syntaxOperator": {
145
+ "dark": "#ff669c",
146
+ "light": "#ff669c"
147
+ },
148
+ "syntaxPunctuation": {
149
+ "dark": "#ffffff",
150
+ "light": "#ffffff"
151
+ },
152
+ "inputBackground": {
153
+ "dark": "#000000",
154
+ "light": "#000000"
155
+ },
156
+ "InputBorder": {
157
+ "dark": "#101520",
158
+ "light": "#101520"
159
+ },
160
+ "InputBorderActive": {
161
+ "dark": "#ff7039",
162
+ "light": "#ff7039"
163
+ },
164
+ "inputPrompt": {
165
+ "dark": "#ff7039",
166
+ "light": "#ff7039"
167
+ },
168
+ "inputCursor": {
169
+ "dark": "#ff7039",
170
+ "light": "#ff7039"
171
+ },
172
+ "inputText": {
173
+ "dark": "#ffffff",
174
+ "light": "#ffffff"
175
+ }
176
+ }
177
+ }