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.
- package/AGENTS.md +1 -1
- package/README.ko.md +12 -0
- package/README.md +49 -0
- package/data/reference-fingerprints.json +957 -2
- package/dist/bin/oh-my-design.js +4 -3
- package/dist/bin/oh-my-design.js.map +1 -1
- package/dist/{install-skills-IETT2TBJ.js → install-skills-6QFSN5BN.js} +108 -42
- package/dist/install-skills-6QFSN5BN.js.map +1 -0
- package/package.json +9 -3
- package/scripts/postinstall.cjs +6 -6
- package/skills/claude-design/SKILL.md +385 -0
- package/skills/claude-design/references/claude-design-flow.md +425 -0
- package/skills/claude-design/references/codebase-analysis.md +373 -0
- package/skills/claude-design/scripts/analyze_codebase.py +1369 -0
- package/skills/claude-design/scripts/clickable_link.sh +48 -0
- package/skills/claude-design/scripts/collect_source.py +178 -0
- package/skills/claude-design/scripts/drive_claude_design.cjs +378 -0
- package/skills/claude-design/scripts/gather_references.py +437 -0
- package/web/references/91app/DESIGN.md +151 -0
- package/web/references/airtable/DESIGN.md +16 -2
- package/web/references/bithumb/DESIGN.md +170 -0
- package/web/references/bunjang/DESIGN.md +20 -1
- package/web/references/cakeresume/DESIGN.md +162 -0
- package/web/references/catchtable/DESIGN.md +19 -0
- package/web/references/classting/DESIGN.md +251 -0
- package/web/references/classum/DESIGN.md +19 -0
- package/web/references/coinone/DESIGN.md +218 -0
- package/web/references/dabang/DESIGN.md +19 -0
- package/web/references/devsisters/DESIGN.md +253 -0
- package/web/references/dji/DESIGN.md +0 -1
- package/web/references/drnow/DESIGN.md +331 -0
- package/web/references/fastcampus/DESIGN.md +19 -0
- package/web/references/flex/DESIGN.md +19 -0
- package/web/references/flo/DESIGN.md +306 -0
- package/web/references/fugle/DESIGN.md +250 -0
- package/web/references/gmarket/DESIGN.md +19 -0
- package/web/references/gogolook/DESIGN.md +131 -0
- package/web/references/grip/DESIGN.md +250 -0
- package/web/references/hahow/DESIGN.md +158 -0
- package/web/references/hogangnono/DESIGN.md +308 -0
- package/web/references/hyundaicard/DESIGN.md +177 -0
- package/web/references/inflearn/DESIGN.md +19 -0
- package/web/references/jkopay/DESIGN.md +249 -0
- package/web/references/jobkorea/DESIGN.md +310 -0
- package/web/references/kbank/DESIGN.md +18 -0
- package/web/references/kdan/DESIGN.md +160 -0
- package/web/references/kkbox/DESIGN.md +114 -0
- package/web/references/krafton/DESIGN.md +230 -0
- package/web/references/kream/DESIGN.md +18 -0
- package/web/references/laftel/DESIGN.md +253 -0
- package/web/references/lezhin/DESIGN.md +301 -0
- package/web/references/lunit/DESIGN.md +19 -0
- package/web/references/melon/DESIGN.md +153 -0
- package/web/references/momoshop/DESIGN.md +279 -0
- package/web/references/mustit/DESIGN.md +282 -0
- package/web/references/nhncloud/DESIGN.md +174 -0
- package/web/references/oliveyoung/DESIGN.md +19 -0
- package/web/references/payco/DESIGN.md +227 -0
- package/web/references/piccollage/DESIGN.md +277 -0
- package/web/references/rayark/DESIGN.md +132 -0
- package/web/references/riiid/DESIGN.md +228 -0
- package/web/references/sendbird/DESIGN.md +285 -0
- package/web/references/socar/DESIGN.md +18 -0
- package/web/references/toss-securities/DESIGN.md +19 -0
- package/web/references/trenbe/DESIGN.md +252 -0
- package/web/references/tving/DESIGN.md +18 -0
- package/web/references/upbit/DESIGN.md +19 -0
- package/web/references/upstage/DESIGN.md +18 -0
- package/web/references/velog/DESIGN.md +168 -0
- package/web/references/voicetube/DESIGN.md +227 -0
- package/web/references/wadiz/DESIGN.md +19 -0
- package/web/references/webflow/DESIGN.md +16 -2
- package/web/references/yeogiotte/DESIGN.md +19 -0
- package/data/architecture-proposals/2026-05-13-thin-install-fresh-fetch.md +0 -189
- package/data/issues/2026-05-13-multi-surface-schema-rfc.md +0 -67
- package/data/reference-audits/2026-05-13-kr10.md +0 -132
- package/data/reference-audits/2026-05-14-kr10.md +0 -72
- package/data/reference-audits/2026-05-15-kr10.md +0 -124
- package/data/research/2026-05-18-agent-landscape.md +0 -69
- package/data/research/2026-05-18-kr-style-presets.md +0 -572
- 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
|
-
|
|
158
|
-
###
|
|
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)
|