@hadoku/themes 0.0.0-stage → 5.12.15
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/LICENSE-lucide +43 -0
- package/README.md +433 -2
- package/THEME_USAGE_GUIDE.md +308 -0
- package/dist/HadokuThemeRoot.d.ts +53 -0
- package/dist/HadokuThemeRoot.js +66 -0
- package/dist/ThemeIcons.d.ts +31 -0
- package/dist/ThemeIcons.js +91 -0
- package/dist/effects.d.ts +25 -0
- package/dist/effects.js +25 -0
- package/dist/icons/Icon.d.ts +36 -0
- package/dist/icons/Icon.js +89 -0
- package/dist/icons/index.d.ts +61 -0
- package/dist/icons/index.js +87 -0
- package/dist/icons/registry.generated.d.ts +158 -0
- package/dist/icons/registry.generated.js +446 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +169 -0
- package/dist/metadata.d.ts +35 -0
- package/dist/metadata.js +108 -0
- package/dist/platform/index.d.ts +94 -0
- package/dist/platform/index.js +149 -0
- package/dist/platform/usePlatform.d.ts +29 -0
- package/dist/platform/usePlatform.js +65 -0
- package/dist/prefsApiBase.d.ts +37 -0
- package/dist/prefsApiBase.js +40 -0
- package/dist/theme-types.d.ts +34 -0
- package/dist/theme-types.js +1 -0
- package/dist/themeContext.d.ts +62 -0
- package/dist/themeContext.js +70 -0
- package/dist/themePrefs.d.ts +29 -0
- package/dist/themePrefs.js +48 -0
- package/dist/useRefElement.d.ts +19 -0
- package/dist/useRefElement.js +52 -0
- package/dist/useTheme.d.ts +46 -0
- package/dist/useTheme.js +216 -0
- package/package.json +83 -4
- package/scripts/audit-tokens.mjs +126 -0
- package/scripts/check-contrast.mjs +24 -0
- package/scripts/check-docs.mjs +53 -0
- package/scripts/check-icons.mjs +473 -0
- package/scripts/check-usage.mjs +297 -0
- package/scripts/codemod-icons.mjs +396 -0
- package/scripts/generate-icons.mjs +199 -0
- package/scripts/generate-preview.mjs +374 -0
- package/scripts/generate-tailwind-colors.mjs +109 -0
- package/scripts/lib/color.mjs +184 -0
- package/scripts/lib/contract.d.mts +58 -0
- package/scripts/lib/contract.mjs +348 -0
- package/scripts/lib/derive.mjs +324 -0
- package/scripts/lib/icon-registry.mjs +92 -0
- package/scripts/lib/parse-themes.mjs +19 -0
- package/scripts/lib/tokens.mjs +188 -0
- package/scripts/normalize-tokens.mjs +161 -0
- package/src/advanced.css +218 -0
- package/src/icons/Icon.tsx +151 -0
- package/src/icons/emoji-map.json +202 -0
- package/src/icons/index.ts +151 -0
- package/src/icons/registry.generated.ts +455 -0
- package/src/icons/sources.json +150 -0
- package/src/icons.css +145 -0
- package/src/style.css +1421 -0
- package/src/tailwind-colors.css +88 -0
- package/src/tailwind-integration.css +49 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": [
|
|
3
|
+
"CANONICAL emoji -> icon name. The migration's single source of truth.",
|
|
4
|
+
"",
|
|
5
|
+
"This exists so `📋` resolves to `clipboard` in every repo instead of `clipboard`",
|
|
6
|
+
"in one and `paste` in the next. Twelve repos migrating independently would each",
|
|
7
|
+
"re-decide these, and the drift would be invisible until two apps sat side by side",
|
|
8
|
+
"on the frontpage showing different glyphs for the same idea.",
|
|
9
|
+
"",
|
|
10
|
+
"`check-icons` reads this to SUGGEST the replacement in its findings, which is what",
|
|
11
|
+
"turns each one from a decision into an edit. Keys are matched with variation",
|
|
12
|
+
"selectors stripped, so `⚙` and `⚙️` both land on `gear`.",
|
|
13
|
+
"",
|
|
14
|
+
"Every value must exist in sources.json — check-icons fails loudly if one does not."
|
|
15
|
+
],
|
|
16
|
+
|
|
17
|
+
"$comment_ambiguous": [
|
|
18
|
+
"A FLAT MAP CANNOT RESOLVE THESE. One emoji, two right answers depending on what",
|
|
19
|
+
"sits next to it, so `$ambiguous` names them and both tools defer to a human:",
|
|
20
|
+
"the codemod refuses them outright, and check-icons prints the caveat under its",
|
|
21
|
+
"suggestion instead of presenting one arm of a pair as settled.",
|
|
22
|
+
"",
|
|
23
|
+
"`◀`/`▶` is the case that proved it. `▶` lives in the transport row below with",
|
|
24
|
+
"`⏸`/`⏭`/`⏮` because a play button really is `play` — hadoku-dataplatform's",
|
|
25
|
+
"VodPlayer renders `isPlaying ? <Icon name=\"pause\"/> : <Icon name=\"play\"/>`.",
|
|
26
|
+
"But `◀`/`▶` is just as often a mirrored nav pair, and there applying the map",
|
|
27
|
+
"arm by arm yields an outline chevron facing a filled triangle. That is exactly",
|
|
28
|
+
"how promptsmith shipped a mismatched tournament row, and how the same repo's",
|
|
29
|
+
"PartBadge `◀ ▶` had to be hand-corrected to chevron-left/chevron-right.",
|
|
30
|
+
"",
|
|
31
|
+
"Neither value can move without breaking the other site, so the ambiguity is",
|
|
32
|
+
"recorded here rather than papered over with a coin-flip default."
|
|
33
|
+
],
|
|
34
|
+
"$ambiguous": {
|
|
35
|
+
"◀": "in a mirrored `◀ … ▶` pair use chevron-left WITH chevron-right — never pair it with play",
|
|
36
|
+
"▶": "play ONLY as a transport control beside ⏸; in a mirrored `◀ … ▶` pair use chevron-right"
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
"$comment_catalogue": "hadoku_site/spec/categories.json — the frontpage catalogue.",
|
|
40
|
+
"👋": "wave",
|
|
41
|
+
"📄": "document",
|
|
42
|
+
"📧": "mail",
|
|
43
|
+
"✉": "mail",
|
|
44
|
+
"🎨": "palette",
|
|
45
|
+
"🖨": "printer",
|
|
46
|
+
"✨": "sparkles",
|
|
47
|
+
"🎭": "masks",
|
|
48
|
+
"🔧": "wrench",
|
|
49
|
+
"📡": "antenna",
|
|
50
|
+
"🔍": "magnifier",
|
|
51
|
+
"🔎": "magnifier",
|
|
52
|
+
"💼": "briefcase",
|
|
53
|
+
"⚙": "gear",
|
|
54
|
+
"🖐": "helping-hand",
|
|
55
|
+
"✅": "check",
|
|
56
|
+
"✔": "tick",
|
|
57
|
+
"☑": "tick",
|
|
58
|
+
"🎬": "clapper",
|
|
59
|
+
"🍿": "popcorn",
|
|
60
|
+
"📼": "videotape",
|
|
61
|
+
|
|
62
|
+
"$comment_status": "State and feedback.",
|
|
63
|
+
"⚠": "warning",
|
|
64
|
+
"❌": "error",
|
|
65
|
+
"✖": "close",
|
|
66
|
+
"❎": "error",
|
|
67
|
+
"ℹ": "info",
|
|
68
|
+
"⏳": "hourglass",
|
|
69
|
+
"⌛": "hourglass",
|
|
70
|
+
"⏰": "clock",
|
|
71
|
+
"🕐": "clock",
|
|
72
|
+
"💤": "sleep",
|
|
73
|
+
"⚪": "circle",
|
|
74
|
+
"🔴": "circle-dot",
|
|
75
|
+
"🟢": "circle-dot",
|
|
76
|
+
"🟡": "circle-dot",
|
|
77
|
+
"🔵": "circle-dot",
|
|
78
|
+
"➖": "minus",
|
|
79
|
+
"🚨": "warning",
|
|
80
|
+
"🔥": "fire",
|
|
81
|
+
"💥": "warning",
|
|
82
|
+
"🎉": "party",
|
|
83
|
+
"🏆": "trophy",
|
|
84
|
+
"⭐": "star",
|
|
85
|
+
"🌟": "star",
|
|
86
|
+
"👍": "thumbs-up",
|
|
87
|
+
"👎": "thumbs-down",
|
|
88
|
+
"❤": "heart",
|
|
89
|
+
"🩷": "heart",
|
|
90
|
+
"🖤": "heart",
|
|
91
|
+
|
|
92
|
+
"$comment_actions": "Verbs — buttons and controls.",
|
|
93
|
+
"🗑": "trash",
|
|
94
|
+
"✏": "edit",
|
|
95
|
+
"📝": "note",
|
|
96
|
+
"📋": "clipboard",
|
|
97
|
+
"📌": "pin",
|
|
98
|
+
"🔄": "refresh",
|
|
99
|
+
"♻": "refresh",
|
|
100
|
+
"▶": "play",
|
|
101
|
+
"⏸": "pause",
|
|
102
|
+
"⏭": "next",
|
|
103
|
+
"⏮": "previous",
|
|
104
|
+
"⏹": "stop",
|
|
105
|
+
"🔀": "shuffle",
|
|
106
|
+
"🔗": "link",
|
|
107
|
+
"⛓": "link",
|
|
108
|
+
"👁": "eye",
|
|
109
|
+
"🔒": "lock",
|
|
110
|
+
"🔑": "key",
|
|
111
|
+
"🛡": "shield",
|
|
112
|
+
"🧹": "broom",
|
|
113
|
+
"🪄": "wand",
|
|
114
|
+
"🔔": "bell",
|
|
115
|
+
"🚩": "flag",
|
|
116
|
+
"🏷": "tag",
|
|
117
|
+
"🔖": "bookmark",
|
|
118
|
+
"📤": "upload",
|
|
119
|
+
"⬆": "arrow-up",
|
|
120
|
+
"📥": "download",
|
|
121
|
+
"⬇": "arrow-down",
|
|
122
|
+
"➡": "arrow-right",
|
|
123
|
+
"⬅": "arrow-left",
|
|
124
|
+
"↔": "arrows-horizontal",
|
|
125
|
+
"↗": "arrow-up-right",
|
|
126
|
+
"↩": "return",
|
|
127
|
+
"⤴": "branch-up",
|
|
128
|
+
"◀": "chevron-left",
|
|
129
|
+
"🔼": "chevron-up",
|
|
130
|
+
"🔽": "chevron-down",
|
|
131
|
+
"➕": "plus",
|
|
132
|
+
"📎": "copy",
|
|
133
|
+
"💾": "save",
|
|
134
|
+
"📨": "send",
|
|
135
|
+
|
|
136
|
+
"$comment_objects": "Nouns — things a screen is about.",
|
|
137
|
+
"📁": "folder",
|
|
138
|
+
"📂": "folder",
|
|
139
|
+
"📦": "package",
|
|
140
|
+
"📅": "calendar",
|
|
141
|
+
"🗓": "calendar",
|
|
142
|
+
"🖼": "image",
|
|
143
|
+
"📷": "camera",
|
|
144
|
+
"🎞": "film",
|
|
145
|
+
"🎵": "music",
|
|
146
|
+
"🎧": "music",
|
|
147
|
+
"🔊": "volume-high",
|
|
148
|
+
"🔈": "volume-low",
|
|
149
|
+
"🔇": "volume-mute",
|
|
150
|
+
"💬": "chat",
|
|
151
|
+
"📖": "book",
|
|
152
|
+
"📚": "book",
|
|
153
|
+
"🤝": "handshake",
|
|
154
|
+
"🤖": "robot",
|
|
155
|
+
"👤": "user",
|
|
156
|
+
"👥": "users",
|
|
157
|
+
"👑": "crown",
|
|
158
|
+
"💀": "skull",
|
|
159
|
+
"🐈": "cat",
|
|
160
|
+
"🦴": "bone",
|
|
161
|
+
"🎮": "gamepad",
|
|
162
|
+
"🧰": "toolbox",
|
|
163
|
+
"🧪": "flask",
|
|
164
|
+
"🏥": "hospital",
|
|
165
|
+
"🌐": "globe",
|
|
166
|
+
"🗺": "map",
|
|
167
|
+
"📊": "chart",
|
|
168
|
+
"📈": "chart",
|
|
169
|
+
"📉": "chart",
|
|
170
|
+
"📐": "crop",
|
|
171
|
+
"🖥": "desktop",
|
|
172
|
+
"💻": "desktop",
|
|
173
|
+
"🗄": "database",
|
|
174
|
+
"🗃": "database",
|
|
175
|
+
"⌨": "terminal",
|
|
176
|
+
"🧱": "layers",
|
|
177
|
+
"🎯": "target",
|
|
178
|
+
"🚀": "rocket",
|
|
179
|
+
"⚡": "zap",
|
|
180
|
+
"🧭": "compass",
|
|
181
|
+
"🏠": "home",
|
|
182
|
+
"🏡": "home",
|
|
183
|
+
"📭": "inbox",
|
|
184
|
+
"📬": "inbox",
|
|
185
|
+
"📮": "inbox",
|
|
186
|
+
"☀": "sun",
|
|
187
|
+
"🌙": "moon",
|
|
188
|
+
"🧑": "user",
|
|
189
|
+
"🙋": "wave",
|
|
190
|
+
"💡": "zap",
|
|
191
|
+
"🔦": "magnifier",
|
|
192
|
+
"📏": "crop",
|
|
193
|
+
"✂": "scissors",
|
|
194
|
+
"🩹": "wrench",
|
|
195
|
+
"🔨": "wrench",
|
|
196
|
+
"🛠": "toolbox",
|
|
197
|
+
"⚔": "toolbox",
|
|
198
|
+
"🏹": "target",
|
|
199
|
+
"🔱": "layers",
|
|
200
|
+
"🚫": "ban",
|
|
201
|
+
"⛔": "ban"
|
|
202
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The enforced icon set — framework-free half.
|
|
3
|
+
*
|
|
4
|
+
* Every consumer references icons BY NAME from `IconName`. Nobody inlines their own
|
|
5
|
+
* SVG: an icon that is not in the registry does not exist, which is what keeps 16 apps
|
|
6
|
+
* rendering the same pictograph for the same idea. TS consumers get that as a type
|
|
7
|
+
* error; everyone else gets it from `scripts/check-icons.mjs`.
|
|
8
|
+
*
|
|
9
|
+
* This module has no React import on purpose. The frontpage POCs are Astro (one Qwik)
|
|
10
|
+
* and cannot mount a React component just to draw a 16px glyph, so the string path is
|
|
11
|
+
* the primary API and `<Icon>` is a convenience on top of it.
|
|
12
|
+
*/
|
|
13
|
+
import { ICON_MARKUP } from './registry.generated'
|
|
14
|
+
import type { IconName } from './registry.generated'
|
|
15
|
+
|
|
16
|
+
export { ICON_MARKUP, ICON_NAMES, ICON_SOURCE_SLUGS, LUCIDE_VERSION } from './registry.generated'
|
|
17
|
+
export type { IconName } from './registry.generated'
|
|
18
|
+
|
|
19
|
+
/** The five accent families. Same set as the colour tokens — no icon-only families. */
|
|
20
|
+
export const ICON_FAMILIES = ['primary', 'success', 'warning', 'danger', 'neutral'] as const
|
|
21
|
+
export type IconFamily = (typeof ICON_FAMILIES)[number]
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* How the icon is coloured. Every option resolves through theme tokens — there is
|
|
25
|
+
* no way to pass a raw colour — but WHICH treatment to use is the consumer's call.
|
|
26
|
+
*
|
|
27
|
+
* - `bare` — glyph inherits `currentColor`. The default. Use it inside a button
|
|
28
|
+
* or beside a label and the icon matches that text automatically, in
|
|
29
|
+
* every theme, with no contrast obligation of its own.
|
|
30
|
+
* - `accent` — glyph painted `--color-<family>`. For when the icon is its own
|
|
31
|
+
* element and should NOT match the surrounding text. You own the
|
|
32
|
+
* contrast: a bare accent glyph clears 3:1 on a card/page surface in
|
|
33
|
+
* 28 of 36 theme/surface combinations for `warning`, so this is right
|
|
34
|
+
* when you know the surface and wrong as a blanket default.
|
|
35
|
+
* - `tint` — glyph in a `--color-<f>-bg` tile with `--color-on-<f>-bg` ink.
|
|
36
|
+
* - `filled` — glyph in a `--color-<f>` tile with `--color-on-<f>` ink.
|
|
37
|
+
*
|
|
38
|
+
* `tint` and `filled` clear 3:1 in all 18 themes for all five families.
|
|
39
|
+
*/
|
|
40
|
+
export type IconVariant = 'bare' | 'accent' | 'tint' | 'filled'
|
|
41
|
+
|
|
42
|
+
export function isIconName(value: unknown): value is IconName {
|
|
43
|
+
return typeof value === 'string' && Object.prototype.hasOwnProperty.call(ICON_MARKUP, value)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ESCAPES: Record<string, string> = {
|
|
47
|
+
'&': '&',
|
|
48
|
+
'<': '<',
|
|
49
|
+
'>': '>',
|
|
50
|
+
'"': '"',
|
|
51
|
+
"'": '''
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Escape a caller-supplied string before it goes into the markup we return. */
|
|
55
|
+
function escapeHtml(value: string): string {
|
|
56
|
+
return value.replace(/[&<>"']/g, c => ESCAPES[c])
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface IconSvgOptions {
|
|
60
|
+
/** CSS length. Defaults to `1em` so the glyph scales with its surrounding text. */
|
|
61
|
+
size?: string | number
|
|
62
|
+
/** Extra classes appended after `hdk-icon`. */
|
|
63
|
+
className?: string
|
|
64
|
+
/**
|
|
65
|
+
* Paint the glyph with `--color-<family>` instead of inheriting `currentColor`.
|
|
66
|
+
* Adds `hdk-icon--<family>`; requires icons.css. Omit to inherit.
|
|
67
|
+
*/
|
|
68
|
+
family?: IconFamily
|
|
69
|
+
/**
|
|
70
|
+
* Accessible name. Omit for decorative icons (the default) — those are rendered
|
|
71
|
+
* `aria-hidden` so a screen reader does not announce a glyph that repeats an
|
|
72
|
+
* adjacent label. Supply it only when the icon is the sole carrier of meaning.
|
|
73
|
+
*/
|
|
74
|
+
title?: string
|
|
75
|
+
/** Stroke width in the 24x24 viewBox. Lucide's default is 2. */
|
|
76
|
+
strokeWidth?: number
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A complete, standalone `<svg>` string for one icon.
|
|
81
|
+
*
|
|
82
|
+
* Astro/Qwik/plain HTML, no client JS:
|
|
83
|
+
* <span set:html={getIconSvg('popcorn')} />
|
|
84
|
+
* <span class="hdk-icon-tile hdk-icon-tile--tint hdk-icon-tile--warning"
|
|
85
|
+
* set:html={getIconSvg('popcorn')} />
|
|
86
|
+
*/
|
|
87
|
+
export function getIconSvg(name: IconName, options: IconSvgOptions = {}): string {
|
|
88
|
+
const markup = ICON_MARKUP[name]
|
|
89
|
+
if (markup === undefined) {
|
|
90
|
+
// Loud rather than silent: a blank space where an icon should be is the failure
|
|
91
|
+
// mode that ships to production unnoticed.
|
|
92
|
+
throw new Error(
|
|
93
|
+
`getIconSvg: "${String(name)}" is not in the icon registry. ` +
|
|
94
|
+
`Add it to themes/src/icons/sources.json and run \`pnpm run generate:icons\`.`
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
const { size = '1em', className, title, strokeWidth = 2, family } = options
|
|
98
|
+
const dim = typeof size === 'number' ? `${size}px` : size
|
|
99
|
+
const cls = ['hdk-icon', family ? `hdk-icon--${family}` : '', className ?? '']
|
|
100
|
+
.filter(Boolean)
|
|
101
|
+
.join(' ')
|
|
102
|
+
const label = title
|
|
103
|
+
? `role="img" aria-label="${escapeHtml(title)}"`
|
|
104
|
+
: 'aria-hidden="true" focusable="false"'
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
`<svg xmlns="http://www.w3.org/2000/svg" class="${escapeHtml(cls)}" ` +
|
|
108
|
+
`width="${escapeHtml(dim)}" height="${escapeHtml(dim)}" viewBox="0 0 24 24" ` +
|
|
109
|
+
`fill="none" stroke="currentColor" stroke-width="${strokeWidth}" ` +
|
|
110
|
+
`stroke-linecap="round" stroke-linejoin="round" ${label}>` +
|
|
111
|
+
(title ? `<title>${escapeHtml(title)}</title>` : '') +
|
|
112
|
+
markup +
|
|
113
|
+
'</svg>'
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Class list for an accent tile, for the framework-free path. */
|
|
118
|
+
export function getIconTileClass(
|
|
119
|
+
family: IconFamily = 'warning',
|
|
120
|
+
variant: Exclude<IconVariant, 'bare'> = 'tint',
|
|
121
|
+
round = false
|
|
122
|
+
): string {
|
|
123
|
+
return (
|
|
124
|
+
`hdk-icon-tile hdk-icon-tile--${variant} hdk-icon-tile--${family}` +
|
|
125
|
+
(round ? ' hdk-icon-tile--round' : '')
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Canonical emoji -> icon name. Generated into registry.generated.ts from
|
|
130
|
+
// emoji-map.json, so nothing loads JSON at runtime and there is one source.
|
|
131
|
+
import { EMOJI_TO_ICON } from './registry.generated'
|
|
132
|
+
|
|
133
|
+
export { EMOJI_TO_ICON } from './registry.generated'
|
|
134
|
+
|
|
135
|
+
/** Variation selectors and skin tones are presentation, not identity. */
|
|
136
|
+
function stripPresentation(s: string): string {
|
|
137
|
+
return [...s]
|
|
138
|
+
.filter(c => {
|
|
139
|
+
const p = c.codePointAt(0) ?? 0
|
|
140
|
+
return p !== 0xfe0f && p !== 0xfe0e && !(p >= 0x1f3fb && p <= 0x1f3ff)
|
|
141
|
+
})
|
|
142
|
+
.join('')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The registry name a raw emoji should become, or undefined if it has no
|
|
147
|
+
* canonical answer. `⚙` and `⚙️` resolve identically.
|
|
148
|
+
*/
|
|
149
|
+
export function emojiToIconName(emoji: string): IconName | undefined {
|
|
150
|
+
return EMOJI_TO_ICON[stripPresentation(emoji)] ?? EMOJI_TO_ICON[emoji]
|
|
151
|
+
}
|