banhaten 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +361 -0
  3. package/banhaten.config.example.json +13 -0
  4. package/package.json +59 -0
  5. package/registry/assets/activity-feed-avatar.png +0 -0
  6. package/registry/assets/avatars/avatar-01.jpg +0 -0
  7. package/registry/assets/avatars/avatar-02.jpg +0 -0
  8. package/registry/assets/avatars/avatar-03.jpg +0 -0
  9. package/registry/assets/avatars/avatar-04.jpg +0 -0
  10. package/registry/assets/avatars/avatar-05.jpg +0 -0
  11. package/registry/assets/avatars/avatar-06.jpg +0 -0
  12. package/registry/assets/avatars/avatar-07.jpg +0 -0
  13. package/registry/assets/avatars/avatar-08.jpg +0 -0
  14. package/registry/assets/avatars/avatar-09.jpg +0 -0
  15. package/registry/assets/avatars/avatar-10.jpg +0 -0
  16. package/registry/assets/avatars/avatar-11.jpg +0 -0
  17. package/registry/assets/avatars/avatar-12.jpg +0 -0
  18. package/registry/assets/avatars/avatar-13.jpg +0 -0
  19. package/registry/assets/avatars/avatar-14.jpg +0 -0
  20. package/registry/assets/avatars/avatar-15.jpg +0 -0
  21. package/registry/assets/avatars/avatar-16.jpg +0 -0
  22. package/registry/assets/avatars/avatar-17.jpg +0 -0
  23. package/registry/assets/avatars/avatar-18.jpg +0 -0
  24. package/registry/assets/avatars/avatar-19.jpg +0 -0
  25. package/registry/assets/avatars/avatar-20.jpg +0 -0
  26. package/registry/assets/avatars/avatar-21.jpg +0 -0
  27. package/registry/assets/avatars/avatar-22.jpg +0 -0
  28. package/registry/assets/avatars/avatar-23.jpg +0 -0
  29. package/registry/assets/avatars/avatar-24.jpg +0 -0
  30. package/registry/assets/avatars/avatar-25.jpg +0 -0
  31. package/registry/assets/avatars/avatar-26.jpg +0 -0
  32. package/registry/assets/avatars/avatar-27.jpg +0 -0
  33. package/registry/assets/avatars/avatar-28.jpg +0 -0
  34. package/registry/assets/avatars/avatar-29.jpg +0 -0
  35. package/registry/assets/avatars/avatar-30.jpg +0 -0
  36. package/registry/assets/avatars/avatar-31.jpg +0 -0
  37. package/registry/assets/avatars/avatar-32.jpg +0 -0
  38. package/registry/assets/avatars/avatar-33.jpg +0 -0
  39. package/registry/assets/avatars/avatar-34.jpg +0 -0
  40. package/registry/assets/avatars/avatar-35.jpg +0 -0
  41. package/registry/assets/image-assets.json +744 -0
  42. package/registry/assets/images/art-01.jpg +0 -0
  43. package/registry/assets/images/art-02.jpg +0 -0
  44. package/registry/assets/images/art-03.jpg +0 -0
  45. package/registry/assets/images/art-04.jpg +0 -0
  46. package/registry/assets/images/art-05.jpg +0 -0
  47. package/registry/assets/images/art-06.jpg +0 -0
  48. package/registry/assets/images/art-07.jpg +0 -0
  49. package/registry/assets/images/art-08.jpg +0 -0
  50. package/registry/assets/images/art-09.jpg +0 -0
  51. package/registry/assets/images/art-10.jpg +0 -0
  52. package/registry/assets/images/art-11.jpg +0 -0
  53. package/registry/assets/images/art-12.jpg +0 -0
  54. package/registry/assets/images/art-13.jpg +0 -0
  55. package/registry/assets/images/art-14.jpg +0 -0
  56. package/registry/assets/images/art-15.jpg +0 -0
  57. package/registry/assets/images/art-16.jpg +0 -0
  58. package/registry/assets/images/art-17.jpg +0 -0
  59. package/registry/assets/images/art-18.jpg +0 -0
  60. package/registry/assets/images/art-19.jpg +0 -0
  61. package/registry/assets/images/art-20.jpg +0 -0
  62. package/registry/assets/images/art-21.jpg +0 -0
  63. package/registry/assets/images/art-22.jpg +0 -0
  64. package/registry/assets/images/art-23.jpg +0 -0
  65. package/registry/assets/images/art-24.jpg +0 -0
  66. package/registry/assets/images/art-25.jpg +0 -0
  67. package/registry/assets/images/art-26.jpg +0 -0
  68. package/registry/assets/images/art-27.jpg +0 -0
  69. package/registry/assets/images/nature-01.jpg +0 -0
  70. package/registry/assets/images/nature-02.jpg +0 -0
  71. package/registry/assets/images/nature-03.jpg +0 -0
  72. package/registry/assets/images/nature-04.jpg +0 -0
  73. package/registry/assets/images/nature-05.jpg +0 -0
  74. package/registry/assets/images/nature-06.jpg +0 -0
  75. package/registry/assets/images/nature-07.jpg +0 -0
  76. package/registry/assets/images/nature-08.jpg +0 -0
  77. package/registry/assets/images/nature-09.jpg +0 -0
  78. package/registry/assets/images/nature-10.jpg +0 -0
  79. package/registry/assets/images/nature-11.jpg +0 -0
  80. package/registry/assets/images/nature-12.jpg +0 -0
  81. package/registry/assets/images/nature-13.jpg +0 -0
  82. package/registry/assets/images/nature-14.jpg +0 -0
  83. package/registry/assets/images/nature-15.jpg +0 -0
  84. package/registry/assets/images/nature-16.jpg +0 -0
  85. package/registry/assets/images/nature-17.jpg +0 -0
  86. package/registry/assets/images/nature-18.jpg +0 -0
  87. package/registry/assets/images/nature-19.jpg +0 -0
  88. package/registry/assets/images/nature-20.jpg +0 -0
  89. package/registry/components/accordion.tsx +119 -0
  90. package/registry/components/alert.tsx +282 -0
  91. package/registry/components/attribute.tsx +452 -0
  92. package/registry/components/avatar.tsx +142 -0
  93. package/registry/components/badge.tsx +567 -0
  94. package/registry/components/button-group.tsx +246 -0
  95. package/registry/components/button.tsx +102 -0
  96. package/registry/components/card.tsx +613 -0
  97. package/registry/components/checkbox.tsx +244 -0
  98. package/registry/components/date-picker.tsx +1143 -0
  99. package/registry/components/divider.tsx +82 -0
  100. package/registry/components/expanded/ActivityFeed.tsx +226 -0
  101. package/registry/components/expanded/Banner.tsx +145 -0
  102. package/registry/components/expanded/BannerBoard.tsx +225 -0
  103. package/registry/components/expanded/Breadcrumbs.tsx +156 -0
  104. package/registry/components/expanded/CatalogComponentsShowcase.tsx +211 -0
  105. package/registry/components/expanded/CatalogDivider.tsx +48 -0
  106. package/registry/components/expanded/CatalogTag.tsx +92 -0
  107. package/registry/components/expanded/CommandBar.tsx +406 -0
  108. package/registry/components/expanded/FileUpload.tsx +231 -0
  109. package/registry/components/expanded/IconExplorer.tsx +612 -0
  110. package/registry/components/expanded/OnboardingStepListItem.tsx +67 -0
  111. package/registry/components/expanded/PageHeader.tsx +184 -0
  112. package/registry/components/expanded/Slideout.tsx +514 -0
  113. package/registry/components/expanded/Steps.tsx +266 -0
  114. package/registry/components/expanded/Table.tsx +1014 -0
  115. package/registry/components/expanded/Tabs.tsx +86 -0
  116. package/registry/components/expanded/Timeline.tsx +235 -0
  117. package/registry/components/expanded/TimelineShowcase.tsx +158 -0
  118. package/registry/components/expanded/activityFeed.css +292 -0
  119. package/registry/components/expanded/banner.css +312 -0
  120. package/registry/components/expanded/breadcrumbs.css +140 -0
  121. package/registry/components/expanded/catalogComponentsShowcase.css +87 -0
  122. package/registry/components/expanded/commandBar.css +473 -0
  123. package/registry/components/expanded/divider.css +75 -0
  124. package/registry/components/expanded/fileUpload.css +228 -0
  125. package/registry/components/expanded/iconExplorer.css +764 -0
  126. package/registry/components/expanded/iconPacks.ts +866 -0
  127. package/registry/components/expanded/onboardingStepListItem.css +126 -0
  128. package/registry/components/expanded/pageHeader.css +287 -0
  129. package/registry/components/expanded/slideout.css +955 -0
  130. package/registry/components/expanded/steps.css +329 -0
  131. package/registry/components/expanded/table.css +607 -0
  132. package/registry/components/expanded/tabs.css +197 -0
  133. package/registry/components/expanded/tag.css +148 -0
  134. package/registry/components/expanded/timeline.css +282 -0
  135. package/registry/components/input-content.ts +106 -0
  136. package/registry/components/input.tsx +866 -0
  137. package/registry/components/menu.tsx +758 -0
  138. package/registry/components/modal.tsx +799 -0
  139. package/registry/components/pagination.tsx +543 -0
  140. package/registry/components/progress-slider.tsx +216 -0
  141. package/registry/components/progress.tsx +367 -0
  142. package/registry/components/radio-card.tsx +654 -0
  143. package/registry/components/radio-group.tsx +570 -0
  144. package/registry/components/select-content.tsx +313 -0
  145. package/registry/components/select.tsx +871 -0
  146. package/registry/components/slider.tsx +380 -0
  147. package/registry/components/social-button.tsx +360 -0
  148. package/registry/components/spinner.tsx +31 -0
  149. package/registry/components/tag.tsx +423 -0
  150. package/registry/components/textarea.tsx +625 -0
  151. package/registry/components/toggle.tsx +272 -0
  152. package/registry/components/toolbar.tsx +467 -0
  153. package/registry/components/tooltip.tsx +427 -0
  154. package/registry/examples/accordion-demo.tsx +34 -0
  155. package/registry/examples/alert-demo.tsx +14 -0
  156. package/registry/examples/attribute-demo.tsx +65 -0
  157. package/registry/examples/avatar-demo.tsx +74 -0
  158. package/registry/examples/badge-demo.tsx +53 -0
  159. package/registry/examples/button-demo.tsx +83 -0
  160. package/registry/examples/button-group-demo.tsx +42 -0
  161. package/registry/examples/card-demo.tsx +48 -0
  162. package/registry/examples/checkbox-demo.tsx +67 -0
  163. package/registry/examples/date-picker-demo.tsx +74 -0
  164. package/registry/examples/divider-demo.tsx +17 -0
  165. package/registry/examples/expanded/activity-feed-demo.tsx +22 -0
  166. package/registry/examples/expanded/banner-demo.tsx +23 -0
  167. package/registry/examples/expanded/catalog-components-demo.tsx +5 -0
  168. package/registry/examples/expanded/command-bar-demo.tsx +10 -0
  169. package/registry/examples/expanded/icons-demo.tsx +5 -0
  170. package/registry/examples/expanded/onboarding-step-demo.tsx +11 -0
  171. package/registry/examples/expanded/page-header-demo.tsx +19 -0
  172. package/registry/examples/expanded/slideout-demo.tsx +15 -0
  173. package/registry/examples/expanded/steps-demo.tsx +18 -0
  174. package/registry/examples/expanded/tabs-demo.tsx +13 -0
  175. package/registry/examples/expanded/timeline-demo.tsx +18 -0
  176. package/registry/examples/input-demo.tsx +87 -0
  177. package/registry/examples/menu-demo.tsx +109 -0
  178. package/registry/examples/modal-demo.tsx +16 -0
  179. package/registry/examples/pagination-demo.tsx +17 -0
  180. package/registry/examples/progress-demo.tsx +37 -0
  181. package/registry/examples/progress-slider-demo.tsx +29 -0
  182. package/registry/examples/radio-card-demo.tsx +51 -0
  183. package/registry/examples/radio-group-demo.tsx +62 -0
  184. package/registry/examples/select-demo.tsx +73 -0
  185. package/registry/examples/slider-demo.tsx +31 -0
  186. package/registry/examples/social-button-demo.tsx +51 -0
  187. package/registry/examples/tag-demo.tsx +29 -0
  188. package/registry/examples/textarea-demo.tsx +79 -0
  189. package/registry/examples/toggle-demo.tsx +59 -0
  190. package/registry/examples/toolbar-demo.tsx +80 -0
  191. package/registry/examples/tooltip-demo.tsx +115 -0
  192. package/registry/hooks/use-direction.ts +27 -0
  193. package/registry/index.json +1213 -0
  194. package/registry/styles/globals.css +4600 -0
  195. package/registry/utils/cn.ts +6 -0
  196. package/src/cli/index.js +826 -0
  197. package/tokens/Color mode.zip +0 -0
  198. package/tokens/Numbers.zip +0 -0
  199. package/tokens/Radius.zip +0 -0
  200. package/tokens/Theme.zip +0 -0
  201. package/tokens/banhaten.tokens.json +5525 -0
@@ -0,0 +1,612 @@
1
+ import { createElement, useEffect, useMemo, useState, type CSSProperties } from "react";
2
+ import {
3
+ iconPacks,
4
+ iconSemantics,
5
+ type IconPack,
6
+ type IconPosture,
7
+ type IconSemantic,
8
+ } from "./iconPacks";
9
+ import "./iconExplorer.css";
10
+
11
+ type ExplorerView = "evaluate" | "catalog" | "compare";
12
+ type Appearance = "default" | "rounded" | "sharp";
13
+ type Density = "comfortable" | "compact";
14
+
15
+ type CatalogResponse = {
16
+ total?: number;
17
+ title?: string;
18
+ categories?: Record<string, string[]>;
19
+ uncategorized?: string[];
20
+ };
21
+
22
+ type CatalogRow = {
23
+ name: string;
24
+ category: string;
25
+ };
26
+
27
+ type CatalogState =
28
+ | { status: "idle" }
29
+ | { status: "loading" }
30
+ | { status: "error"; message: string }
31
+ | { status: "loaded"; total: number; title: string; rows: CatalogRow[] };
32
+
33
+ const postureOptions: Array<{ value: "all" | IconPosture; label: string }> = [
34
+ { value: "all", label: "All candidates" },
35
+ { value: "shortlist", label: "Shortlist" },
36
+ { value: "broad", label: "Broad coverage" },
37
+ { value: "specialized", label: "Specialized" },
38
+ { value: "brand", label: "Brand logos" },
39
+ ];
40
+
41
+ function Icon({ name, size = 18, className }: { name: string; size?: number; className?: string }) {
42
+ return createElement("iconify-icon", {
43
+ icon: name,
44
+ width: size,
45
+ height: size,
46
+ class: className,
47
+ "aria-hidden": "true",
48
+ });
49
+ }
50
+
51
+ function collectionToRows(collection: CatalogResponse) {
52
+ const rows: CatalogRow[] = [];
53
+ const seen = new Set<string>();
54
+
55
+ Object.entries(collection.categories ?? {}).forEach(([category, names]) => {
56
+ names.forEach((name) => {
57
+ if (seen.has(name)) return;
58
+ seen.add(name);
59
+ rows.push({ name, category });
60
+ });
61
+ });
62
+
63
+ (collection.uncategorized ?? []).forEach((name) => {
64
+ if (seen.has(name)) return;
65
+ seen.add(name);
66
+ rows.push({ name, category: "Uncategorized" });
67
+ });
68
+
69
+ return rows;
70
+ }
71
+
72
+ export function IconExplorer() {
73
+ const [activePackId, setActivePackId] = useState("lucide");
74
+ const [activeVariantByPack, setActiveVariantByPack] = useState<Record<string, string>>({});
75
+ const [view, setView] = useState<ExplorerView>("evaluate");
76
+ const [packQuery, setPackQuery] = useState("");
77
+ const [postureFilter, setPostureFilter] = useState<"all" | IconPosture>("all");
78
+ const [iconSize, setIconSize] = useState(18);
79
+ const [appearance, setAppearance] = useState<Appearance>("default");
80
+ const [density, setDensity] = useState<Density>("comfortable");
81
+ const [catalogs, setCatalogs] = useState<Record<string, CatalogState>>({});
82
+ const [catalogQuery, setCatalogQuery] = useState("");
83
+ const [catalogCategory, setCatalogCategory] = useState("all");
84
+ const [renderLimit, setRenderLimit] = useState(180);
85
+
86
+ useEffect(() => {
87
+ if (document.querySelector('script[data-iconify-icon="true"]')) return;
88
+
89
+ const script = document.createElement("script");
90
+ script.src = "https://code.iconify.design/iconify-icon/3.0.0/iconify-icon.min.js";
91
+ script.async = true;
92
+ script.dataset.iconifyIcon = "true";
93
+ document.head.appendChild(script);
94
+ }, []);
95
+
96
+ const activePack = useMemo(
97
+ () => iconPacks.find((pack) => pack.id === activePackId) ?? iconPacks[0],
98
+ [activePackId],
99
+ );
100
+
101
+ const activeVariant = useMemo(() => {
102
+ const id = activeVariantByPack[activePack.id] ?? activePack.variants[0].id;
103
+ return activePack.variants.find((variant) => variant.id === id) ?? activePack.variants[0];
104
+ }, [activePack, activeVariantByPack]);
105
+
106
+ function getIconName(pack: IconPack, semantic: IconSemantic) {
107
+ const variantId = activeVariantByPack[pack.id] ?? pack.variants[0].id;
108
+ const variant = pack.variants.find((item) => item.id === variantId) ?? pack.variants[0];
109
+ const override = variant.overrides?.[semantic];
110
+ let base = override ?? pack.icons[semantic] ?? pack.icons.home;
111
+
112
+ if (!override && pack.variantMode === "prefix" && variant.prefix) {
113
+ base = `${variant.prefix}${base}`;
114
+ }
115
+
116
+ if (!override && pack.variantMode === "suffix" && variant.suffix) {
117
+ base = `${base}${variant.suffix}`;
118
+ }
119
+
120
+ return `${pack.prefix}:${base}`;
121
+ }
122
+
123
+ const filteredPacks = useMemo(() => {
124
+ const query = packQuery.trim().toLowerCase();
125
+ return iconPacks.filter((pack) => {
126
+ const matchesQuery = `${pack.name} ${pack.license} ${pack.tags.join(" ")}`
127
+ .toLowerCase()
128
+ .includes(query);
129
+ const matchesPosture = postureFilter === "all" || pack.posture === postureFilter;
130
+ return matchesQuery && matchesPosture;
131
+ });
132
+ }, [packQuery, postureFilter]);
133
+
134
+ const activeCatalog = catalogs[activePack.id] ?? { status: "idle" };
135
+
136
+ const catalogRows = useMemo(() => {
137
+ if (activeCatalog.status !== "loaded") return [];
138
+
139
+ const query = catalogQuery.trim().toLowerCase();
140
+ return activeCatalog.rows.filter((row) => {
141
+ const matchesQuery = !query || row.name.toLowerCase().includes(query);
142
+ const matchesCategory = catalogCategory === "all" || row.category === catalogCategory;
143
+ return matchesQuery && matchesCategory;
144
+ });
145
+ }, [activeCatalog, catalogCategory, catalogQuery]);
146
+
147
+ const catalogCategories = useMemo(() => {
148
+ if (activeCatalog.status !== "loaded") return [];
149
+ return Array.from(new Set(activeCatalog.rows.map((row) => row.category))).sort();
150
+ }, [activeCatalog]);
151
+
152
+ async function loadCatalog() {
153
+ setCatalogs((current) => ({ ...current, [activePack.id]: { status: "loading" } }));
154
+ try {
155
+ const response = await fetch(`https://api.iconify.design/collection?prefix=${activePack.prefix}&info=1`);
156
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
157
+
158
+ const collection = (await response.json()) as CatalogResponse;
159
+ setCatalogs((current) => ({
160
+ ...current,
161
+ [activePack.id]: {
162
+ status: "loaded",
163
+ total: collection.total ?? 0,
164
+ title: collection.title ?? activePack.name,
165
+ rows: collectionToRows(collection),
166
+ },
167
+ }));
168
+ setRenderLimit(180);
169
+ } catch (error) {
170
+ setCatalogs((current) => ({
171
+ ...current,
172
+ [activePack.id]: {
173
+ status: "error",
174
+ message: error instanceof Error ? error.message : "Unknown error",
175
+ },
176
+ }));
177
+ }
178
+ }
179
+
180
+ function selectPack(packId: string) {
181
+ setActivePackId(packId);
182
+ setCatalogCategory("all");
183
+ setCatalogQuery("");
184
+ setRenderLimit(180);
185
+ }
186
+
187
+ function selectVariant(variantId: string) {
188
+ setActiveVariantByPack((current) => ({ ...current, [activePack.id]: variantId }));
189
+ }
190
+
191
+ const radius =
192
+ appearance === "sharp"
193
+ ? "var(--bh-radius-none)"
194
+ : appearance === "rounded"
195
+ ? "var(--bh-radius-full)"
196
+ : "var(--bh-control-default)";
197
+ const panelRadius =
198
+ appearance === "sharp" ? "var(--bh-radius-none)" : "var(--bh-radius-lg-8)";
199
+ const rowHeight =
200
+ density === "compact"
201
+ ? "calc(var(--bh-space-6xl-32) + var(--bh-space-xs-4))"
202
+ : "calc(var(--bh-space-7xl-40) + var(--bh-space-xxs-2))";
203
+ const previewStyle = { "--preview-icon-size": `${iconSize}px` } as CSSProperties;
204
+
205
+ return (
206
+ <section className="icons-page" aria-label="Icon pack explorer">
207
+ <aside className="icons-sidebar">
208
+ <div className="icons-sidebar__header">
209
+ <span className="icons-mark">
210
+ <Icon name="lucide:boxes" size={20} />
211
+ </span>
212
+ <div>
213
+ <h1>Icons</h1>
214
+ <p>Open-source pack review</p>
215
+ </div>
216
+ </div>
217
+
218
+ <div className="icons-sidebar__tools">
219
+ <input
220
+ aria-label="Filter icon packs"
221
+ className="icons-input"
222
+ onChange={(event) => setPackQuery(event.target.value)}
223
+ placeholder="Filter packs"
224
+ type="search"
225
+ value={packQuery}
226
+ />
227
+ <select
228
+ aria-label="Filter candidates"
229
+ className="icons-select"
230
+ onChange={(event) => setPostureFilter(event.target.value as "all" | IconPosture)}
231
+ value={postureFilter}
232
+ >
233
+ {postureOptions.map((option) => (
234
+ <option key={option.value} value={option.value}>
235
+ {option.label}
236
+ </option>
237
+ ))}
238
+ </select>
239
+ </div>
240
+
241
+ <div className="icons-pack-list">
242
+ {filteredPacks.map((pack) => (
243
+ <button
244
+ className={pack.id === activePack.id ? "icons-pack is-active" : "icons-pack"}
245
+ key={pack.id}
246
+ onClick={() => selectPack(pack.id)}
247
+ type="button"
248
+ >
249
+ <span className="icons-pack__glyph">
250
+ <Icon name={getIconName(pack, "home")} size={22} />
251
+ </span>
252
+ <span className="icons-pack__copy">
253
+ <strong>{pack.name}</strong>
254
+ <span>
255
+ {pack.total.toLocaleString()} icons, {pack.license}
256
+ </span>
257
+ </span>
258
+ </button>
259
+ ))}
260
+ </div>
261
+ </aside>
262
+
263
+ <main className="icons-main">
264
+ <header className="icons-topbar">
265
+ <div>
266
+ <h2>Icon pack explorer</h2>
267
+ <p>
268
+ {activePack.total.toLocaleString()} icons through Iconify prefix <code>{activePack.prefix}</code>
269
+ </p>
270
+ </div>
271
+
272
+ <nav className="icons-tabs" aria-label="Icon explorer views">
273
+ {(["evaluate", "catalog", "compare"] as ExplorerView[]).map((item) => (
274
+ <button
275
+ className={view === item ? "icons-tab is-active" : "icons-tab"}
276
+ key={item}
277
+ onClick={() => setView(item)}
278
+ type="button"
279
+ >
280
+ {item[0].toUpperCase() + item.slice(1)}
281
+ </button>
282
+ ))}
283
+ </nav>
284
+ </header>
285
+
286
+ {view === "evaluate" && (
287
+ <div className="icons-content">
288
+ <section className="icons-summary-grid">
289
+ <div className="icons-panel icons-hero-panel">
290
+ <span className="icons-hero-glyph">
291
+ <Icon name={getIconName(activePack, "home")} size={46} />
292
+ </span>
293
+ <div className="icons-hero-copy">
294
+ <h3>{activePack.name}</h3>
295
+ <p>{activePack.summary}</p>
296
+ <div className="icons-tags">
297
+ <span className="icons-tag is-warm">{activePack.posture}</span>
298
+ <span className="icons-tag">{activePack.license}</span>
299
+ {activePack.tags.map((tag) => (
300
+ <span className="icons-tag" key={tag}>
301
+ {tag}
302
+ </span>
303
+ ))}
304
+ </div>
305
+ <div className="icons-links">
306
+ <a href={activePack.officialUrl} rel="noreferrer" target="_blank">
307
+ Official
308
+ </a>
309
+ <a href={activePack.licenseUrl} rel="noreferrer" target="_blank">
310
+ License
311
+ </a>
312
+ <a href={`https://icon-sets.iconify.design/${activePack.prefix}/`} rel="noreferrer" target="_blank">
313
+ Iconify catalog
314
+ </a>
315
+ </div>
316
+ </div>
317
+ </div>
318
+
319
+ <div className="icons-panel">
320
+ <div className="icons-panel__header">
321
+ <h3>Fit snapshot</h3>
322
+ </div>
323
+ <div className="icons-score-list">
324
+ {[
325
+ ["System fit", activePack.scores.fit],
326
+ ["Coverage", activePack.scores.coverage],
327
+ ["Variant depth", activePack.scores.variants],
328
+ ["Dense UI clarity", activePack.scores.density],
329
+ ].map(([label, value]) => (
330
+ <div className="icons-score" key={label}>
331
+ <span>
332
+ <strong>{label}</strong>
333
+ <em>{value}</em>
334
+ </span>
335
+ <div>
336
+ <i style={{ width: `${value}%` }} />
337
+ </div>
338
+ </div>
339
+ ))}
340
+ </div>
341
+ </div>
342
+ </section>
343
+
344
+ <section className="icons-panel">
345
+ <div className="icons-panel__header">
346
+ <h3>Variants</h3>
347
+ <span>{activeVariant.label}</span>
348
+ </div>
349
+ <div className="icons-variants">
350
+ {activePack.variants.map((variant) => (
351
+ <button
352
+ className={variant.id === activeVariant.id ? "icons-variant is-active" : "icons-variant"}
353
+ key={variant.id}
354
+ onClick={() => selectVariant(variant.id)}
355
+ type="button"
356
+ >
357
+ {variant.label}
358
+ </button>
359
+ ))}
360
+ </div>
361
+ </section>
362
+
363
+ <section className="icons-panel">
364
+ <div className="icons-panel__header">
365
+ <h3>Icon samples</h3>
366
+ <span>{activePack.prefix}</span>
367
+ </div>
368
+ <div className="icons-sample-grid">
369
+ {iconSemantics.map(([semantic, label]) => (
370
+ <button className="icons-sample" key={semantic} title={getIconName(activePack, semantic)} type="button">
371
+ <Icon name={getIconName(activePack, semantic)} size={Math.max(iconSize, 22)} />
372
+ <span>{label}</span>
373
+ </button>
374
+ ))}
375
+ </div>
376
+ </section>
377
+
378
+ <section className="icons-component-grid">
379
+ <div className="icons-panel">
380
+ <div className="icons-panel__header">
381
+ <h3>Component controls</h3>
382
+ </div>
383
+ <div className="icons-controls">
384
+ <label>
385
+ <span>Icon size</span>
386
+ <input
387
+ max={30}
388
+ min={14}
389
+ onChange={(event) => setIconSize(Number(event.target.value))}
390
+ type="range"
391
+ value={iconSize}
392
+ />
393
+ </label>
394
+ <label>
395
+ <span>Appearance</span>
396
+ <select
397
+ className="icons-select"
398
+ onChange={(event) => setAppearance(event.target.value as Appearance)}
399
+ value={appearance}
400
+ >
401
+ <option value="default">Default</option>
402
+ <option value="rounded">Rounded</option>
403
+ <option value="sharp">Sharp</option>
404
+ </select>
405
+ </label>
406
+ <label>
407
+ <span>Density</span>
408
+ <select
409
+ className="icons-select"
410
+ onChange={(event) => setDensity(event.target.value as Density)}
411
+ value={density}
412
+ >
413
+ <option value="comfortable">Comfortable</option>
414
+ <option value="compact">Compact</option>
415
+ </select>
416
+ </label>
417
+ </div>
418
+ </div>
419
+
420
+ <div className="icons-panel">
421
+ <div className="icons-panel__header">
422
+ <h3>Component preview</h3>
423
+ </div>
424
+ <div className="icons-preview" style={previewStyle}>
425
+ <div className="icons-button-row">
426
+ <button className="preview-button is-primary" style={{ borderRadius: radius }} type="button">
427
+ <Icon name={getIconName(activePack, "add")} size={iconSize} />
428
+ Create
429
+ </button>
430
+ <button className="preview-button" style={{ borderRadius: radius }} type="button">
431
+ <Icon name={getIconName(activePack, "download")} size={iconSize} />
432
+ Export
433
+ </button>
434
+ <button className="preview-button is-danger" style={{ borderRadius: radius }} type="button">
435
+ <Icon name={getIconName(activePack, "trash")} size={iconSize} />
436
+ Delete
437
+ </button>
438
+ <button className="preview-button is-icon-only" style={{ borderRadius: radius }} type="button">
439
+ <Icon name={getIconName(activePack, "settings")} size={iconSize} />
440
+ </button>
441
+ </div>
442
+
443
+ <div className="icons-field-row">
444
+ <div className="icons-field" style={{ borderRadius: radius }}>
445
+ <Icon name={getIconName(activePack, "search")} size={iconSize} />
446
+ Search projects, users, invoices
447
+ </div>
448
+ <div className="icons-field is-select" style={{ borderRadius: radius }}>
449
+ <span>
450
+ <Icon name={getIconName(activePack, "filter")} size={iconSize} />
451
+ Status
452
+ </span>
453
+ <Icon name={getIconName(activePack, "chevron")} size={16} />
454
+ </div>
455
+ </div>
456
+
457
+ <div className="icons-menu-preview" style={{ borderRadius: panelRadius }}>
458
+ {[
459
+ ["home", "Dashboard", "chevron"],
460
+ ["users", "Customers", "24"],
461
+ ["bell", "Notifications", "Live"],
462
+ ].map(([left, label, right]) => (
463
+ <div className="icons-menu-row" key={label} style={{ minHeight: rowHeight }}>
464
+ <Icon name={getIconName(activePack, left as IconSemantic)} size={iconSize} />
465
+ <span>{label}</span>
466
+ {right === "Live" ? (
467
+ <em>
468
+ <Icon name={getIconName(activePack, "check")} size={14} />
469
+ Live
470
+ </em>
471
+ ) : right === "24" ? (
472
+ <small>24</small>
473
+ ) : (
474
+ <Icon name={getIconName(activePack, "chevron")} size={16} />
475
+ )}
476
+ </div>
477
+ ))}
478
+ </div>
479
+
480
+ <div className="icons-table-preview" style={{ borderRadius: panelRadius }}>
481
+ {[
482
+ ["chart", "Usage API", "Infrastructure", "92.8%", "more"],
483
+ ["calendar", "Billing review", "Finance", "Jun 12", "edit"],
484
+ ["warning", "Risk queue", "Operations", "4 open", "more"],
485
+ ].map(([left, name, area, value, action]) => (
486
+ <div className="icons-table-row" key={name} style={{ minHeight: rowHeight }}>
487
+ <Icon name={getIconName(activePack, left as IconSemantic)} size={iconSize} />
488
+ <strong>{name}</strong>
489
+ <span>{area}</span>
490
+ <span>{value}</span>
491
+ <Icon name={getIconName(activePack, action as IconSemantic)} size={16} />
492
+ </div>
493
+ ))}
494
+ </div>
495
+
496
+ <div className="icons-empty-preview" style={{ borderRadius: panelRadius }}>
497
+ <div>
498
+ <span style={{ borderRadius: panelRadius }}>
499
+ <Icon name={getIconName(activePack, "filter")} size={28} />
500
+ </span>
501
+ <h3>No matching records</h3>
502
+ <p>Adjust the active filters or create a saved view for this workflow.</p>
503
+ <button className="preview-button is-primary" style={{ borderRadius: radius }} type="button">
504
+ <Icon name={getIconName(activePack, "add")} size={iconSize} />
505
+ Saved view
506
+ </button>
507
+ </div>
508
+ </div>
509
+ </div>
510
+ </div>
511
+ </section>
512
+ </div>
513
+ )}
514
+
515
+ {view === "catalog" && (
516
+ <div className="icons-content">
517
+ <section className="icons-panel">
518
+ <div className="icons-panel__header">
519
+ <div>
520
+ <h3>{activePack.name} catalog</h3>
521
+ <span>{catalogStatusText(activeCatalog, catalogRows.length)}</span>
522
+ </div>
523
+ </div>
524
+ <div className="icons-catalog-tools">
525
+ <input
526
+ className="icons-input"
527
+ onChange={(event) => {
528
+ setCatalogQuery(event.target.value);
529
+ setRenderLimit(180);
530
+ }}
531
+ placeholder="Search icons in this pack"
532
+ type="search"
533
+ value={catalogQuery}
534
+ />
535
+ <select
536
+ className="icons-select"
537
+ onChange={(event) => {
538
+ setCatalogCategory(event.target.value);
539
+ setRenderLimit(180);
540
+ }}
541
+ value={catalogCategory}
542
+ >
543
+ <option value="all">All categories</option>
544
+ {catalogCategories.map((category) => (
545
+ <option key={category} value={category}>
546
+ {category}
547
+ </option>
548
+ ))}
549
+ </select>
550
+ <button className="preview-button" disabled={activeCatalog.status === "loading"} onClick={loadCatalog} type="button">
551
+ <Icon name="lucide:download-cloud" size={18} />
552
+ {activeCatalog.status === "loading" ? "Loading" : "Load catalog"}
553
+ </button>
554
+ </div>
555
+ </section>
556
+
557
+ {activeCatalog.status === "error" && <div className="icons-error">Could not load catalog: {activeCatalog.message}</div>}
558
+
559
+ <section className="icons-catalog-grid">
560
+ {activeCatalog.status === "loaded" &&
561
+ catalogRows.slice(0, renderLimit).map((row) => (
562
+ <button className="icons-catalog-card" key={`${row.category}-${row.name}`} title={`${activePack.prefix}:${row.name}`} type="button">
563
+ <Icon name={`${activePack.prefix}:${row.name}`} size={30} />
564
+ <span>{row.name}</span>
565
+ </button>
566
+ ))}
567
+ </section>
568
+
569
+ {activeCatalog.status === "loaded" && renderLimit < catalogRows.length && (
570
+ <button className="preview-button icons-load-more" onClick={() => setRenderLimit((current) => current + 180)} type="button">
571
+ <Icon name="lucide:plus" size={18} />
572
+ Show more
573
+ </button>
574
+ )}
575
+ </div>
576
+ )}
577
+
578
+ {view === "compare" && (
579
+ <div className="icons-content">
580
+ <section className="icons-compare-grid">
581
+ {iconPacks.map((pack) => (
582
+ <article className="icons-compare-card" key={pack.id}>
583
+ <div>
584
+ <strong>{pack.name}</strong>
585
+ <span>{pack.license}</span>
586
+ </div>
587
+ <div className="icons-compare-icons">
588
+ {(["home", "search", "settings", "users", "warning"] as IconSemantic[]).map((semantic) => (
589
+ <span key={semantic}>
590
+ <Icon name={getIconName(pack, semantic)} size={22} />
591
+ </span>
592
+ ))}
593
+ </div>
594
+ <p>
595
+ {pack.total.toLocaleString()} icons, {pack.tags.join(", ")}
596
+ </p>
597
+ </article>
598
+ ))}
599
+ </section>
600
+ </div>
601
+ )}
602
+ </main>
603
+ </section>
604
+ );
605
+ }
606
+
607
+ function catalogStatusText(catalog: CatalogState, matchCount: number) {
608
+ if (catalog.status === "idle") return "Catalog names load on demand from Iconify.";
609
+ if (catalog.status === "loading") return "Loading the full icon name list...";
610
+ if (catalog.status === "error") return "Catalog failed to load.";
611
+ return `${catalog.total.toLocaleString()} icons loaded, ${matchCount.toLocaleString()} matching.`;
612
+ }
@@ -0,0 +1,67 @@
1
+ import "./onboardingStepListItem.css";
2
+
3
+ export type OnboardingStepState = "finished" | "active" | "inactive";
4
+
5
+ export type OnboardingStepListItemProps = {
6
+ className?: string;
7
+ dir?: "ltr" | "rtl";
8
+ label?: string;
9
+ showLeadingLine?: boolean;
10
+ showTrailingLine?: boolean;
11
+ state?: OnboardingStepState;
12
+ };
13
+
14
+ export function OnboardingStepListItem({
15
+ className = "",
16
+ dir = "ltr",
17
+ label,
18
+ showLeadingLine = true,
19
+ showTrailingLine = true,
20
+ state = "finished",
21
+ }: OnboardingStepListItemProps) {
22
+ const rtl = dir === "rtl";
23
+ const isFinished = state === "finished";
24
+ const isActive = state === "active";
25
+ const isInactive = state === "inactive";
26
+
27
+ return (
28
+ <div
29
+ className={[
30
+ "ds-onboarding-step",
31
+ `ds-onboarding-step--${state}`,
32
+ rtl ? "ds-onboarding-step--rtl" : "ds-onboarding-step--ltr",
33
+ className,
34
+ ]
35
+ .filter(Boolean)
36
+ .join(" ")}
37
+ dir={dir}
38
+ >
39
+ <span className="ds-onboarding-step__line-wrap" aria-hidden="true">
40
+ {isFinished && showLeadingLine && <span className="ds-onboarding-step__line ds-onboarding-step__line--leading" />}
41
+ {isFinished && <CheckCircleIcon />}
42
+ {isFinished && showTrailingLine && <span className="ds-onboarding-step__line ds-onboarding-step__line--trailing" />}
43
+ {isActive && <span className="ds-onboarding-step__line ds-onboarding-step__line--active" />}
44
+ {isInactive && <span className="ds-onboarding-step__line ds-onboarding-step__line--inactive" />}
45
+ </span>
46
+ {label && <span className="ds-onboarding-step__label">{label}</span>}
47
+ </div>
48
+ );
49
+ }
50
+
51
+ function CheckCircleIcon() {
52
+ return (
53
+ <svg
54
+ className="ds-onboarding-step__check"
55
+ fill="none"
56
+ height="20"
57
+ viewBox="0 0 20 20"
58
+ width="20"
59
+ xmlns="http://www.w3.org/2000/svg"
60
+ >
61
+ <path
62
+ d="M10 1.667A8.333 8.333 0 1 1 10 18.333 8.333 8.333 0 0 1 10 1.667Zm-.831 11.667 5.89-5.893-1.179-1.178-4.711 4.714-2.357-2.357-1.179 1.178 3.536 3.536Z"
63
+ fill="currentColor"
64
+ />
65
+ </svg>
66
+ );
67
+ }