design-protocol 1.0.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.md +225 -0
- package/agents/dp-researcher.md +239 -0
- package/agents/dp-verifier.md +207 -0
- package/bin/install.js +464 -0
- package/commands/dp-back.md +221 -0
- package/commands/dp-discuss.md +257 -0
- package/commands/dp-execute.md +513 -0
- package/commands/dp-journey.md +85 -0
- package/commands/dp-progress.md +178 -0
- package/commands/dp-roadmap.md +83 -0
- package/commands/dp-skip.md +186 -0
- package/commands/dp-start.md +510 -0
- package/commands/dp-storytell.md +94 -0
- package/commands/dp-verify.md +207 -0
- package/package.json +59 -0
- package/skills/dp-color/SKILL.md +214 -0
- package/skills/dp-color/export_tokens.py +297 -0
- package/skills/dp-color/references/apca-contrast.md +87 -0
- package/skills/dp-color/references/hue-emotions.md +109 -0
- package/skills/dp-color/references/oklch-gamut.md +79 -0
- package/skills/dp-color/references/pitfalls.md +171 -0
- package/skills/dp-color/references/scale-patterns.md +206 -0
- package/skills/dp-color/references/tool-workflows.md +200 -0
- package/skills/dp-discovery/SKILL.md +480 -0
- package/skills/dp-eng_review/SKILL.md +471 -0
- package/skills/dp-eng_review/references/code-review-checklist.md +385 -0
- package/skills/dp-eng_review/references/react-patterns.md +512 -0
- package/skills/dp-eng_review/references/shadcn-patterns.md +510 -0
- package/skills/dp-eng_review/references/tailwind-conventions.md +351 -0
- package/skills/dp-journey/SKILL.md +682 -0
- package/skills/dp-journey/references/journey-types.md +97 -0
- package/skills/dp-journey/references/map-structures.md +177 -0
- package/skills/dp-journey/references/omnichannel-patterns.md +208 -0
- package/skills/dp-journey/references/research-methods.md +125 -0
- package/skills/dp-prd/SKILL.md +201 -0
- package/skills/dp-prd/references/claude-code-spec.md +107 -0
- package/skills/dp-prd/references/interview-questions.md +158 -0
- package/skills/dp-prd/references/section-templates.md +231 -0
- package/skills/dp-research/SKILL.md +540 -0
- package/skills/dp-research/references/facilitation-guide.md +291 -0
- package/skills/dp-research/references/interview-guide-template.md +190 -0
- package/skills/dp-research/references/method-selection.md +195 -0
- package/skills/dp-research/references/question-writing.md +244 -0
- package/skills/dp-research/references/research-report-template.md +363 -0
- package/skills/dp-research/references/synthesis-methods.md +289 -0
- package/skills/dp-research/references/usability-test-template.md +260 -0
- package/skills/dp-roadmap/SKILL.md +648 -0
- package/skills/dp-roadmap/references/prioritization-frameworks.md +312 -0
- package/skills/dp-roadmap/references/roadmap-structures.md +179 -0
- package/skills/dp-roadmap/references/roadmap-workshops.md +264 -0
- package/skills/dp-roadmap/references/theme-development.md +168 -0
- package/skills/dp-storytell/SKILL.md +645 -0
- package/skills/dp-storytell/references/audience-playbooks.md +260 -0
- package/skills/dp-storytell/references/content-type-templates.md +310 -0
- package/skills/dp-storytell/references/delivery-tactics.md +228 -0
- package/skills/dp-storytell/references/narrative-frameworks.md +259 -0
- package/skills/dp-ui/SKILL.md +503 -0
- package/skills/dp-ui/references/b2b-enterprise-patterns.md +319 -0
- package/skills/dp-ui/references/data-visualization.md +304 -0
- package/skills/dp-ui/references/visual-design-principles.md +237 -0
- package/skills/dp-ux/SKILL.md +414 -0
- package/skills/dp-ux/references/accessibility-checklist.md +128 -0
- package/skills/dp-ux/references/product-excellence.md +149 -0
- package/skills/dp-ux/references/usability-principles.md +140 -0
- package/skills/dp-ux/references/ux-patterns.md +221 -0
- package/templates/config.json +55 -0
- package/templates/context.md +96 -0
- package/templates/project.md +83 -0
- package/templates/requirements.md +137 -0
- package/templates/roadmap.md +168 -0
- package/templates/state.md +107 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Export color palette tokens in multiple formats.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python3 export_tokens.py --input palette.json --format css,json,figma --output ./tokens/
|
|
7
|
+
python3 export_tokens.py --input palette.json --format css --p3 # Include P3 enhanced values
|
|
8
|
+
python3 export_tokens.py --input palette.json --format all --dark # Include dark mode remap
|
|
9
|
+
|
|
10
|
+
Input JSON format:
|
|
11
|
+
{
|
|
12
|
+
"colors": {
|
|
13
|
+
"primary": {
|
|
14
|
+
"hue": 260,
|
|
15
|
+
"max_chroma": 0.145,
|
|
16
|
+
"p3_max_chroma": 0.19,
|
|
17
|
+
"steps": {
|
|
18
|
+
"50": { "L": 0.96, "C": 0.029, "H": 256 },
|
|
19
|
+
"100": { "L": 0.93, "C": 0.044, "H": 257 },
|
|
20
|
+
...
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"meta": {
|
|
25
|
+
"name": "Enterprise Color System",
|
|
26
|
+
"base": "#1c2739",
|
|
27
|
+
"wcag_target": "AA"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
If "steps" are omitted, the script auto-generates an 11-step ramp from hue + max_chroma
|
|
32
|
+
using the eased lightness curve.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
import math
|
|
37
|
+
import argparse
|
|
38
|
+
import os
|
|
39
|
+
import sys
|
|
40
|
+
|
|
41
|
+
# ─── OKLCH → sRGB Conversion ───
|
|
42
|
+
|
|
43
|
+
def oklch_to_oklab(L, C, H):
|
|
44
|
+
h_rad = math.radians(H)
|
|
45
|
+
return L, C * math.cos(h_rad), C * math.sin(h_rad)
|
|
46
|
+
|
|
47
|
+
def oklab_to_linear_rgb(L, a, b):
|
|
48
|
+
l_ = L + 0.3963377774 * a + 0.2158037573 * b
|
|
49
|
+
m_ = L - 0.1055613458 * a - 0.0638541728 * b
|
|
50
|
+
s_ = L - 0.0894841775 * a - 1.2914855480 * b
|
|
51
|
+
|
|
52
|
+
l = l_ ** 3
|
|
53
|
+
m = m_ ** 3
|
|
54
|
+
s = s_ ** 3
|
|
55
|
+
|
|
56
|
+
r = +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
|
|
57
|
+
g = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
|
|
58
|
+
bl = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
|
|
59
|
+
return r, g, bl
|
|
60
|
+
|
|
61
|
+
def linear_to_srgb(c):
|
|
62
|
+
if c <= 0.0031308:
|
|
63
|
+
return 12.92 * c
|
|
64
|
+
return 1.055 * (c ** (1 / 2.4)) - 0.055
|
|
65
|
+
|
|
66
|
+
def clamp(v, lo=0.0, hi=1.0):
|
|
67
|
+
return max(lo, min(hi, v))
|
|
68
|
+
|
|
69
|
+
def oklch_to_hex(L, C, H):
|
|
70
|
+
ol, oa, ob = oklch_to_oklab(L, C, H)
|
|
71
|
+
r, g, b = oklab_to_linear_rgb(ol, oa, ob)
|
|
72
|
+
r = clamp(linear_to_srgb(r))
|
|
73
|
+
g = clamp(linear_to_srgb(g))
|
|
74
|
+
b = clamp(linear_to_srgb(b))
|
|
75
|
+
return f"#{int(round(r*255)):02x}{int(round(g*255)):02x}{int(round(b*255)):02x}"
|
|
76
|
+
|
|
77
|
+
def is_in_srgb_gamut(L, C, H):
|
|
78
|
+
"""Check if OKLCH value is within sRGB gamut (no clamping needed)."""
|
|
79
|
+
ol, oa, ob = oklch_to_oklab(L, C, H)
|
|
80
|
+
r, g, b = oklab_to_linear_rgb(ol, oa, ob)
|
|
81
|
+
return all(-0.001 <= v <= 1.001 for v in [r, g, b])
|
|
82
|
+
|
|
83
|
+
# ─── Auto-generate ramp ───
|
|
84
|
+
|
|
85
|
+
STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]
|
|
86
|
+
LIGHTNESS_EASED = [0.96, 0.93, 0.87, 0.79, 0.70, 0.60, 0.50, 0.40, 0.32, 0.25, 0.18]
|
|
87
|
+
CHROMA_BELL = [0.10, 0.18, 0.40, 0.65, 0.85, 1.00, 0.90, 0.75, 0.55, 0.35, 0.25]
|
|
88
|
+
HUE_OFFSETS = [-4, -3, -2, -1, 0, 0, 1, 2, 3, 4, 5]
|
|
89
|
+
|
|
90
|
+
def generate_ramp(hue, max_chroma, hue_shift=1.0):
|
|
91
|
+
ramp = {}
|
|
92
|
+
for i, step in enumerate(STEPS):
|
|
93
|
+
L = LIGHTNESS_EASED[i]
|
|
94
|
+
C = round(max_chroma * CHROMA_BELL[i], 4)
|
|
95
|
+
H = round((hue + HUE_OFFSETS[i] * hue_shift) % 360, 1)
|
|
96
|
+
# Gamut safety: reduce chroma until in-gamut
|
|
97
|
+
while C > 0.001 and not is_in_srgb_gamut(L, C, H):
|
|
98
|
+
C = round(C - 0.001, 4)
|
|
99
|
+
ramp[str(step)] = {"L": L, "C": C, "H": H}
|
|
100
|
+
return ramp
|
|
101
|
+
|
|
102
|
+
def generate_dark_ramp(light_ramp):
|
|
103
|
+
"""Remap a light-mode ramp for dark mode surfaces."""
|
|
104
|
+
dark = {}
|
|
105
|
+
for step_name, vals in light_ramp.items():
|
|
106
|
+
L = vals["L"]
|
|
107
|
+
C = vals["C"]
|
|
108
|
+
H = vals["H"]
|
|
109
|
+
# Invert lightness mapping and reduce chroma ~20%
|
|
110
|
+
dark_L = round(1.0 - L, 3)
|
|
111
|
+
# Clamp to useful range
|
|
112
|
+
dark_L = round(clamp(dark_L, 0.10, 0.95), 3)
|
|
113
|
+
dark_C = round(C * 0.80, 4)
|
|
114
|
+
dark[step_name] = {"L": dark_L, "C": dark_C, "H": H}
|
|
115
|
+
return dark
|
|
116
|
+
|
|
117
|
+
# ─── Output formatters ───
|
|
118
|
+
|
|
119
|
+
def format_css(colors, meta, include_p3=False, include_dark=False):
|
|
120
|
+
lines = [f"/* {meta.get('name', 'Color System')} — CSS Custom Properties */",
|
|
121
|
+
f"/* Base: {meta.get('base', 'N/A')} | WCAG: {meta.get('wcag_target', 'AA')} */", ""]
|
|
122
|
+
lines.append(":root {")
|
|
123
|
+
for color_name, color_data in colors.items():
|
|
124
|
+
steps = color_data.get("steps", {})
|
|
125
|
+
lines.append(f" /* {color_name} */")
|
|
126
|
+
for step, vals in sorted(steps.items(), key=lambda x: int(x[0])):
|
|
127
|
+
oklch = f"oklch({vals['L']:.2f} {vals['C']:.3f} {vals['H']:.0f})"
|
|
128
|
+
hx = oklch_to_hex(vals['L'], vals['C'], vals['H'])
|
|
129
|
+
lines.append(f" --{color_name}-{step}: {oklch}; /* {hx} */")
|
|
130
|
+
lines.append("")
|
|
131
|
+
lines.append("}")
|
|
132
|
+
|
|
133
|
+
if include_p3:
|
|
134
|
+
lines.extend(["", "@media (color-gamut: p3) {", " :root {"])
|
|
135
|
+
for color_name, color_data in colors.items():
|
|
136
|
+
p3_max = color_data.get("p3_max_chroma")
|
|
137
|
+
if not p3_max:
|
|
138
|
+
continue
|
|
139
|
+
base_max = color_data.get("max_chroma", 0.15)
|
|
140
|
+
steps = color_data.get("steps", {})
|
|
141
|
+
lines.append(f" /* {color_name} — P3 enhanced */")
|
|
142
|
+
for step, vals in sorted(steps.items(), key=lambda x: int(x[0])):
|
|
143
|
+
# Scale chroma proportionally to P3 ceiling
|
|
144
|
+
p3_C = round(vals['C'] * (p3_max / base_max), 4)
|
|
145
|
+
if p3_C > vals['C'] + 0.005: # Only include if meaningfully different
|
|
146
|
+
oklch = f"oklch({vals['L']:.2f} {p3_C:.3f} {vals['H']:.0f})"
|
|
147
|
+
lines.append(f" --{color_name}-{step}: {oklch};")
|
|
148
|
+
lines.append("")
|
|
149
|
+
lines.extend([" }", "}"])
|
|
150
|
+
|
|
151
|
+
if include_dark:
|
|
152
|
+
lines.extend(["", "/* Dark mode */",
|
|
153
|
+
"@media (prefers-color-scheme: dark) {", " :root {"])
|
|
154
|
+
for color_name, color_data in colors.items():
|
|
155
|
+
steps = color_data.get("steps", {})
|
|
156
|
+
dark_steps = generate_dark_ramp(steps)
|
|
157
|
+
lines.append(f" /* {color_name} — dark */")
|
|
158
|
+
for step, vals in sorted(dark_steps.items(), key=lambda x: int(x[0])):
|
|
159
|
+
oklch = f"oklch({vals['L']:.2f} {vals['C']:.3f} {vals['H']:.0f})"
|
|
160
|
+
hx = oklch_to_hex(vals['L'], vals['C'], vals['H'])
|
|
161
|
+
lines.append(f" --{color_name}-{step}: {oklch}; /* {hx} */")
|
|
162
|
+
lines.append("")
|
|
163
|
+
lines.extend([" }", "}"])
|
|
164
|
+
|
|
165
|
+
return "\n".join(lines)
|
|
166
|
+
|
|
167
|
+
def format_json_tokens(colors, meta, include_dark=False):
|
|
168
|
+
"""W3C Design Tokens Community Group format."""
|
|
169
|
+
tokens = {
|
|
170
|
+
"$name": meta.get("name", "Color System"),
|
|
171
|
+
"$description": f"Base: {meta.get('base', 'N/A')} | WCAG: {meta.get('wcag_target', 'AA')}",
|
|
172
|
+
"color": {}
|
|
173
|
+
}
|
|
174
|
+
for color_name, color_data in colors.items():
|
|
175
|
+
steps = color_data.get("steps", {})
|
|
176
|
+
token_group = {}
|
|
177
|
+
for step, vals in sorted(steps.items(), key=lambda x: int(x[0])):
|
|
178
|
+
oklch_str = f"oklch({vals['L']:.2f} {vals['C']:.3f} {vals['H']:.0f})"
|
|
179
|
+
hx = oklch_to_hex(vals['L'], vals['C'], vals['H'])
|
|
180
|
+
token_group[step] = {
|
|
181
|
+
"$type": "color",
|
|
182
|
+
"$value": oklch_str,
|
|
183
|
+
"hex": hx,
|
|
184
|
+
"oklch": {"L": vals["L"], "C": vals["C"], "H": vals["H"]}
|
|
185
|
+
}
|
|
186
|
+
tokens["color"][color_name] = token_group
|
|
187
|
+
|
|
188
|
+
if include_dark:
|
|
189
|
+
tokens["color-dark"] = {}
|
|
190
|
+
for color_name, color_data in colors.items():
|
|
191
|
+
steps = color_data.get("steps", {})
|
|
192
|
+
dark_steps = generate_dark_ramp(steps)
|
|
193
|
+
token_group = {}
|
|
194
|
+
for step, vals in sorted(dark_steps.items(), key=lambda x: int(x[0])):
|
|
195
|
+
oklch_str = f"oklch({vals['L']:.2f} {vals['C']:.3f} {vals['H']:.0f})"
|
|
196
|
+
hx = oklch_to_hex(vals['L'], vals['C'], vals['H'])
|
|
197
|
+
token_group[step] = {
|
|
198
|
+
"$type": "color",
|
|
199
|
+
"$value": oklch_str,
|
|
200
|
+
"hex": hx,
|
|
201
|
+
"oklch": {"L": vals["L"], "C": vals["C"], "H": vals["H"]}
|
|
202
|
+
}
|
|
203
|
+
tokens["color-dark"][color_name] = token_group
|
|
204
|
+
|
|
205
|
+
return json.dumps(tokens, indent=2)
|
|
206
|
+
|
|
207
|
+
def format_figma_tokens(colors, meta):
|
|
208
|
+
"""Figma Tokens plugin format (Tokens Studio compatible)."""
|
|
209
|
+
figma = {}
|
|
210
|
+
for color_name, color_data in colors.items():
|
|
211
|
+
steps = color_data.get("steps", {})
|
|
212
|
+
group = {}
|
|
213
|
+
for step, vals in sorted(steps.items(), key=lambda x: int(x[0])):
|
|
214
|
+
hx = oklch_to_hex(vals['L'], vals['C'], vals['H'])
|
|
215
|
+
group[step] = {
|
|
216
|
+
"value": hx,
|
|
217
|
+
"type": "color",
|
|
218
|
+
"description": f"oklch({vals['L']:.2f} {vals['C']:.3f} {vals['H']:.0f})"
|
|
219
|
+
}
|
|
220
|
+
figma[color_name] = group
|
|
221
|
+
return json.dumps(figma, indent=2)
|
|
222
|
+
|
|
223
|
+
# ─── Main ───
|
|
224
|
+
|
|
225
|
+
def main():
|
|
226
|
+
parser = argparse.ArgumentParser(description="Export color palette tokens")
|
|
227
|
+
parser.add_argument("--input", required=True, help="Input palette JSON file")
|
|
228
|
+
parser.add_argument("--format", default="all",
|
|
229
|
+
help="Output formats: css, json, figma, or all (comma-separated)")
|
|
230
|
+
parser.add_argument("--output", default="./tokens", help="Output directory")
|
|
231
|
+
parser.add_argument("--p3", action="store_true", help="Include P3 gamut enhanced values in CSS")
|
|
232
|
+
parser.add_argument("--dark", action="store_true", help="Include dark mode remapped values")
|
|
233
|
+
args = parser.parse_args()
|
|
234
|
+
|
|
235
|
+
with open(args.input, "r") as f:
|
|
236
|
+
palette = json.load(f)
|
|
237
|
+
|
|
238
|
+
colors = palette.get("colors", {})
|
|
239
|
+
meta = palette.get("meta", {})
|
|
240
|
+
|
|
241
|
+
# Auto-generate ramps for colors that only have hue + max_chroma
|
|
242
|
+
for name, data in colors.items():
|
|
243
|
+
if "steps" not in data:
|
|
244
|
+
data["steps"] = generate_ramp(
|
|
245
|
+
data.get("hue", 0),
|
|
246
|
+
data.get("max_chroma", 0.15),
|
|
247
|
+
data.get("hue_shift", 1.0)
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
os.makedirs(args.output, exist_ok=True)
|
|
251
|
+
formats = [f.strip() for f in args.format.split(",")]
|
|
252
|
+
if "all" in formats:
|
|
253
|
+
formats = ["css", "json", "figma"]
|
|
254
|
+
|
|
255
|
+
generated = []
|
|
256
|
+
|
|
257
|
+
if "css" in formats:
|
|
258
|
+
css = format_css(colors, meta, include_p3=args.p3, include_dark=args.dark)
|
|
259
|
+
path = os.path.join(args.output, "tokens.css")
|
|
260
|
+
with open(path, "w") as f:
|
|
261
|
+
f.write(css)
|
|
262
|
+
generated.append(path)
|
|
263
|
+
|
|
264
|
+
if "json" in formats:
|
|
265
|
+
tokens = format_json_tokens(colors, meta, include_dark=args.dark)
|
|
266
|
+
path = os.path.join(args.output, "tokens.json")
|
|
267
|
+
with open(path, "w") as f:
|
|
268
|
+
f.write(tokens)
|
|
269
|
+
generated.append(path)
|
|
270
|
+
|
|
271
|
+
if "figma" in formats:
|
|
272
|
+
figma = format_figma_tokens(colors, meta)
|
|
273
|
+
path = os.path.join(args.output, "figma-tokens.json")
|
|
274
|
+
with open(path, "w") as f:
|
|
275
|
+
f.write(figma)
|
|
276
|
+
generated.append(path)
|
|
277
|
+
|
|
278
|
+
# Gamut check
|
|
279
|
+
warnings = []
|
|
280
|
+
for name, data in colors.items():
|
|
281
|
+
for step, vals in data.get("steps", {}).items():
|
|
282
|
+
if not is_in_srgb_gamut(vals["L"], vals["C"], vals["H"]):
|
|
283
|
+
warnings.append(f" ⚠ {name}-{step}: oklch({vals['L']} {vals['C']} {vals['H']}) is OUT of sRGB gamut")
|
|
284
|
+
|
|
285
|
+
print(f"✅ Generated {len(generated)} token file(s):")
|
|
286
|
+
for p in generated:
|
|
287
|
+
print(f" {p}")
|
|
288
|
+
|
|
289
|
+
if warnings:
|
|
290
|
+
print(f"\n⚠ Gamut warnings ({len(warnings)}):")
|
|
291
|
+
for w in warnings:
|
|
292
|
+
print(w)
|
|
293
|
+
else:
|
|
294
|
+
print("\n✅ All colors are within sRGB gamut.")
|
|
295
|
+
|
|
296
|
+
if __name__ == "__main__":
|
|
297
|
+
main()
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# APCA & WCAG Contrast Reference
|
|
2
|
+
|
|
3
|
+
## APCA (Accessible Perceptual Contrast Algorithm)
|
|
4
|
+
|
|
5
|
+
APCA uses **Lc (Lightness Contrast)** values. Unlike WCAG 2.x ratios, APCA is polarity-aware:
|
|
6
|
+
dark text on light background produces positive Lc; light text on dark background produces
|
|
7
|
+
negative Lc. Use the absolute value for threshold comparisons.
|
|
8
|
+
|
|
9
|
+
### APCA Minimum Lc Thresholds by Use Case
|
|
10
|
+
|
|
11
|
+
| Use Case | Min |Lc| | Font Size Context |
|
|
12
|
+
|----------|---------|-------------------|
|
|
13
|
+
| Body text (primary content) | 75 | 16px+ regular, 14px+ bold |
|
|
14
|
+
| Large text / headings | 60 | 24px+ regular, 18.66px+ bold |
|
|
15
|
+
| Sub-text, captions, labels | 60 | 14px+ |
|
|
16
|
+
| Placeholder text | 60 | Minimum for legibility |
|
|
17
|
+
| Non-text UI elements (icons, borders) | 45 | Meaningful boundaries |
|
|
18
|
+
| Decorative / disabled states | 30 | Minimum perceptible |
|
|
19
|
+
| Large non-text (logos, hero graphics) | 45 | Large-scale elements |
|
|
20
|
+
|
|
21
|
+
### APCA Bronze/Silver/Gold Conformance
|
|
22
|
+
|
|
23
|
+
| Level | Body Text |Lc| | Large Text |Lc| | Non-text |Lc| |
|
|
24
|
+
|-------|-----------|------------|------------|
|
|
25
|
+
| Bronze (minimum) | 60 | 45 | 30 |
|
|
26
|
+
| Silver (recommended) | 75 | 60 | 45 |
|
|
27
|
+
| Gold (enhanced) | 90 | 75 | 60 |
|
|
28
|
+
|
|
29
|
+
### Computing APCA Lc from OKLCH
|
|
30
|
+
|
|
31
|
+
APCA operates on luminance, not OKLCH lightness directly. However, OKLCH L correlates well
|
|
32
|
+
with perceptual lightness. As a rough heuristic:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
ΔL (OKLCH) of 0.40+ between text and background → usually |Lc| ≥ 60
|
|
36
|
+
ΔL (OKLCH) of 0.55+ between text and background → usually |Lc| ≥ 75
|
|
37
|
+
ΔL (OKLCH) of 0.65+ between text and background → usually |Lc| ≥ 90
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**These are approximations.** Always validate with a proper APCA calculator for production.
|
|
41
|
+
The correlation is best when chroma is low; high-chroma colors can deviate.
|
|
42
|
+
|
|
43
|
+
## WCAG 2.2 Contrast Ratios
|
|
44
|
+
|
|
45
|
+
WCAG 2.x uses a simpler luminance ratio (1:1 to 21:1).
|
|
46
|
+
|
|
47
|
+
### Minimum Ratios
|
|
48
|
+
|
|
49
|
+
| Level | Normal Text (< 24px / < 18.66px bold) | Large Text (≥ 24px / ≥ 18.66px bold) | Non-text UI |
|
|
50
|
+
|-------|---------------------------------------|--------------------------------------|-------------|
|
|
51
|
+
| AA | 4.5:1 | 3:1 | 3:1 |
|
|
52
|
+
| AAA | 7:1 | 4.5:1 | — |
|
|
53
|
+
|
|
54
|
+
### WCAG Ratio from OKLCH Lightness (Heuristic)
|
|
55
|
+
|
|
56
|
+
| ΔL (OKLCH) | Approx. WCAG Ratio | Meets |
|
|
57
|
+
|------------|---------------------|-------|
|
|
58
|
+
| 0.30 | ~3:1 | AA Large, UI |
|
|
59
|
+
| 0.40 | ~4.5:1 | AA Normal |
|
|
60
|
+
| 0.55 | ~7:1 | AAA Normal |
|
|
61
|
+
| 0.70+ | ~12:1+ | Well exceeds AAA |
|
|
62
|
+
|
|
63
|
+
Again, these are directional. WCAG ratio depends on relative luminance, not perceptual
|
|
64
|
+
lightness, so hue and chroma affect the actual ratio.
|
|
65
|
+
|
|
66
|
+
## Practical Guidelines for Palette Design
|
|
67
|
+
|
|
68
|
+
1. **Text colors on light backgrounds (L ≥ 0.92):** Use L ≤ 0.45 for body text (AA),
|
|
69
|
+
L ≤ 0.35 for AAA.
|
|
70
|
+
2. **Text colors on dark backgrounds (L ≤ 0.20):** Use L ≥ 0.65 for body text (AA),
|
|
71
|
+
L ≥ 0.75 for AAA.
|
|
72
|
+
3. **Colored text on colored backgrounds:** Reduce chroma on both to improve luminance
|
|
73
|
+
separation. High-chroma pairs can pass OKLCH ΔL checks but fail WCAG because chroma
|
|
74
|
+
affects relative luminance differently per hue.
|
|
75
|
+
4. **Interactive states:** Ensure hover/focus states maintain at least the same contrast as
|
|
76
|
+
the default state. Don't rely solely on color change — combine with underline, outline,
|
|
77
|
+
or weight changes.
|
|
78
|
+
5. **Dark mode:** Don't just invert. Remap: light backgrounds become L ≈ 0.15–0.20,
|
|
79
|
+
text becomes L ≈ 0.85–0.92. Reduce chroma slightly to avoid vibrating colors on dark
|
|
80
|
+
backgrounds.
|
|
81
|
+
|
|
82
|
+
## Tools for Validation
|
|
83
|
+
|
|
84
|
+
- **APCA Calculator:** [apcacontrast.com](https://apcacontrast.com)
|
|
85
|
+
- **WCAG Contrast Checker:** browser DevTools (Chrome, Firefox both have built-in)
|
|
86
|
+
- **Huetone:** Validates OKLCH palettes against contrast targets
|
|
87
|
+
- **Leonardo (Adobe):** Contrast-ratio-driven palette generation
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Hue Emotion Map
|
|
2
|
+
|
|
3
|
+
Consolidated from Adams' *Designer's Dictionary of Color* and Goethe's *Theory of Colours*,
|
|
4
|
+
with practical UI/UX context from Sherin.
|
|
5
|
+
|
|
6
|
+
## How to Use This Reference
|
|
7
|
+
|
|
8
|
+
When selecting hues for a design system, don't pick colors in isolation. Cross-reference the
|
|
9
|
+
emotional/cultural associations below with:
|
|
10
|
+
|
|
11
|
+
1. The **brand personality** the system needs to convey
|
|
12
|
+
2. The **audience's cultural context** (Western associations dominate below — adjust for other markets)
|
|
13
|
+
3. The **functional role** (is this color semantic, like "error", or expressive, like "brand"?)
|
|
14
|
+
|
|
15
|
+
## Hue Associations by Range
|
|
16
|
+
|
|
17
|
+
### Reds (H: 15–40°)
|
|
18
|
+
|
|
19
|
+
| Aspect | Association |
|
|
20
|
+
|--------|------------|
|
|
21
|
+
| **Goethe** | The "plus side" — stimulating, energetic, demands attention. The most forceful color. |
|
|
22
|
+
| **Adams** | Passion, urgency, danger, love, power. In finance: loss/decline. In East Asia: luck, prosperity. |
|
|
23
|
+
| **UI context** | Error states, destructive actions, urgent alerts. Use sparingly — it always dominates. |
|
|
24
|
+
| **Chroma note** | High sRGB chroma available (~0.22 at L=0.65). Can push vibrant reds safely. |
|
|
25
|
+
|
|
26
|
+
### Oranges (H: 40–70°)
|
|
27
|
+
|
|
28
|
+
| Aspect | Association |
|
|
29
|
+
|--------|------------|
|
|
30
|
+
| **Goethe** | Warmth, comfort, approachability. Less aggressive than red. |
|
|
31
|
+
| **Adams** | Energy, enthusiasm, creativity, warmth. Associated with affordability and friendliness. |
|
|
32
|
+
| **UI context** | Warning states (amber/orange), CTAs that feel energetic but not alarming. |
|
|
33
|
+
| **Chroma note** | Moderate ceiling (~0.18). Vibrant oranges need careful gamut management. |
|
|
34
|
+
|
|
35
|
+
### Yellows (H: 70–100°)
|
|
36
|
+
|
|
37
|
+
| Aspect | Association |
|
|
38
|
+
|--------|------------|
|
|
39
|
+
| **Goethe** | Closest to light itself. Joy, cheerfulness, but also anxiety and caution when desaturated. |
|
|
40
|
+
| **Adams** | Optimism, caution (traffic signals), intellect. Can feel cheap if oversaturated. |
|
|
41
|
+
| **UI context** | Warning/caution (amber-yellow), highlights, badges. Avoid as backgrounds — low contrast with white. |
|
|
42
|
+
| **Chroma note** | Drops fast at high lightness. Yellow-50 will be nearly white. Use darker steps for legibility. |
|
|
43
|
+
|
|
44
|
+
### Yellow-Greens (H: 100–130°)
|
|
45
|
+
|
|
46
|
+
| Aspect | Association |
|
|
47
|
+
|--------|------------|
|
|
48
|
+
| **Goethe** | Renewal, growth. The transition zone — can feel fresh or sickly depending on chroma. |
|
|
49
|
+
| **Adams** | Nature, freshness, sustainability. Chartreuse reads as modern/edgy. |
|
|
50
|
+
| **UI context** | Less common in enterprise. Works for eco/sustainability brands. |
|
|
51
|
+
| **Chroma note** | Narrow sRGB gamut (~0.17). Be conservative. |
|
|
52
|
+
|
|
53
|
+
### Greens (H: 130–170°)
|
|
54
|
+
|
|
55
|
+
| Aspect | Association |
|
|
56
|
+
|--------|------------|
|
|
57
|
+
| **Goethe** | Balance, rest, natural equilibrium. The "satisfying" color. |
|
|
58
|
+
| **Adams** | Growth, success, safety, money, health. Universal "positive" signal. |
|
|
59
|
+
| **UI context** | Success states, confirmations, positive metrics, "go" signals. |
|
|
60
|
+
| **Chroma note** | sRGB is very constrained here (~0.13–0.17). P3 adds significant headroom. |
|
|
61
|
+
|
|
62
|
+
### Teals / Cyans (H: 170–210°)
|
|
63
|
+
|
|
64
|
+
| Aspect | Association |
|
|
65
|
+
|--------|------------|
|
|
66
|
+
| **Goethe** | Cool, calming, evokes water and sky at their most serene. |
|
|
67
|
+
| **Adams** | Clarity, professionalism, modern tech. Teal signals sophistication without coldness. |
|
|
68
|
+
| **UI context** | Secondary/accent in tech products. Info states. Dashboard visualizations. |
|
|
69
|
+
| **Chroma note** | Most constrained sRGB region (~0.13). P3 is the biggest win here (+46% chroma). |
|
|
70
|
+
|
|
71
|
+
### Blues (H: 210–270°)
|
|
72
|
+
|
|
73
|
+
| Aspect | Association |
|
|
74
|
+
|--------|------------|
|
|
75
|
+
| **Goethe** | The "minus side" — calm, contemplative, receding. Evokes distance and depth. |
|
|
76
|
+
| **Adams** | Trust, stability, authority, intelligence. The most universally "safe" brand color. Dominates enterprise, finance, healthcare, tech. |
|
|
77
|
+
| **UI context** | Primary brand, links, interactive elements, info states. The default enterprise choice. |
|
|
78
|
+
| **Chroma note** | Moderate ceiling (~0.17 sRGB). Navy blues (high L contrast) are universally accessible. |
|
|
79
|
+
|
|
80
|
+
### Violets / Purples (H: 270–310°)
|
|
81
|
+
|
|
82
|
+
| Aspect | Association |
|
|
83
|
+
|--------|------------|
|
|
84
|
+
| **Goethe** | Restless, ambiguous — neither warm nor cool. Creates tension and mystery. |
|
|
85
|
+
| **Adams** | Luxury, creativity, royalty, spirituality. In tech: AI, innovation, premium tiers. |
|
|
86
|
+
| **UI context** | Premium features, AI/ML indicators, creative tools. Avoid for error/warning (confusion with semantic colors). |
|
|
87
|
+
| **Chroma note** | Opens up slightly (~0.18). Deep purples can be very rich. |
|
|
88
|
+
|
|
89
|
+
### Magentas / Pinks (H: 310–360°/0–15°)
|
|
90
|
+
|
|
91
|
+
| Aspect | Association |
|
|
92
|
+
|--------|------------|
|
|
93
|
+
| **Goethe** | The culmination of red and violet — maximum complexity. Regal and theatrical. |
|
|
94
|
+
| **Adams** | Romance, playfulness, femininity (culturally loaded — use with care). Hot pink reads as bold/rebellious. |
|
|
95
|
+
| **UI context** | Accent/highlight in consumer products. Rare in enterprise unless brand-driven. |
|
|
96
|
+
| **Chroma note** | High availability (~0.22). Can push vibrant magentas in sRGB. |
|
|
97
|
+
|
|
98
|
+
## Quick Reference Table
|
|
99
|
+
|
|
100
|
+
| Hue Range | H° | Primary Emotion | Enterprise Role | Itten Contrast Pair |
|
|
101
|
+
|-----------|-----|-----------------|-----------------|---------------------|
|
|
102
|
+
| Red | 15–40 | Urgency, power | Error, destructive | Complement: Teal (175–200) |
|
|
103
|
+
| Orange | 40–70 | Warmth, energy | Warning, CTA | Complement: Blue (220–250) |
|
|
104
|
+
| Yellow | 70–100 | Optimism, caution | Highlight, badge | Complement: Violet (250–280) |
|
|
105
|
+
| Green | 130–170 | Balance, success | Success, confirm | Complement: Magenta (310–350) |
|
|
106
|
+
| Teal | 170–210 | Clarity, calm | Info, secondary | Complement: Red-orange (25–50) |
|
|
107
|
+
| Blue | 210–270 | Trust, stability | Primary, links | Complement: Orange-yellow (50–90) |
|
|
108
|
+
| Violet | 270–310 | Luxury, creativity | Premium, AI | Complement: Yellow-green (90–130) |
|
|
109
|
+
| Magenta | 310–360 | Boldness, play | Accent, highlight | Complement: Green (130–170) |
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# OKLCH Gamut Reference
|
|
2
|
+
|
|
3
|
+
## How OKLCH Works
|
|
4
|
+
|
|
5
|
+
- **L (Lightness):** 0 (black) → 1 (white). Perceptually uniform — equal steps feel equal.
|
|
6
|
+
- **C (Chroma):** 0 (gray) → ~0.4 (max saturation). Actual max depends on hue and lightness.
|
|
7
|
+
- **H (Hue):** 0–360 degrees. 0/360 = pink-red, 90 = yellow, 145 = green, 265 = blue, 330 = magenta.
|
|
8
|
+
|
|
9
|
+
## sRGB Maximum Chroma by Hue (at L = 0.65)
|
|
10
|
+
|
|
11
|
+
These are approximate ceilings. Exceeding them produces out-of-gamut colors that browsers
|
|
12
|
+
will clamp, potentially shifting hue or lightness unpredictably.
|
|
13
|
+
|
|
14
|
+
| Hue Range | Hue (°) | Max Chroma (sRGB) | Notes |
|
|
15
|
+
|-----------|---------|-------------------|-------|
|
|
16
|
+
| Red | 25 | ~0.22 | High chroma available |
|
|
17
|
+
| Orange | 55 | ~0.18 | Moderate ceiling |
|
|
18
|
+
| Yellow | 90 | ~0.18 | Drops fast at high L |
|
|
19
|
+
| Yellow-green | 120 | ~0.17 | Narrow gamut |
|
|
20
|
+
| Green | 145 | ~0.17 | sRGB is weakest here |
|
|
21
|
+
| Teal | 175 | ~0.13 | Very constrained |
|
|
22
|
+
| Cyan | 200 | ~0.13 | Constrained |
|
|
23
|
+
| Blue | 265 | ~0.17 | Moderate |
|
|
24
|
+
| Violet | 295 | ~0.18 | Opens up slightly |
|
|
25
|
+
| Magenta | 330 | ~0.22 | High chroma |
|
|
26
|
+
| Pink-red | 0/360 | ~0.22 | High chroma |
|
|
27
|
+
|
|
28
|
+
## Key Gamut Rules
|
|
29
|
+
|
|
30
|
+
1. **Chroma ceiling varies by both hue AND lightness.** At very low or very high L, max chroma
|
|
31
|
+
drops significantly regardless of hue.
|
|
32
|
+
2. **sRGB is smallest around teal/cyan (H ≈ 175–200).** If you need vibrant teals, consider
|
|
33
|
+
P3 gamut or accept lower chroma.
|
|
34
|
+
3. **P3 gamut extends chroma ~20–40% beyond sRGB** depending on hue region. Most impactful
|
|
35
|
+
in greens and cyans.
|
|
36
|
+
4. **Always test in-browser.** Even "safe" values can clip on some displays. Use
|
|
37
|
+
`color-mix(in oklch, ...)` or `@media (color-gamut: p3)` for progressive enhancement.
|
|
38
|
+
|
|
39
|
+
## Display P3 Approximate Max Chroma (at L = 0.65)
|
|
40
|
+
|
|
41
|
+
| Hue Region | Max Chroma (P3) | Gain over sRGB |
|
|
42
|
+
|------------|-----------------|----------------|
|
|
43
|
+
| Red (25°) | ~0.29 | +32% |
|
|
44
|
+
| Green (145°) | ~0.24 | +41% |
|
|
45
|
+
| Teal (175°) | ~0.19 | +46% |
|
|
46
|
+
| Cyan (200°) | ~0.18 | +38% |
|
|
47
|
+
| Blue (265°) | ~0.22 | +29% |
|
|
48
|
+
| Magenta (330°) | ~0.28 | +27% |
|
|
49
|
+
|
|
50
|
+
## Lightness vs. Chroma Envelope
|
|
51
|
+
|
|
52
|
+
General pattern for sRGB at any hue:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
L = 0.00–0.15 → Max C ≈ 0.05–0.10 (very dark, low chroma ceiling)
|
|
56
|
+
L = 0.15–0.35 → Max C rises steeply
|
|
57
|
+
L = 0.35–0.70 → Max C at peak (hue-dependent, see table above)
|
|
58
|
+
L = 0.70–0.85 → Max C starts dropping
|
|
59
|
+
L = 0.85–1.00 → Max C ≈ 0.04–0.08 (very light, low chroma ceiling)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This means your most saturated shades will sit in the mid-lightness range (~400–600 on a
|
|
63
|
+
typical scale). The extremes (25, 50, 850, 900) will always be lower chroma.
|
|
64
|
+
|
|
65
|
+
## Practical CSS
|
|
66
|
+
|
|
67
|
+
```css
|
|
68
|
+
/* sRGB safe */
|
|
69
|
+
--color-primary-500: oklch(0.65 0.17 265);
|
|
70
|
+
|
|
71
|
+
/* P3 enhanced with sRGB fallback */
|
|
72
|
+
--color-primary-500: oklch(0.65 0.17 265);
|
|
73
|
+
@media (color-gamut: p3) {
|
|
74
|
+
--color-primary-500: oklch(0.65 0.22 265);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Relative color syntax for tinting */
|
|
78
|
+
--color-primary-100: oklch(from var(--color-primary-500) 0.95 0.03 h);
|
|
79
|
+
```
|