@hadoku/themes 0.0.0-stage → 5.12.16

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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSE-lucide +43 -0
  3. package/README.md +433 -2
  4. package/THEME_USAGE_GUIDE.md +308 -0
  5. package/dist/HadokuThemeRoot.d.ts +53 -0
  6. package/dist/HadokuThemeRoot.js +66 -0
  7. package/dist/ThemeIcons.d.ts +31 -0
  8. package/dist/ThemeIcons.js +91 -0
  9. package/dist/effects.d.ts +25 -0
  10. package/dist/effects.js +25 -0
  11. package/dist/icons/Icon.d.ts +36 -0
  12. package/dist/icons/Icon.js +89 -0
  13. package/dist/icons/index.d.ts +61 -0
  14. package/dist/icons/index.js +87 -0
  15. package/dist/icons/registry.generated.d.ts +158 -0
  16. package/dist/icons/registry.generated.js +446 -0
  17. package/dist/index.d.ts +82 -0
  18. package/dist/index.js +169 -0
  19. package/dist/metadata.d.ts +35 -0
  20. package/dist/metadata.js +108 -0
  21. package/dist/platform/index.d.ts +94 -0
  22. package/dist/platform/index.js +149 -0
  23. package/dist/platform/usePlatform.d.ts +29 -0
  24. package/dist/platform/usePlatform.js +65 -0
  25. package/dist/prefsApiBase.d.ts +37 -0
  26. package/dist/prefsApiBase.js +40 -0
  27. package/dist/theme-types.d.ts +34 -0
  28. package/dist/theme-types.js +1 -0
  29. package/dist/themeContext.d.ts +62 -0
  30. package/dist/themeContext.js +70 -0
  31. package/dist/themePrefs.d.ts +29 -0
  32. package/dist/themePrefs.js +48 -0
  33. package/dist/useRefElement.d.ts +19 -0
  34. package/dist/useRefElement.js +52 -0
  35. package/dist/useTheme.d.ts +46 -0
  36. package/dist/useTheme.js +216 -0
  37. package/package.json +83 -4
  38. package/scripts/audit-tokens.mjs +126 -0
  39. package/scripts/check-contrast.mjs +24 -0
  40. package/scripts/check-docs.mjs +53 -0
  41. package/scripts/check-icons.mjs +473 -0
  42. package/scripts/check-usage.mjs +297 -0
  43. package/scripts/codemod-icons.mjs +396 -0
  44. package/scripts/generate-icons.mjs +199 -0
  45. package/scripts/generate-preview.mjs +374 -0
  46. package/scripts/generate-tailwind-colors.mjs +109 -0
  47. package/scripts/lib/color.mjs +184 -0
  48. package/scripts/lib/contract.d.mts +58 -0
  49. package/scripts/lib/contract.mjs +348 -0
  50. package/scripts/lib/derive.mjs +324 -0
  51. package/scripts/lib/icon-registry.mjs +92 -0
  52. package/scripts/lib/parse-themes.mjs +19 -0
  53. package/scripts/lib/tokens.mjs +188 -0
  54. package/scripts/normalize-tokens.mjs +161 -0
  55. package/src/advanced.css +218 -0
  56. package/src/icons/Icon.tsx +151 -0
  57. package/src/icons/emoji-map.json +202 -0
  58. package/src/icons/index.ts +151 -0
  59. package/src/icons/registry.generated.ts +455 -0
  60. package/src/icons/sources.json +150 -0
  61. package/src/icons.css +145 -0
  62. package/src/style.css +1421 -0
  63. package/src/tailwind-colors.css +88 -0
  64. package/src/tailwind-integration.css +49 -0
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Rewrites themes/src/style.css to the canonical symmetric token set.
4
+ *
5
+ * Every theme block currently opens with exactly 38 contiguous `--color-*`
6
+ * declarations followed by shadow/spacing/advanced tokens. This replaces that
7
+ * leading span with the generated canonical set and leaves everything after
8
+ * it byte-identical.
9
+ *
10
+ * Idempotent: running it twice produces the same file.
11
+ *
12
+ * node themes/scripts/normalize-tokens.mjs [--dry]
13
+ */
14
+
15
+ import { readFileSync, writeFileSync } from 'node:fs'
16
+ import {
17
+ STYLE_PATH,
18
+ readBlocks,
19
+ themesForSelector,
20
+ readDeclarations,
21
+ FAMILIES
22
+ } from './lib/parse-themes.mjs'
23
+ import { deriveTheme } from './lib/derive.mjs'
24
+
25
+ const dry = process.argv.includes('--dry')
26
+ const css = readFileSync(STYLE_PATH, 'utf8')
27
+ const eol = css.includes('\r\n') ? '\r\n' : '\n'
28
+
29
+ const FAMILY_LABEL = {
30
+ primary: 'Primary',
31
+ success: 'Success',
32
+ warning: 'Warning',
33
+ danger: 'Danger',
34
+ neutral: 'Neutral'
35
+ }
36
+
37
+ /** Render the canonical color declarations for one theme, with section comments. */
38
+ function renderColors(tokens, indent) {
39
+ const lines = []
40
+ const push = s => lines.push(s ? indent + s : '')
41
+
42
+ for (const f of FAMILIES) {
43
+ push(`/* ${FAMILY_LABEL[f]} */`)
44
+ push(`--color-${f}: ${tokens[`--color-${f}`]};`)
45
+ push(`--color-${f}-dark: ${tokens[`--color-${f}-dark`]};`)
46
+ push(`--color-${f}-bg: ${tokens[`--color-${f}-bg`]};`)
47
+ push(`--color-${f}-hover: ${tokens[`--color-${f}-hover`]};`)
48
+ push(`--color-on-${f}: ${tokens[`--color-on-${f}`]};`)
49
+ push(`--color-on-${f}-bg: ${tokens[`--color-on-${f}-bg`]};`)
50
+ push('')
51
+ }
52
+
53
+ push('/* Text */')
54
+ for (const n of [
55
+ '--color-text',
56
+ '--color-text-secondary',
57
+ '--color-text-tertiary',
58
+ '--color-text-muted'
59
+ ]) {
60
+ push(`${n}: ${tokens[n]};`)
61
+ }
62
+ push('')
63
+ push('/* Borders */')
64
+ for (const n of ['--color-border', '--color-border-light']) push(`${n}: ${tokens[n]};`)
65
+ push('')
66
+ push('/* Backgrounds */')
67
+ for (const n of [
68
+ '--color-bg',
69
+ '--color-bg-card',
70
+ '--color-bg-alt',
71
+ '--color-bg-hover',
72
+ '--color-bg-overlay'
73
+ ]) {
74
+ push(`${n}: ${tokens[n]};`)
75
+ }
76
+
77
+ return lines.join(eol)
78
+ }
79
+
80
+ /* Collect edits first, apply back-to-front so earlier offsets stay valid. */
81
+ const edits = []
82
+ /* Values that changed for a reason other than a verbatim rename. */
83
+ const changed = []
84
+
85
+ for (const block of readBlocks(css)) {
86
+ const names = themesForSelector(block.selector)
87
+ if (!names.length) continue
88
+
89
+ const decls = readDeclarations(block.body)
90
+ const colorDecls = decls.filter(d => d.name.startsWith('--color-'))
91
+ if (!colorDecls.length) continue
92
+
93
+ const tokens = Object.fromEntries(decls.map(d => [d.name, d.value]))
94
+ const notes = []
95
+ const canonical = deriveTheme(tokens, notes)
96
+ for (const n of notes) changed.push({ theme: names[0], ...n })
97
+
98
+ // The color span covers whole lines: from the line holding the first
99
+ // `--color-` declaration through the line closing the last one.
100
+ const body = block.body
101
+ const firstAt = body.indexOf('--color-')
102
+ const lastName = colorDecls[colorDecls.length - 1].name
103
+ const endInBody = body.indexOf(';', body.lastIndexOf(lastName)) + 1
104
+
105
+ const lines = body.split(/\r?\n/)
106
+ const offsetOfLine = []
107
+ for (let i = 0, at = 0; i < lines.length; i += 1) {
108
+ offsetOfLine.push(at)
109
+ at += lines[i].length + eol.length
110
+ }
111
+ const lineOf = offset => {
112
+ let n = 0
113
+ while (n + 1 < offsetOfLine.length && offsetOfLine[n + 1] <= offset) n += 1
114
+ return n
115
+ }
116
+
117
+ let firstLine = lineOf(firstAt)
118
+ const lastLine = lineOf(endInBody - 1)
119
+
120
+ // Section comments inside the span get regenerated, but the first one sits
121
+ // on the line above it — pull it in so it isn't left orphaned.
122
+ if (firstLine > 0 && /^\s*\/\*[^*]*\*\/\s*$/.test(lines[firstLine - 1])) firstLine -= 1
123
+
124
+ const indent = lines[lineOf(firstAt)].match(/^[ \t]*/)[0]
125
+
126
+ edits.push({
127
+ start: block.start + offsetOfLine[firstLine],
128
+ end: block.start + endInBody,
129
+ text: renderColors(canonical, indent),
130
+ theme: names.join('+')
131
+ })
132
+ }
133
+
134
+ let out = css
135
+ for (const e of edits.sort((a, b) => b.start - a.start)) {
136
+ out = out.slice(0, e.start) + e.text + out.slice(e.end)
137
+ }
138
+
139
+ console.log(`Rewrote ${edits.length} theme blocks.`)
140
+
141
+ if (changed.length) {
142
+ console.log(
143
+ `\n${changed.length} value(s) adjusted for contrast (everything else carried over verbatim):`
144
+ )
145
+ for (const c of changed) {
146
+ const why =
147
+ c.reason === 'contrast'
148
+ ? 'authored value broke the on-<f> contract'
149
+ : c.reason === 'separation'
150
+ ? 'surface step under its floor'
151
+ : `dead-zone nudge, ΔL -${c.shift.toFixed(3)}`
152
+ console.log(` ${c.theme.padEnd(17)} ${c.token.padEnd(24)} ${c.from} → ${c.to} (${why})`)
153
+ }
154
+ }
155
+
156
+ if (dry) {
157
+ console.log('--dry: not writing.')
158
+ } else {
159
+ writeFileSync(STYLE_PATH, out)
160
+ console.log(`Wrote ${STYLE_PATH}`)
161
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Hadoku Themes — Advanced Visual Kit
3
+ *
4
+ * Class hooks for themes that ship an advanced visual contract
5
+ * (gradient + animated effects). Per-theme tokens live inside that
6
+ * theme's [data-theme='X'] block in style.css; this file is the
7
+ * shared rendering kit.
8
+ *
9
+ * Mode is controlled by data-theme-mode='simple' | 'advanced':
10
+ * - 'simple' (default): flatten to the theme's solid colors
11
+ * - 'advanced': render the gradient + effects (opt-in)
12
+ *
13
+ * Class hooks (consumer applies these in JSX):
14
+ * .hdk-advanced-page — viewport-fixed gradient page background
15
+ * .hdk-advanced-surface — "window into the gradient" element
16
+ * .hdk-advanced-surface--shift — gradient displacement on hover
17
+ * .hdk-advanced-surface--cascade-rich — multi-wave shimmer
18
+ * .hdk-advanced-surface--sat-rich — saturation + cascade
19
+ *
20
+ * When --advanced-gradient is undefined (theme has no advanced
21
+ * contract), the surfaces fall back to their flat backgrounds so the
22
+ * classes are safe to apply unconditionally.
23
+ */
24
+
25
+ /* ---------- Page surface ---------- */
26
+
27
+ .hdk-advanced-page {
28
+ background: var(--advanced-gradient, var(--color-bg));
29
+ background-attachment: fixed;
30
+ }
31
+
32
+ /* ---------- Card surface ---------- */
33
+ /* Each surface is its own stacking context so effect ::after layers
34
+ sit above the bg but below content without forcing children to set
35
+ z-index. */
36
+
37
+ .hdk-advanced-surface {
38
+ position: relative;
39
+ overflow: hidden;
40
+ background: var(--advanced-gradient, var(--color-bg-card));
41
+ background-attachment: fixed;
42
+ isolation: isolate;
43
+ }
44
+
45
+ /* ---------- Effect: SHIFT — gradient displacement on hover ---------- */
46
+
47
+ .hdk-advanced-surface--shift::after {
48
+ content: '';
49
+ position: absolute;
50
+ inset: 0;
51
+ border-radius: inherit;
52
+ background: var(--advanced-gradient);
53
+ background-attachment: scroll;
54
+ background-size: 130% 130%;
55
+ background-position: 30% 30%;
56
+ opacity: 0;
57
+ transition:
58
+ opacity 0.4s ease,
59
+ background-position 0.8s ease-out;
60
+ pointer-events: none;
61
+ z-index: -1;
62
+ }
63
+
64
+ .hdk-advanced-surface--shift:hover::after {
65
+ opacity: 1;
66
+ background-position: 70% 70%;
67
+ }
68
+
69
+ /* ---------- Effect: CASCADE-RICH — multi-wave, varied speeds ---------- */
70
+
71
+ .hdk-advanced-surface--cascade-rich::before {
72
+ content: '';
73
+ position: absolute;
74
+ inset: -80%;
75
+ background: linear-gradient(
76
+ 135deg,
77
+ transparent 38%,
78
+ rgba(255, 255, 255, 0.03) 44%,
79
+ rgba(255, 255, 255, 0.03) 48%,
80
+ transparent 54%
81
+ );
82
+ transform: translateX(-50%) translateY(-50%);
83
+ opacity: 0;
84
+ transition:
85
+ transform 1.2s ease-out,
86
+ opacity 0.2s ease;
87
+ pointer-events: none;
88
+ z-index: -1;
89
+ }
90
+
91
+ .hdk-advanced-surface--cascade-rich::after {
92
+ content: '';
93
+ position: absolute;
94
+ inset: -150%;
95
+ background: linear-gradient(
96
+ 135deg,
97
+ transparent 0%,
98
+ transparent 22%,
99
+ rgba(255, 255, 255, 0.05) 26%,
100
+ transparent 30%,
101
+ transparent 40%,
102
+ rgba(255, 255, 255, 0.12) 45%,
103
+ rgba(255, 255, 255, 0.18) 48%,
104
+ rgba(255, 255, 255, 0.12) 51%,
105
+ transparent 56%,
106
+ transparent 66%,
107
+ rgba(255, 255, 255, 0.06) 70%,
108
+ transparent 74%,
109
+ transparent 100%
110
+ );
111
+ transform: translateX(-45%) translateY(-45%);
112
+ opacity: 0;
113
+ transition:
114
+ transform 4s cubic-bezier(0.22, 0.61, 0.36, 1) 0.1s,
115
+ opacity 0.5s ease 0.1s;
116
+ pointer-events: none;
117
+ z-index: -1;
118
+ }
119
+
120
+ .hdk-advanced-surface--cascade-rich:hover::before {
121
+ transform: translateX(50%) translateY(50%);
122
+ opacity: 1;
123
+ }
124
+
125
+ .hdk-advanced-surface--cascade-rich:hover::after {
126
+ transform: translateX(45%) translateY(45%);
127
+ opacity: 1;
128
+ }
129
+
130
+ /* ---------- Effect: SAT-RICH — saturation boost + 4-band cascade ---------- */
131
+
132
+ .hdk-advanced-surface--sat-rich {
133
+ filter: saturate(1) brightness(1);
134
+ transition:
135
+ filter 0.6s ease-out,
136
+ box-shadow 0.6s ease-out;
137
+ }
138
+
139
+ .hdk-advanced-surface--sat-rich::after {
140
+ content: '';
141
+ position: absolute;
142
+ inset: -100%;
143
+ background: linear-gradient(
144
+ 135deg,
145
+ transparent 20%,
146
+ rgba(255, 255, 255, 0.03) 30%,
147
+ transparent 38%,
148
+ transparent 44%,
149
+ rgba(255, 255, 255, 0.1) 50%,
150
+ rgba(255, 255, 255, 0.16) 52%,
151
+ rgba(255, 255, 255, 0.1) 54%,
152
+ transparent 60%,
153
+ transparent 70%,
154
+ rgba(255, 255, 255, 0.04) 78%,
155
+ transparent 86%
156
+ );
157
+ transform: translateX(-50%) translateY(-50%);
158
+ opacity: 0;
159
+ transition:
160
+ transform 3s ease-out,
161
+ opacity 0.6s ease-out;
162
+ pointer-events: none;
163
+ z-index: -1;
164
+ }
165
+
166
+ .hdk-advanced-surface--sat-rich:hover {
167
+ filter: saturate(1.15) brightness(1.05);
168
+ box-shadow:
169
+ 0 4px 16px rgba(0, 0, 0, 0.1),
170
+ inset 0 0 0 1px rgba(255, 255, 255, 0.18);
171
+ }
172
+
173
+ .hdk-advanced-surface--sat-rich:hover::after {
174
+ transform: translateX(50%) translateY(50%);
175
+ opacity: 1;
176
+ }
177
+
178
+ /* ---------- Simple mode override ---------- */
179
+ /* Forces flat colors regardless of theme. The selectors target
180
+ common scope roots so the override applies whether the attribute
181
+ is set on documentElement, body, or a microfrontend container. */
182
+
183
+ [data-theme-mode='simple'] .hdk-advanced-page,
184
+ [data-theme-mode='simple'].hdk-advanced-page {
185
+ background: var(--color-bg);
186
+ background-attachment: scroll;
187
+ }
188
+
189
+ [data-theme-mode='simple'] .hdk-advanced-surface,
190
+ [data-theme-mode='simple'].hdk-advanced-surface {
191
+ background: var(--color-bg-card);
192
+ background-attachment: scroll;
193
+ }
194
+
195
+ [data-theme-mode='simple'] .hdk-advanced-surface::before,
196
+ [data-theme-mode='simple'] .hdk-advanced-surface::after,
197
+ [data-theme-mode='simple'].hdk-advanced-surface::before,
198
+ [data-theme-mode='simple'].hdk-advanced-surface::after {
199
+ display: none;
200
+ }
201
+
202
+ [data-theme-mode='simple'] .hdk-advanced-surface--sat-rich:hover,
203
+ [data-theme-mode='simple'].hdk-advanced-surface--sat-rich:hover {
204
+ filter: none;
205
+ box-shadow: none;
206
+ }
207
+
208
+ /* ---------- Reduced motion ---------- */
209
+
210
+ @media (prefers-reduced-motion: reduce) {
211
+ .hdk-advanced-surface--shift::after,
212
+ .hdk-advanced-surface--cascade-rich::before,
213
+ .hdk-advanced-surface--cascade-rich::after,
214
+ .hdk-advanced-surface--sat-rich,
215
+ .hdk-advanced-surface--sat-rich::after {
216
+ transition: none;
217
+ }
218
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * React `<Icon>` — a thin wrapper over the string registry.
3
+ *
4
+ * The glyph is emitted as real JSX children rather than dangerouslySetInnerHTML: the
5
+ * markup is ours and build-time-generated, but React apps run under the same strict
6
+ * CSP as the frontpage, and parsing 78 vendored strings at runtime buys nothing when
7
+ * the shapes can be elements. So the paths are parsed once, lazily, per icon actually
8
+ * rendered — an app that uses six icons never touches the other seventy-two.
9
+ */
10
+ import React from 'react'
11
+ import { ICON_MARKUP } from './registry.generated'
12
+ import type { IconName } from './registry.generated'
13
+ import type { IconFamily, IconVariant } from './index'
14
+
15
+ export interface IconProps {
16
+ name: IconName
17
+ /** CSS length. Defaults to `1em` so the glyph scales with surrounding text. */
18
+ size?: string | number
19
+ /**
20
+ * Which family token colours this icon. Applies to `accent` (the glyph itself)
21
+ * and to `tint`/`filled` (the tile). Ignored for `bare`, which inherits
22
+ * `currentColor` by design.
23
+ */
24
+ family?: IconFamily
25
+ /** `bare` (default), `accent`, `tint`, or `filled` — see IconVariant. */
26
+ variant?: IconVariant
27
+ /** Round the accent tile. No effect when `variant` is `bare`. */
28
+ round?: boolean
29
+ /**
30
+ * Accessible name. Omit for decorative icons — those render `aria-hidden` so a
31
+ * screen reader does not announce a glyph that repeats an adjacent text label.
32
+ */
33
+ title?: string
34
+ className?: string
35
+ /** Stroke width in the 24x24 viewBox. Lucide's default is 2. */
36
+ strokeWidth?: number
37
+ }
38
+
39
+ /** `<path d="..." />` etc. -> React elements. Parsed once per icon, then cached. */
40
+ const parsed = new Map<IconName, React.ReactNode[]>()
41
+
42
+ const SELF_CLOSING = /<([a-z]+)\s+([^>]*?)\/>/g
43
+ const ATTR = /([a-z-]+)="([^"]*)"/g
44
+
45
+ /** SVG presentation attributes we emit, mapped to their React prop names. */
46
+ const PROP_NAMES: Record<string, string> = {
47
+ d: 'd',
48
+ cx: 'cx',
49
+ cy: 'cy',
50
+ r: 'r',
51
+ x: 'x',
52
+ y: 'y',
53
+ x1: 'x1',
54
+ x2: 'x2',
55
+ y1: 'y1',
56
+ y2: 'y2',
57
+ rx: 'rx',
58
+ ry: 'ry',
59
+ width: 'width',
60
+ height: 'height',
61
+ points: 'points',
62
+ transform: 'transform',
63
+ fill: 'fill'
64
+ }
65
+
66
+ function parseMarkup(name: IconName): React.ReactNode[] {
67
+ const cached = parsed.get(name)
68
+ if (cached) return cached
69
+
70
+ const nodes: React.ReactNode[] = []
71
+ const markup = ICON_MARKUP[name]
72
+ let match: RegExpExecArray | null
73
+ SELF_CLOSING.lastIndex = 0
74
+ let i = 0
75
+ while ((match = SELF_CLOSING.exec(markup)) !== null) {
76
+ const [, tag, attrString] = match
77
+ const props: Record<string, string> = { key: String(i++) }
78
+ let attr: RegExpExecArray | null
79
+ ATTR.lastIndex = 0
80
+ while ((attr = ATTR.exec(attrString)) !== null) {
81
+ const prop = PROP_NAMES[attr[1]]
82
+ if (prop) props[prop] = attr[2]
83
+ }
84
+ nodes.push(React.createElement(tag, props))
85
+ }
86
+ parsed.set(name, nodes)
87
+ return nodes
88
+ }
89
+
90
+ export const Icon: React.FC<IconProps> = ({
91
+ name,
92
+ size = '1em',
93
+ family = 'warning',
94
+ variant = 'bare',
95
+ round = false,
96
+ title,
97
+ className,
98
+ strokeWidth = 2
99
+ }) => {
100
+ if (ICON_MARKUP[name] === undefined) {
101
+ throw new Error(
102
+ `<Icon>: "${String(name)}" is not in the icon registry. ` +
103
+ `Add it to themes/src/icons/sources.json and run \`pnpm run generate:icons\`.`
104
+ )
105
+ }
106
+
107
+ const dim = typeof size === 'number' ? `${size}px` : size
108
+ const a11y = title
109
+ ? ({ role: 'img', 'aria-label': title } as const)
110
+ : ({ 'aria-hidden': true, focusable: 'false' } as const)
111
+
112
+ // `bare` and `accent` render the <svg> as the outermost element, so the caller's
113
+ // className belongs on it. `tint`/`filled` wrap it in a tile, and the className
114
+ // goes there instead — in both cases it lands on the element you get back.
115
+ const isTile = variant === 'tint' || variant === 'filled'
116
+ const svgClass = [
117
+ 'hdk-icon',
118
+ variant === 'accent' ? `hdk-icon--${family}` : '',
119
+ !isTile && className ? className : ''
120
+ ]
121
+ .filter(Boolean)
122
+ .join(' ')
123
+
124
+ const svg = (
125
+ <svg
126
+ xmlns="http://www.w3.org/2000/svg"
127
+ className={svgClass}
128
+ width={dim}
129
+ height={dim}
130
+ viewBox="0 0 24 24"
131
+ fill="none"
132
+ stroke="currentColor"
133
+ strokeWidth={strokeWidth}
134
+ strokeLinecap="round"
135
+ strokeLinejoin="round"
136
+ {...a11y}
137
+ >
138
+ {title ? <title>{title}</title> : null}
139
+ {parseMarkup(name)}
140
+ </svg>
141
+ )
142
+
143
+ if (!isTile) return svg
144
+
145
+ const tile =
146
+ `hdk-icon-tile hdk-icon-tile--${variant} hdk-icon-tile--${family}` +
147
+ (round ? ' hdk-icon-tile--round' : '') +
148
+ (className ? ` ${className}` : '')
149
+
150
+ return <span className={tile}>{svg}</span>
151
+ }