oh-my-design-cli 1.6.0 → 1.6.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.
Files changed (81) hide show
  1. package/AGENTS.md +1 -1
  2. package/README.ko.md +12 -0
  3. package/README.md +49 -0
  4. package/data/reference-fingerprints.json +957 -2
  5. package/dist/bin/oh-my-design.js +4 -3
  6. package/dist/bin/oh-my-design.js.map +1 -1
  7. package/dist/{install-skills-IETT2TBJ.js → install-skills-6QFSN5BN.js} +108 -42
  8. package/dist/install-skills-6QFSN5BN.js.map +1 -0
  9. package/package.json +9 -3
  10. package/scripts/postinstall.cjs +6 -6
  11. package/skills/claude-design/SKILL.md +385 -0
  12. package/skills/claude-design/references/claude-design-flow.md +425 -0
  13. package/skills/claude-design/references/codebase-analysis.md +373 -0
  14. package/skills/claude-design/scripts/analyze_codebase.py +1369 -0
  15. package/skills/claude-design/scripts/clickable_link.sh +48 -0
  16. package/skills/claude-design/scripts/collect_source.py +178 -0
  17. package/skills/claude-design/scripts/drive_claude_design.cjs +378 -0
  18. package/skills/claude-design/scripts/gather_references.py +437 -0
  19. package/web/references/91app/DESIGN.md +151 -0
  20. package/web/references/airtable/DESIGN.md +16 -2
  21. package/web/references/bithumb/DESIGN.md +170 -0
  22. package/web/references/bunjang/DESIGN.md +20 -1
  23. package/web/references/cakeresume/DESIGN.md +162 -0
  24. package/web/references/catchtable/DESIGN.md +19 -0
  25. package/web/references/classting/DESIGN.md +251 -0
  26. package/web/references/classum/DESIGN.md +19 -0
  27. package/web/references/coinone/DESIGN.md +218 -0
  28. package/web/references/dabang/DESIGN.md +19 -0
  29. package/web/references/devsisters/DESIGN.md +253 -0
  30. package/web/references/dji/DESIGN.md +0 -1
  31. package/web/references/drnow/DESIGN.md +331 -0
  32. package/web/references/fastcampus/DESIGN.md +19 -0
  33. package/web/references/flex/DESIGN.md +19 -0
  34. package/web/references/flo/DESIGN.md +306 -0
  35. package/web/references/fugle/DESIGN.md +250 -0
  36. package/web/references/gmarket/DESIGN.md +19 -0
  37. package/web/references/gogolook/DESIGN.md +131 -0
  38. package/web/references/grip/DESIGN.md +250 -0
  39. package/web/references/hahow/DESIGN.md +158 -0
  40. package/web/references/hogangnono/DESIGN.md +308 -0
  41. package/web/references/hyundaicard/DESIGN.md +177 -0
  42. package/web/references/inflearn/DESIGN.md +19 -0
  43. package/web/references/jkopay/DESIGN.md +249 -0
  44. package/web/references/jobkorea/DESIGN.md +310 -0
  45. package/web/references/kbank/DESIGN.md +18 -0
  46. package/web/references/kdan/DESIGN.md +160 -0
  47. package/web/references/kkbox/DESIGN.md +114 -0
  48. package/web/references/krafton/DESIGN.md +230 -0
  49. package/web/references/kream/DESIGN.md +18 -0
  50. package/web/references/laftel/DESIGN.md +253 -0
  51. package/web/references/lezhin/DESIGN.md +301 -0
  52. package/web/references/lunit/DESIGN.md +19 -0
  53. package/web/references/melon/DESIGN.md +153 -0
  54. package/web/references/momoshop/DESIGN.md +279 -0
  55. package/web/references/mustit/DESIGN.md +282 -0
  56. package/web/references/nhncloud/DESIGN.md +174 -0
  57. package/web/references/oliveyoung/DESIGN.md +19 -0
  58. package/web/references/payco/DESIGN.md +227 -0
  59. package/web/references/piccollage/DESIGN.md +277 -0
  60. package/web/references/rayark/DESIGN.md +132 -0
  61. package/web/references/riiid/DESIGN.md +228 -0
  62. package/web/references/sendbird/DESIGN.md +285 -0
  63. package/web/references/socar/DESIGN.md +18 -0
  64. package/web/references/toss-securities/DESIGN.md +19 -0
  65. package/web/references/trenbe/DESIGN.md +252 -0
  66. package/web/references/tving/DESIGN.md +18 -0
  67. package/web/references/upbit/DESIGN.md +19 -0
  68. package/web/references/upstage/DESIGN.md +18 -0
  69. package/web/references/velog/DESIGN.md +168 -0
  70. package/web/references/voicetube/DESIGN.md +227 -0
  71. package/web/references/wadiz/DESIGN.md +19 -0
  72. package/web/references/webflow/DESIGN.md +16 -2
  73. package/web/references/yeogiotte/DESIGN.md +19 -0
  74. package/data/architecture-proposals/2026-05-13-thin-install-fresh-fetch.md +0 -189
  75. package/data/issues/2026-05-13-multi-surface-schema-rfc.md +0 -67
  76. package/data/reference-audits/2026-05-13-kr10.md +0 -132
  77. package/data/reference-audits/2026-05-14-kr10.md +0 -72
  78. package/data/reference-audits/2026-05-15-kr10.md +0 -124
  79. package/data/research/2026-05-18-agent-landscape.md +0 -69
  80. package/data/research/2026-05-18-kr-style-presets.md +0 -572
  81. package/dist/install-skills-IETT2TBJ.js.map +0 -1
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env python3
2
+ """Gather local brand/visual reference assets (and explicit URL references) for the claude-design skill.
3
+
4
+ Walks a root directory (default: current working directory) up to a bounded depth, pruning
5
+ noisy/excluded directories in place, and collects files whose extension matches a set of
6
+ VISUAL image asset types. By default DOCUMENT files (.pdf, .hwp/.hwpx, .doc/.docx, .ppt/.pptx,
7
+ .xls/.xlsx) are EXCLUDED — they are document templates, not brand visuals — unless --include-docs
8
+ is passed.
9
+
10
+ Ranking (v2): brand-likelihood FIRST, then modification time (newest first). Filenames that look
11
+ like real brand assets (logo, wordmark, symbol, icon, brand, og, hero, cover, favicon, app-icon)
12
+ score high and float to the top; everything else falls back to mtime. Results are then size-filtered
13
+ and capped at a maximum count. This fixes the prior bug where large PDF business-plan templates
14
+ outranked the real brand assets (logo.svg / og.png) that Claude Design actually wants.
15
+
16
+ Explicit URL references can be passed (repeatable) and are echoed back alongside the discovered files.
17
+
18
+ Designed to be robust: never crashes on a bad/unreadable path (PermissionError, FileNotFoundError,
19
+ OSError are all swallowed gracefully). stdlib only.
20
+
21
+ CLI:
22
+ python3 gather_references.py [--root DIR] [--max N] [--max-mb FLOAT] [--url URL ...]
23
+ [--ext .png,.jpg,...] [--exclude GLOB ...] [--depth N] [--include-docs] [--json]
24
+
25
+ Output:
26
+ With --json: a JSON object
27
+ {"root": ..., "files": [{"path","type","bytes","mtime_iso"}], "urls": [...],
28
+ "truncated": bool, "ranked_by": ..., "note": ...}
29
+ Without --json: a short human-readable summary.
30
+ """
31
+
32
+ import argparse
33
+ import fnmatch
34
+ import json
35
+ import os
36
+ import re
37
+ import sys
38
+ from datetime import datetime, timezone
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Defaults (mirrored in the SKILL.md file contract)
42
+ # ---------------------------------------------------------------------------
43
+
44
+ DEFAULT_MAX_FILES = 12
45
+ DEFAULT_MAX_MB = 8.0
46
+ DEFAULT_DEPTH = 4
47
+
48
+ # v2: VISUAL-ONLY by default. Brand visuals, not document templates.
49
+ DEFAULT_EXTENSIONS = [
50
+ ".png", ".jpg", ".jpeg", ".webp", ".svg", ".avif", ".gif",
51
+ ]
52
+
53
+ # Document extensions are never visual brand assets. Excluded from the default
54
+ # scan; only included when --include-docs is passed (they are then *added* to the
55
+ # active extension set, alongside whatever visual/--ext set is in effect).
56
+ DOC_EXTENSIONS = [
57
+ ".pdf",
58
+ ".hwp", ".hwpx",
59
+ ".doc", ".docx",
60
+ ".ppt", ".pptx",
61
+ ".xls", ".xlsx",
62
+ ]
63
+
64
+ DEFAULT_EXCLUDED_DIRS = [
65
+ "node_modules", ".git", "dist", "build", ".next", ".nuxt", "out",
66
+ "venv", ".venv", "__pycache__", ".cache", "coverage", "target",
67
+ "Pods", ".gradle", ".idea", ".DS_Store",
68
+ ]
69
+
70
+ # v2: brand-likelihood ranking. A filename whose basename contains one of these
71
+ # tokens (as a whole token, delimited by start/end of name or any non-alphanumeric
72
+ # separator) is treated as a likely real brand asset and floated to the top of the
73
+ # results, ahead of generic screenshots / incidental images.
74
+ #
75
+ # Token boundaries matter: the short "og" token (Open Graph: og.png / og-image.png)
76
+ # must NOT match as a substring inside ordinary words like dog/frog/blog/logout/
77
+ # progress/recognition/cognac/biography, and "logo" must not match "logout". The
78
+ # leading group consumes one separator (or start-of-string); the trailing boundary
79
+ # is a zero-width lookahead so adjacent tokens (e.g. "icon-logo") both still match.
80
+ BRAND_KEYWORD_PATTERN = re.compile(
81
+ r"(?:^|[^a-z0-9])"
82
+ r"(?:logo|wordmark|symbol|icon|brand|hero|cover|favicon|app[-_]?icon|og[-_]?image|og)"
83
+ r"(?=[^a-z0-9]|$)",
84
+ re.IGNORECASE,
85
+ )
86
+
87
+ RANKED_BY = "brand-likelihood, then mtime-desc"
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # Helpers
92
+ # ---------------------------------------------------------------------------
93
+
94
+ def _normalize_extensions(raw):
95
+ """Turn a comma-separated extension string into a lowercased set with leading dots.
96
+
97
+ Accepts forms like ".png,jpg, .JPEG" and yields {".png", ".jpg", ".jpeg"}.
98
+ Returns None to signal "fall back to defaults" when nothing usable is parsed.
99
+ """
100
+ if raw is None:
101
+ return None
102
+ exts = set()
103
+ for part in str(raw).split(","):
104
+ part = part.strip().lower()
105
+ if not part:
106
+ continue
107
+ if not part.startswith("."):
108
+ part = "." + part
109
+ exts.add(part)
110
+ return exts or None
111
+
112
+
113
+ def _ext_of(path):
114
+ """Lowercased file extension including the leading dot (e.g. '.png'), or '' if none."""
115
+ return os.path.splitext(path)[1].lower()
116
+
117
+
118
+ def _type_of(path):
119
+ """Lowercased extension WITHOUT the leading dot (e.g. 'png'), or '' if none."""
120
+ return _ext_of(path).lstrip(".")
121
+
122
+
123
+ def _mtime_iso(epoch):
124
+ """Convert an epoch float to an ISO-8601 UTC string; never raises."""
125
+ try:
126
+ return datetime.fromtimestamp(epoch, tz=timezone.utc).isoformat()
127
+ except (OverflowError, OSError, ValueError):
128
+ return ""
129
+
130
+
131
+ def _is_excluded_dir(name, exclude_globs):
132
+ """True if a directory basename matches any exclude glob (or default excluded set)."""
133
+ for pattern in exclude_globs:
134
+ if fnmatch.fnmatch(name, pattern):
135
+ return True
136
+ return False
137
+
138
+
139
+ def _brand_score(path):
140
+ """Heuristic brand-likelihood score for a file path.
141
+
142
+ Higher = more likely to be a real brand asset Claude Design should reference.
143
+ Scored on the *filename* (the basename), case-insensitively, against
144
+ BRAND_KEYWORD_PATTERN (which matches brand tokens only at token boundaries, so
145
+ "og" matches og.png/og-image but never dog/blog/logout). Returns 1 if any brand
146
+ token is present, else 0.
147
+
148
+ This is intentionally a coarse binary signal: brand-likelihood is the PRIMARY
149
+ sort key, with mtime as the tie-breaker, so we only need to separate "looks
150
+ like a brand asset" from "everything else". Never raises.
151
+ """
152
+ try:
153
+ name = os.path.basename(path)
154
+ except Exception:
155
+ return 0
156
+ return 1 if BRAND_KEYWORD_PATTERN.search(name) else 0
157
+
158
+
159
+ def gather(root, max_files, max_mb, extensions, exclude_globs, depth):
160
+ """Walk `root` up to `depth` levels, collecting matching files.
161
+
162
+ Returns (files, truncated, note) where:
163
+ - files: list of dicts {path, type, bytes, mtime_iso}, ranked by brand-likelihood
164
+ first then mtime-desc, size-filtered, and capped.
165
+ - truncated: True if matching files were dropped because of the cap. A negative
166
+ `max_files` means "keep everything", so it is never truncated.
167
+ - note: optional human note (e.g. about an unreadable root); '' otherwise.
168
+ """
169
+ note = ""
170
+ matched = [] # list of (brand_score, mtime, path, size)
171
+
172
+ try:
173
+ abs_root = os.path.abspath(root)
174
+ except (OSError, ValueError):
175
+ return [], False, "Could not resolve root path: %r" % (root,)
176
+
177
+ if not os.path.exists(abs_root):
178
+ return [], False, "Root path does not exist: %s" % abs_root
179
+ if not os.path.isdir(abs_root):
180
+ return [], False, "Root path is not a directory: %s" % abs_root
181
+
182
+ max_bytes = None
183
+ if max_mb is not None and max_mb >= 0:
184
+ max_bytes = int(max_mb * 1024 * 1024)
185
+
186
+ root_depth = abs_root.rstrip(os.sep).count(os.sep)
187
+
188
+ try:
189
+ # os.walk does NOT follow directory symlinks by default, so symlink loops cannot hang us.
190
+ walker = os.walk(abs_root, topdown=True, onerror=lambda err: None)
191
+ for current_dir, dirnames, filenames in walker:
192
+ # Enforce bounded depth by pruning descent once we exceed the limit.
193
+ try:
194
+ current_depth = current_dir.rstrip(os.sep).count(os.sep) - root_depth
195
+ except Exception:
196
+ current_depth = 0
197
+ if current_depth >= depth:
198
+ # Do not descend any deeper.
199
+ dirnames[:] = []
200
+ else:
201
+ # Prune excluded directories IN PLACE so os.walk skips them entirely.
202
+ dirnames[:] = [
203
+ d for d in dirnames if not _is_excluded_dir(d, exclude_globs)
204
+ ]
205
+
206
+ for fname in filenames:
207
+ if _ext_of(fname) not in extensions:
208
+ continue
209
+ fpath = os.path.join(current_dir, fname)
210
+ try:
211
+ st = os.stat(fpath)
212
+ except (PermissionError, FileNotFoundError, OSError):
213
+ # Unreadable entry or broken symlink; skip gracefully.
214
+ continue
215
+ # Skip anything that isn't a regular file (e.g. a directory named "foo.png").
216
+ if not os.path.isfile(fpath):
217
+ continue
218
+ size = st.st_size
219
+ if max_bytes is not None and size > max_bytes:
220
+ continue
221
+ matched.append((_brand_score(fpath), st.st_mtime, fpath, size))
222
+ except (PermissionError, FileNotFoundError, OSError) as exc:
223
+ note = "Walk interrupted: %s" % exc
224
+
225
+ # v2 ranking: brand-likelihood FIRST (high score first), then newest first.
226
+ matched.sort(key=lambda item: (item[0], item[1]), reverse=True)
227
+
228
+ if max_files >= 0:
229
+ truncated = len(matched) > max_files
230
+ kept = matched[:max_files]
231
+ else:
232
+ # Negative cap means "keep everything"; nothing is dropped.
233
+ truncated = False
234
+ kept = matched
235
+
236
+ files = []
237
+ for _score, mtime, fpath, size in kept:
238
+ files.append({
239
+ "path": fpath,
240
+ "type": _type_of(fpath),
241
+ "bytes": size,
242
+ "mtime_iso": _mtime_iso(mtime),
243
+ })
244
+
245
+ return files, truncated, note
246
+
247
+
248
+ # ---------------------------------------------------------------------------
249
+ # CLI
250
+ # ---------------------------------------------------------------------------
251
+
252
+ def build_parser():
253
+ parser = argparse.ArgumentParser(
254
+ prog="gather_references.py",
255
+ description=(
256
+ "Scan a directory for brand/visual reference assets and collect explicit URL "
257
+ "references for the claude-design skill. Visual-only by default; documents "
258
+ "(.pdf/.hwp/.doc/.ppt/.xls) are excluded unless --include-docs is given. "
259
+ "Results are ranked by brand-likelihood first, then by modification time."
260
+ ),
261
+ )
262
+ parser.add_argument(
263
+ "--root",
264
+ default=os.getcwd(),
265
+ help="Directory to scan (default: current working directory).",
266
+ )
267
+ parser.add_argument(
268
+ "--max",
269
+ dest="max_files",
270
+ type=int,
271
+ default=DEFAULT_MAX_FILES,
272
+ help="Maximum number of files to return (default: %d)." % DEFAULT_MAX_FILES,
273
+ )
274
+ parser.add_argument(
275
+ "--max-mb",
276
+ dest="max_mb",
277
+ type=float,
278
+ default=DEFAULT_MAX_MB,
279
+ help="Skip files larger than this many megabytes (default: %s)." % DEFAULT_MAX_MB,
280
+ )
281
+ parser.add_argument(
282
+ "--url",
283
+ dest="urls",
284
+ action="append",
285
+ default=[],
286
+ metavar="URL",
287
+ help="Explicit URL reference (repeatable).",
288
+ )
289
+ parser.add_argument(
290
+ "--ext",
291
+ dest="ext",
292
+ default=None,
293
+ metavar=".png,.jpg,...",
294
+ help=(
295
+ "Comma-separated list of extensions to match. Overrides the visual-only "
296
+ "default (%s). If --include-docs is also passed, document extensions are "
297
+ "added on top of this set." % ",".join(DEFAULT_EXTENSIONS)
298
+ ),
299
+ )
300
+ parser.add_argument(
301
+ "--include-docs",
302
+ dest="include_docs",
303
+ action="store_true",
304
+ help=(
305
+ "Also include document files (%s). These are EXCLUDED by default because "
306
+ "they are document templates, not brand visuals." % ",".join(DOC_EXTENSIONS)
307
+ ),
308
+ )
309
+ parser.add_argument(
310
+ "--exclude",
311
+ dest="exclude",
312
+ action="append",
313
+ default=[],
314
+ metavar="GLOB",
315
+ help="Additional directory-name glob(s) to exclude (repeatable).",
316
+ )
317
+ parser.add_argument(
318
+ "--depth",
319
+ type=int,
320
+ default=DEFAULT_DEPTH,
321
+ help="Maximum directory depth to descend (default: %d)." % DEFAULT_DEPTH,
322
+ )
323
+ parser.add_argument(
324
+ "--json",
325
+ action="store_true",
326
+ help="Emit a JSON object instead of a human-readable summary.",
327
+ )
328
+ return parser
329
+
330
+
331
+ def _human_size(num_bytes):
332
+ """Render a byte count as a compact human-readable string."""
333
+ try:
334
+ value = float(num_bytes)
335
+ except (TypeError, ValueError):
336
+ return "?"
337
+ for unit in ("B", "KB", "MB", "GB", "TB"):
338
+ if value < 1024.0 or unit == "TB":
339
+ if unit == "B":
340
+ return "%d %s" % (int(value), unit)
341
+ return "%.1f %s" % (value, unit)
342
+ value /= 1024.0
343
+ return "%.1f TB" % value
344
+
345
+
346
+ def main(argv=None):
347
+ parser = build_parser()
348
+ args = parser.parse_args(argv)
349
+
350
+ # Resolve the active extension set:
351
+ # - --ext (if given) replaces the visual-only default;
352
+ # - otherwise use the visual-only default;
353
+ # - --include-docs ADDS document extensions on top of whichever set is active.
354
+ extensions = _normalize_extensions(args.ext)
355
+ if extensions is None:
356
+ extensions = set(DEFAULT_EXTENSIONS)
357
+ if args.include_docs:
358
+ extensions = set(extensions) | set(DOC_EXTENSIONS)
359
+
360
+ exclude_globs = list(DEFAULT_EXCLUDED_DIRS) + list(args.exclude or [])
361
+
362
+ depth = args.depth if args.depth is not None and args.depth >= 0 else DEFAULT_DEPTH
363
+
364
+ files, truncated, note = gather(
365
+ root=args.root,
366
+ max_files=args.max_files,
367
+ max_mb=args.max_mb,
368
+ extensions=extensions,
369
+ exclude_globs=exclude_globs,
370
+ depth=depth,
371
+ )
372
+
373
+ urls = list(args.urls or [])
374
+
375
+ try:
376
+ abs_root = os.path.abspath(args.root)
377
+ except (OSError, ValueError):
378
+ abs_root = args.root
379
+
380
+ if args.json:
381
+ payload = {
382
+ "root": abs_root,
383
+ "files": files,
384
+ "urls": urls,
385
+ "truncated": truncated,
386
+ "ranked_by": RANKED_BY,
387
+ "note": note,
388
+ }
389
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
390
+ return 0
391
+
392
+ # Human-readable summary.
393
+ lines = []
394
+ lines.append("Reference scan root: %s" % abs_root)
395
+ lines.append(
396
+ "Found %d file(s) (cap=%d, max-mb=%s, depth=%d, docs=%s)%s"
397
+ % (
398
+ len(files),
399
+ args.max_files,
400
+ args.max_mb,
401
+ depth,
402
+ "included" if args.include_docs else "excluded",
403
+ " [truncated: more matched than shown]" if truncated else "",
404
+ )
405
+ )
406
+ lines.append("Ranked by: %s" % RANKED_BY)
407
+ if files:
408
+ lines.append("")
409
+ for entry in files:
410
+ lines.append(
411
+ " - %s [%s, %s, %s]"
412
+ % (
413
+ entry["path"],
414
+ entry["type"] or "?",
415
+ _human_size(entry["bytes"]),
416
+ entry["mtime_iso"] or "?",
417
+ )
418
+ )
419
+ else:
420
+ lines.append(" (no matching brand/visual assets found)")
421
+
422
+ if urls:
423
+ lines.append("")
424
+ lines.append("URL references (%d):" % len(urls))
425
+ for url in urls:
426
+ lines.append(" - %s" % url)
427
+
428
+ if note:
429
+ lines.append("")
430
+ lines.append("Note: %s" % note)
431
+
432
+ print("\n".join(lines))
433
+ return 0
434
+
435
+
436
+ if __name__ == "__main__":
437
+ sys.exit(main())
@@ -0,0 +1,151 @@
1
+ ---
2
+ id: "91app"
3
+ name: "91APP"
4
+ country: TW
5
+ category: ecommerce
6
+ homepage: "https://www.91app.com"
7
+ primary_color: "#061C3D"
8
+ logo:
9
+ type: favicon
10
+ slug: "https://www.google.com/s2/favicons?domain=91app.com&sz=128"
11
+ verified: "2026-06-01"
12
+ omd: "0.1"
13
+ ---
14
+ # Design System Inspiration of 91APP
15
+
16
+ ## 1. Visual Theme & Atmosphere
17
+
18
+ 91APP carries the composure of retail infrastructure built to be trusted at scale — the brand of Taiwan's leading omnichannel OMO (online-merge-offline) commerce SaaS. Its identity rests on a deep structural navy (#061C3D) that anchors text, headings, and the primary call-to-action, giving every screen the gravity of a B2B platform that merchants stake their storefronts on. Against a clean white ground (#FFFFFF), that navy reads as steady and engineered rather than playful. A coral-red accent (#E85040) provides the single point of energy — the action color reserved for moments that should feel decisive. Traditional-Chinese Noto Sans TC sets the type with neutral, legible clarity suited to a Taiwanese merchant audience. The overall atmosphere is one of confident retail infrastructure: orderly, generously rounded at the touch points, and quietly serious.
19
+
20
+ ## 2. Color Palette & Roles
21
+
22
+ The palette is disciplined and role-driven, with navy as the dominant structural color and coral as the lone action accent.
23
+
24
+ | Token | Value | Role |
25
+ |-------|-------|------|
26
+ | Structural navy | #061C3D | Body text, headings, primary button background |
27
+ | Action coral | #E85040 | Coral CTA background — the action accent |
28
+ | Red emphasis | #CB200E | Emphasis text, highlight callouts |
29
+ | Neutral fill | #F7F6FB | Neutral / secondary button fill |
30
+ | Ground | #FFFFFF | Page background, text on filled buttons |
31
+
32
+ Navy (#061C3D, rgb 6,28,61) does the heavy lifting: it is the text color, the heading color, and the fill of the primary button — chosen as the brand's primary color precisely because it carries so much of the interface. Coral (#E85040, rgb 232,80,64) is the deliberate counterpoint, an action accent that should stay rare to keep its decisiveness. Red emphasis (#CB200E) is a hotter red reserved for emphasis text. The neutral fill (#F7F6FB) gives secondary surfaces a soft, near-white lift off the pure-white ground.
33
+
34
+ ## 3. Typography Rules
35
+
36
+ Type is set in Noto Sans TC (Traditional Chinese), with Helvetica as the fallback — a neutral, highly legible pairing appropriate for a Taiwanese merchant audience. Body copy runs at 16px. Hero headings step up to 44px at weight 700 in structural navy (#061C3D), giving the top of the page clear authority without ornament. The hierarchy is straightforward: large bold navy headings over calm 16px body, letting the content of a commerce platform stay scannable.
37
+
38
+ - Body: 16px, Noto Sans TC / Helvetica
39
+ - Hero heading: 44px / 700, #061C3D, Noto Sans TC
40
+
41
+ ## 4. Component Stylings
42
+
43
+ ### Primary Button
44
+
45
+ **Default (navy)**
46
+ - Background: #061C3D
47
+ - Text: #FFFFFF
48
+ - Border: none
49
+ - Radius: 16px
50
+ - Height: 48px
51
+ - Font: 16px / 600
52
+ - Use: Primary call-to-action — the dominant navy action on white ground
53
+
54
+ ### Coral CTA Button
55
+
56
+ **Default (coral accent)**
57
+ - Background: #E85040
58
+ - Text: #FFFFFF
59
+ - Border: none
60
+ - Radius: 16px
61
+ - Height: 40px
62
+ - Font: 16px / 500
63
+ - Use: The single energetic action accent — reserve for decisive, attention-drawing moments
64
+
65
+ ### Neutral Button
66
+
67
+ **Default (secondary)**
68
+ - Background: #F7F6FB
69
+ - Text: #061C3D
70
+ - Border: none
71
+ - Radius: 3px
72
+ - Height: 48px
73
+ - Font: 16px / 600
74
+ - Use: Secondary / neutral action on a soft near-white fill
75
+
76
+ ### Hero Heading
77
+
78
+ **Default**
79
+ - Background: transparent
80
+ - Text: #061C3D
81
+ - Border: none
82
+ - Font: 44px / 700
83
+ - Use: Top-of-page heading in Noto Sans TC carrying brand authority
84
+
85
+ ## 5. Layout Principles
86
+
87
+ The layout reads as clean retail infrastructure: a white ground (#FFFFFF) gives content room to breathe, navy structure organizes the hierarchy, and the coral accent is placed sparingly so the eye knows exactly where the action is. Generously rounded primary buttons (16px radius) sit as confident, tappable anchors. Secondary surfaces use the neutral fill (#F7F6FB) to separate without hard borders, keeping the page calm and uncluttered. The composition favors order and legibility over decoration — the look of a platform whose job is to make merchants feel secure.
88
+
89
+ ## 6. Depth & Elevation
90
+
91
+ Depth is handled with restraint. Rather than heavy shadows, separation comes from color and fill: the neutral #F7F6FB surfaces lift gently off the pure-white ground, and the saturated navy and coral buttons stand forward through contrast alone. The generous 16px corner radius on primary actions softens the interface and signals approachability, while the tighter 3px radius on the neutral button reads as a more utilitarian, grounded surface. The overall sense of elevation is flat and modern, leaning on contrast and rounding instead of literal shadow stacking.
92
+
93
+ ## 7. Do's and Don'ts
94
+
95
+ ### Do
96
+ - Use navy #061C3D as the structural backbone — text, headings, and the primary button.
97
+ - Keep coral #E85040 rare, reserved for the single most important action.
98
+ - Set type in Noto Sans TC with Helvetica fallback; body at 16px.
99
+ - Give primary actions the generous 16px radius for an approachable, tappable feel.
100
+ - Lift secondary surfaces with the soft #F7F6FB neutral fill instead of hard borders.
101
+
102
+ ### Don't
103
+ - Spread coral #E85040 across many elements — it loses its decisive force.
104
+ - Put navy text on navy fill or otherwise compromise the navy/white contrast.
105
+ - Mix the red emphasis #CB200E into general body copy; keep it for emphasis.
106
+ - Invent ornament or heavy shadows — the brand reads engineered and calm.
107
+
108
+ ## 8. Responsive Behavior
109
+
110
+ The brand's button system is sized for touch and scale: a 48px-tall primary button and 48px neutral button give comfortable tap targets, while the 40px coral CTA reads as a slightly more compact accent. With a 16px body size and large 44px hero headings, the hierarchy holds up from desktop down to mobile merchant views. The white ground and soft neutral fills keep content legible across viewport sizes without relying on layout-specific decoration. (Specific breakpoint values are not provided in the source; size and contrast carry the responsive behavior.)
111
+
112
+ ## 9. Agent Prompt Guide
113
+
114
+ When generating UI in the 91APP style, instruct the agent: build on a white ground (#FFFFFF) with deep navy (#061C3D) as the structural color for body text, headings, and the primary button. Make the primary button navy with white text, 16px radius, 48px height, 16px/600 type. Reserve a single coral (#E85040) action accent — white text, 16px radius, 40px height, 16px/500 — for the most decisive call-to-action only. For secondary actions, use the neutral fill #F7F6FB with navy text, 3px radius, 48px height, 16px/600. Set hero headings at 44px/700 in navy. Use Noto Sans TC (Helvetica fallback), 16px body. Keep the feel calm, engineered, and trustworthy — retail infrastructure, not decoration. Use red emphasis #CB200E only for emphasized text.
115
+
116
+ ## 10. Voice & Tone
117
+
118
+ The voice is that of trustworthy retail infrastructure — confident, clear, and B2B-grade. It speaks to merchants who are betting their storefronts on the platform, so it favors steadiness and competence over hype. Like the navy-dominant palette, the tone is structural and dependable, with energy reserved for the moments that matter. It is professional Traditional-Chinese-first, addressing a Taiwanese commerce audience directly and practically.
119
+
120
+ ## 11. Brand Narrative
121
+
122
+ 91APP is Taiwan's leading omnichannel retail-commerce SaaS, built around OMO — online-merge-offline. Its visual identity tells that story: a deep-navy structural brand conveys the reliability of infrastructure merchants depend on, while a coral-red action accent on a clean white ground signals the decisive moments of commerce. The Traditional-Chinese Noto Sans typography and generously rounded primary buttons round out a brand that reads as confident, approachable retail infrastructure — serious where it counts, welcoming at the point of action.
123
+
124
+ ## 12. Principles
125
+
126
+ - Structure first: navy carries text, headings, and the primary action — the brand's backbone.
127
+ - One point of energy: coral is the lone action accent, kept rare to stay decisive.
128
+ - Clarity over ornament: white ground, legible 16px Noto Sans TC, no unnecessary decoration.
129
+ - Approachable touch points: generous 16px rounding on primary actions invites interaction.
130
+ - Trust through restraint: soft neutral fills and flat depth keep the platform calm and credible.
131
+
132
+ ## 13. Personas
133
+
134
+ - **The Taiwanese merchant** — runs an omnichannel store and needs a platform that feels dependable; reassured by the navy structural brand and clear, legible interface.
135
+ - **The operations lead** — manages day-to-day commerce flows and values the unambiguous hierarchy where coral marks exactly where to act.
136
+ - **The decision-maker evaluating SaaS** — reads the engineered, confident aesthetic as a signal of trustworthy retail infrastructure worth staking a business on.
137
+
138
+ ## 14. States
139
+
140
+ State styling is expressed through the documented button variants. The primary navy button (#061C3D background, white text, 16px radius, 48px height, 16px/600) is the default decisive action. The coral CTA (#E85040 background, white text, 16px radius, 40px height, 16px/500) marks the single high-energy action state. The neutral button (#F7F6FB fill, navy text, 3px radius, 48px height, 16px/600) covers the calm secondary state. Red emphasis (#CB200E) signals an emphasized or highlighted text state. (Hover, pressed, focus, and disabled values are not provided in the source; derive them by darkening or lightening these base colors while preserving the navy/coral roles.)
141
+
142
+ ## 15. Motion & Easing
143
+
144
+ Specific motion and easing values are not provided in the source. In keeping with the brand's engineered, trustworthy character, any motion should be restrained and purposeful — calm transitions that reinforce stability rather than draw attention to themselves, with the coral action accent reserved for the moments worth animating.
145
+
146
+ ---
147
+ **Verified:** 2026-06-01
148
+ **Tier 1 sources:** https://www.91app.com (live DOM — body text, primary/coral/neutral buttons, hero heading, all hex/px values), https://91app.tech (brand-owned regional engineering/tech source), https://github.com/91APP (brand-owned regional org)
149
+ **Tier 2 sources:** getdesign.md/91app — NOT LISTED. refero — not listed. Note: navy #061C3D is structural/dominant; coral #E85040 is the action accent (brand-color choice: navy chosen as primary as it carries text+headings+primary button).
150
+ **Conflicts unresolved:** none
151
+ **Proof:** see .verification.md (## Proof block)
@@ -154,8 +154,22 @@ Airtable's website is a clean, enterprise-friendly platform that communicates "s
154
154
  - Soft ambient: `rgba(15,48,106,0.05) 0px 0px 20px`
155
155
 
156
156
  ## 7. Do's and Don'ts
157
- ### Do: Use Airtable Blue for CTAs, Haas with positive tracking, 12px radius buttons
158
- ### Don't: Skip positive letter-spacing, use heavy shadows
157
+
158
+ ### Do
159
+ - Use Airtable Blue (`#1b61c9`) only for CTAs and links, set on a white (`#ffffff`) canvas with Deep Navy (`#181d26`) text
160
+ - Set the Haas / Haas Groot Disp font system with positive letter-spacing on body and small text (0.08px–0.28px) — it is Airtable's typographic signature
161
+ - Apply the radius scale by component size: 12px buttons, 16px standard cards, 24px sections, 32px large containers
162
+ - Lift primary buttons with the signature blue-tinted multi-layer shadow (`rgba(45,127,249,0.28) 0px 1px 3px`) so elevation ties back to the brand color
163
+ - Reserve color for user data and keep chrome neutral, signaling 'live work' with the spotlight surface (`rgba(249,252,255,0.97)`) plus subtle `#e0e2e6` borders
164
+ - Name theme variables with the semantic `--theme_*` convention (e.g. `--theme_success-text` for `#006400`) to match Airtable's internal tokens
165
+
166
+ ### Don't
167
+ - Skip the positive letter-spacing on body and caption text — it is what gives Airtable its Swiss-precision feel
168
+ - Lean on heavy gray backgrounds or dark drop shadows for depth instead of the spotlight surface and the soft ambient `rgba(15,48,106,0.05) 0px 0px 20px` glow
169
+ - Spread Airtable Blue (`#1b61c9`) across chrome or large backgrounds — color belongs to user data, not the UI frame
170
+ - Reach for the deliberately sharp 2px radius outside its cookie-consent context where buttons and cards use 12px and up
171
+ - Add bouncy spring motion or exceed the 150–400ms timing tokens, and respect `prefers-reduced-motion` by dropping the spotlight fade-in
172
+ - Use forbidden voice like 'revolutionary database', 'no-code magic', or emoji in product chrome
159
173
 
160
174
  ## 8. Responsive Behavior
161
175
  Breakpoints: 425–1664px (23 breakpoints)