velloo 0.0.0 → 0.2.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 (267) hide show
  1. package/BUN-LICENSE.md +91 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +9 -0
  4. package/README.md +288 -2
  5. package/THIRD-PARTY-NOTICES.md +467 -0
  6. package/assets/elsewhere-alps-9fqhv029.jpg +0 -0
  7. package/assets/elsewhere-bali-vp60g115.jpg +0 -0
  8. package/assets/elsewhere-forest-xgn23zrj.jpg +0 -0
  9. package/assets/elsewhere-japan-qs7qkbr2.jpg +0 -0
  10. package/assets/elsewhere-kyoto-map-n2bvkrya.svg +1 -0
  11. package/assets/elsewhere-kyoto-qemtn6nk.jpg +0 -0
  12. package/assets/elsewhere-room-g1gaw7cs.jpg +0 -0
  13. package/canvas/apple-touch-icon.png +0 -0
  14. package/canvas/assets/index-BCvDZiBo.css +2 -0
  15. package/canvas/assets/index-ClaBqcaS.js +28 -0
  16. package/canvas/assets/lucide-all-iIF-pmzN.js +1 -0
  17. package/canvas/assets/radix-C3HaOB-d.js +41 -0
  18. package/canvas/assets/react-D0Ec-185.js +9 -0
  19. package/canvas/assets/rolldown-runtime-hePW80VL.js +1 -0
  20. package/canvas/assets/vendor-CKmQ-Zst.js +53 -0
  21. package/canvas/favicon.ico +0 -0
  22. package/canvas/favicon.svg +12 -0
  23. package/canvas/fonts/OFL-1.1.txt +93 -0
  24. package/canvas/fonts/poppins-700.woff2 +0 -0
  25. package/canvas/icon-192.png +0 -0
  26. package/canvas/icon-512.png +0 -0
  27. package/canvas/index.html +22 -0
  28. package/canvas/site.webmanifest +11 -0
  29. package/chunk-08sv3k41.js +4 -0
  30. package/chunk-0fe83yzt.js +2 -0
  31. package/chunk-0t68szgr.js +3 -0
  32. package/chunk-1h55s5he.js +2 -0
  33. package/chunk-2c7jbx46.js +2 -0
  34. package/chunk-2smsg2ct.js +7 -0
  35. package/chunk-308ynsxd.js +2 -0
  36. package/chunk-3e8vez4d.js +1229 -0
  37. package/chunk-40xgcth8.js +63 -0
  38. package/chunk-4dn75m6t.js +3 -0
  39. package/chunk-4fj5tahv.js +3 -0
  40. package/chunk-6nqghjda.js +3 -0
  41. package/chunk-6tx68d90.js +3 -0
  42. package/chunk-6w6ch4ce.js +2 -0
  43. package/chunk-735ezz23.js +86 -0
  44. package/chunk-7eq7nx37.js +7 -0
  45. package/chunk-7eyb8yrf.js +6 -0
  46. package/chunk-80fjfyha.js +86 -0
  47. package/chunk-80r93t2e.js +4 -0
  48. package/chunk-94vx1yb0.js +3 -0
  49. package/chunk-9vg3yafz.js +3 -0
  50. package/chunk-a84n72m2.js +4 -0
  51. package/chunk-ampmdzz4.js +10 -0
  52. package/chunk-avns4dbf.js +2 -0
  53. package/chunk-ayrb3qnm.js +3 -0
  54. package/chunk-bwnvcncx.js +3 -0
  55. package/chunk-ccszg02q.js +5 -0
  56. package/chunk-cvn689d5.js +3 -0
  57. package/chunk-cxe6jjwj.js +3 -0
  58. package/chunk-dg6z4vdd.js +3 -0
  59. package/chunk-dgwjpbp9.js +3 -0
  60. package/chunk-dy41n60j.js +52 -0
  61. package/chunk-ek71z2fv.js +226 -0
  62. package/chunk-er1fxb7d.js +2 -0
  63. package/chunk-eyr8hrzf.js +2 -0
  64. package/chunk-fjbzk4sw.js +3 -0
  65. package/chunk-fsbsksga.js +3 -0
  66. package/chunk-h6gchare.js +3 -0
  67. package/chunk-hcb68zt7.js +85 -0
  68. package/chunk-hggap8fq.js +3 -0
  69. package/chunk-j11fyeqh.js +2 -0
  70. package/chunk-jb21jkp1.js +8 -0
  71. package/chunk-jfx9xjmr.js +18 -0
  72. package/chunk-jr0ss0mm.js +303 -0
  73. package/chunk-jrsknzjt.js +3 -0
  74. package/chunk-jv0xggh0.js +2 -0
  75. package/chunk-k17gfwq3.js +2 -0
  76. package/chunk-mh2pr7cq.js +5 -0
  77. package/chunk-mnapdp1y.js +2 -0
  78. package/chunk-mzhdrawd.js +3 -0
  79. package/chunk-n9gs6yx9.js +2 -0
  80. package/chunk-ngnymg2q.js +3 -0
  81. package/chunk-nwx5700b.js +3 -0
  82. package/chunk-pjtg3wnm.js +4 -0
  83. package/chunk-pp86v6m6.js +3 -0
  84. package/chunk-r22epxz2.js +3 -0
  85. package/chunk-rgasy862.js +5 -0
  86. package/chunk-rpwpeph5.js +3 -0
  87. package/chunk-s23gtejf.js +3 -0
  88. package/chunk-sjgqnykd.js +6 -0
  89. package/chunk-sn3x2m0m.js +6 -0
  90. package/chunk-t448rrfr.js +2 -0
  91. package/chunk-tdsk5wxa.js +451 -0
  92. package/chunk-v40fn08f.js +442 -0
  93. package/chunk-vbk1qhdz.js +3 -0
  94. package/chunk-vdkrbzh1.js +2 -0
  95. package/chunk-vgmr98cv.js +40 -0
  96. package/chunk-vsv8z729.js +6 -0
  97. package/chunk-x4f4k9pp.js +26 -0
  98. package/chunk-xt5ayg7g.js +4 -0
  99. package/chunk-ydhwv29b.js +244 -0
  100. package/chunk-zb6hc1y0.js +2 -0
  101. package/cli.js +3 -0
  102. package/launcher.cjs +97 -0
  103. package/package.json +58 -5
  104. package/pkgs/helpers/src/box.tsx +21 -0
  105. package/pkgs/helpers/src/cn.ts +32 -0
  106. package/pkgs/helpers/src/descriptors.ts +270 -0
  107. package/pkgs/helpers/src/divider.tsx +123 -0
  108. package/pkgs/helpers/src/gradient.tsx +90 -0
  109. package/pkgs/helpers/src/heading.tsx +20 -0
  110. package/pkgs/helpers/src/icon-data.ts +8009 -0
  111. package/pkgs/helpers/src/icon.tsx +53 -0
  112. package/pkgs/helpers/src/image.tsx +111 -0
  113. package/pkgs/helpers/src/index.ts +29 -0
  114. package/pkgs/helpers/src/layer.tsx +61 -0
  115. package/pkgs/helpers/src/lowering.ts +53 -0
  116. package/pkgs/helpers/src/paths.ts +47 -0
  117. package/pkgs/helpers/src/placeholder.tsx +88 -0
  118. package/pkgs/helpers/src/prose.tsx +42 -0
  119. package/pkgs/helpers/src/registry.ts +41 -0
  120. package/pkgs/helpers/src/svg.tsx +62 -0
  121. package/pkgs/helpers/src/text.tsx +16 -0
  122. package/pkgs/provider-antd/src/index.ts +78 -0
  123. package/pkgs/provider-antd/src/intro.ts +14 -0
  124. package/pkgs/provider-antd/src/manifest.ts +439 -0
  125. package/pkgs/provider-antd/src/overlays.ts +176 -0
  126. package/pkgs/provider-antd/src/registry.ts +104 -0
  127. package/pkgs/provider-antd/src/render-pass.ts +43 -0
  128. package/pkgs/provider-antd/src/tailwind-entry.css +21 -0
  129. package/pkgs/provider-antd/src/theme.ts +132 -0
  130. package/pkgs/provider-antd/src/version.ts +8 -0
  131. package/pkgs/provider-chakra/src/index.ts +80 -0
  132. package/pkgs/provider-chakra/src/intro.ts +14 -0
  133. package/pkgs/provider-chakra/src/manifest.ts +421 -0
  134. package/pkgs/provider-chakra/src/overlays.ts +246 -0
  135. package/pkgs/provider-chakra/src/registry.ts +227 -0
  136. package/pkgs/provider-chakra/src/render-pass.ts +55 -0
  137. package/pkgs/provider-chakra/src/tailwind-entry.css +21 -0
  138. package/pkgs/provider-chakra/src/theme.ts +180 -0
  139. package/pkgs/provider-chakra/src/version.ts +8 -0
  140. package/pkgs/provider-mui/src/index.ts +117 -0
  141. package/pkgs/provider-mui/src/intro.ts +12 -0
  142. package/pkgs/provider-mui/src/manifest.ts +263 -0
  143. package/pkgs/provider-mui/src/overlays.ts +124 -0
  144. package/pkgs/provider-mui/src/registry.ts +120 -0
  145. package/pkgs/provider-mui/src/render-pass.ts +36 -0
  146. package/pkgs/provider-mui/src/tailwind-entry.css +21 -0
  147. package/pkgs/provider-mui/src/theme.ts +129 -0
  148. package/pkgs/provider-mui/src/version.ts +8 -0
  149. package/pkgs/provider-none/src/components-inline.tsx +204 -0
  150. package/pkgs/provider-none/src/components.tsx +155 -0
  151. package/pkgs/provider-none/src/index.ts +57 -0
  152. package/pkgs/provider-none/src/intro.ts +18 -0
  153. package/pkgs/provider-none/src/manifest.ts +163 -0
  154. package/pkgs/provider-none/src/registry-inline.ts +36 -0
  155. package/pkgs/provider-none/src/registry.ts +39 -0
  156. package/pkgs/provider-none/src/tailwind-entry.css +57 -0
  157. package/pkgs/provider-none/src/version.ts +10 -0
  158. package/pkgs/schema/src/annotation.ts +120 -0
  159. package/pkgs/schema/src/asset.ts +66 -0
  160. package/pkgs/schema/src/board.ts +71 -0
  161. package/pkgs/schema/src/comment.ts +107 -0
  162. package/pkgs/schema/src/config.ts +185 -0
  163. package/pkgs/schema/src/css-sanitize.ts +56 -0
  164. package/pkgs/schema/src/extension.ts +105 -0
  165. package/pkgs/schema/src/fonts.ts +690 -0
  166. package/pkgs/schema/src/frame.ts +40 -0
  167. package/pkgs/schema/src/icon-name.ts +21 -0
  168. package/pkgs/schema/src/ids.ts +14 -0
  169. package/pkgs/schema/src/index.ts +169 -0
  170. package/pkgs/schema/src/migrate.ts +297 -0
  171. package/pkgs/schema/src/node.ts +229 -0
  172. package/pkgs/schema/src/paths.ts +28 -0
  173. package/pkgs/schema/src/repo.ts +134 -0
  174. package/pkgs/schema/src/screen.ts +30 -0
  175. package/pkgs/schema/src/snippet-resolve.ts +193 -0
  176. package/pkgs/schema/src/snippet.ts +104 -0
  177. package/pkgs/schema/src/svg-sanitize.ts +487 -0
  178. package/pkgs/schema/src/theme.ts +218 -0
  179. package/pkgs/schema/src/typeset.ts +693 -0
  180. package/pkgs/schema/src/validate-ids.ts +46 -0
  181. package/pkgs/schema/src/viewport.ts +8 -0
  182. package/pkgs/shadcn-snapshot/dist/manifest.json +8109 -0
  183. package/pkgs/shadcn-snapshot/src/components/canvas-portal.tsx +55 -0
  184. package/pkgs/shadcn-snapshot/src/components/ui/accordion.tsx +122 -0
  185. package/pkgs/shadcn-snapshot/src/components/ui/alert-dialog.tsx +164 -0
  186. package/pkgs/shadcn-snapshot/src/components/ui/alert.tsx +74 -0
  187. package/pkgs/shadcn-snapshot/src/components/ui/aspect-ratio.tsx +13 -0
  188. package/pkgs/shadcn-snapshot/src/components/ui/attachment.tsx +197 -0
  189. package/pkgs/shadcn-snapshot/src/components/ui/avatar.tsx +100 -0
  190. package/pkgs/shadcn-snapshot/src/components/ui/badge.tsx +50 -0
  191. package/pkgs/shadcn-snapshot/src/components/ui/breadcrumb.tsx +109 -0
  192. package/pkgs/shadcn-snapshot/src/components/ui/bubble.tsx +130 -0
  193. package/pkgs/shadcn-snapshot/src/components/ui/button-group.tsx +82 -0
  194. package/pkgs/shadcn-snapshot/src/components/ui/button.tsx +72 -0
  195. package/pkgs/shadcn-snapshot/src/components/ui/calendar.tsx +229 -0
  196. package/pkgs/shadcn-snapshot/src/components/ui/card.tsx +92 -0
  197. package/pkgs/shadcn-snapshot/src/components/ui/carousel.tsx +235 -0
  198. package/pkgs/shadcn-snapshot/src/components/ui/chart-option.ts +250 -0
  199. package/pkgs/shadcn-snapshot/src/components/ui/chart.tsx +177 -0
  200. package/pkgs/shadcn-snapshot/src/components/ui/checkbox.tsx +41 -0
  201. package/pkgs/shadcn-snapshot/src/components/ui/collapsible.tsx +32 -0
  202. package/pkgs/shadcn-snapshot/src/components/ui/combobox.tsx +196 -0
  203. package/pkgs/shadcn-snapshot/src/components/ui/context-menu.tsx +227 -0
  204. package/pkgs/shadcn-snapshot/src/components/ui/dialog.tsx +131 -0
  205. package/pkgs/shadcn-snapshot/src/components/ui/direction.tsx +24 -0
  206. package/pkgs/shadcn-snapshot/src/components/ui/drawer.tsx +112 -0
  207. package/pkgs/shadcn-snapshot/src/components/ui/dropdown-menu.tsx +205 -0
  208. package/pkgs/shadcn-snapshot/src/components/ui/empty.tsx +98 -0
  209. package/pkgs/shadcn-snapshot/src/components/ui/field.tsx +228 -0
  210. package/pkgs/shadcn-snapshot/src/components/ui/hover-card.tsx +51 -0
  211. package/pkgs/shadcn-snapshot/src/components/ui/input-group.tsx +148 -0
  212. package/pkgs/shadcn-snapshot/src/components/ui/input.tsx +43 -0
  213. package/pkgs/shadcn-snapshot/src/components/ui/item.tsx +187 -0
  214. package/pkgs/shadcn-snapshot/src/components/ui/kbd.tsx +30 -0
  215. package/pkgs/shadcn-snapshot/src/components/ui/label.tsx +25 -0
  216. package/pkgs/shadcn-snapshot/src/components/ui/marker.tsx +71 -0
  217. package/pkgs/shadcn-snapshot/src/components/ui/menubar.tsx +243 -0
  218. package/pkgs/shadcn-snapshot/src/components/ui/message.tsx +89 -0
  219. package/pkgs/shadcn-snapshot/src/components/ui/native-select.tsx +60 -0
  220. package/pkgs/shadcn-snapshot/src/components/ui/navigation-menu.tsx +165 -0
  221. package/pkgs/shadcn-snapshot/src/components/ui/pagination.tsx +117 -0
  222. package/pkgs/shadcn-snapshot/src/components/ui/popover.tsx +66 -0
  223. package/pkgs/shadcn-snapshot/src/components/ui/progress.tsx +35 -0
  224. package/pkgs/shadcn-snapshot/src/components/ui/radio-group.tsx +48 -0
  225. package/pkgs/shadcn-snapshot/src/components/ui/scroll-area.tsx +59 -0
  226. package/pkgs/shadcn-snapshot/src/components/ui/select.tsx +180 -0
  227. package/pkgs/shadcn-snapshot/src/components/ui/separator.tsx +32 -0
  228. package/pkgs/shadcn-snapshot/src/components/ui/sheet.tsx +107 -0
  229. package/pkgs/shadcn-snapshot/src/components/ui/skeleton.tsx +17 -0
  230. package/pkgs/shadcn-snapshot/src/components/ui/slider.tsx +68 -0
  231. package/pkgs/shadcn-snapshot/src/components/ui/sonner.tsx +91 -0
  232. package/pkgs/shadcn-snapshot/src/components/ui/spinner.tsx +21 -0
  233. package/pkgs/shadcn-snapshot/src/components/ui/switch.tsx +41 -0
  234. package/pkgs/shadcn-snapshot/src/components/ui/table.tsx +93 -0
  235. package/pkgs/shadcn-snapshot/src/components/ui/tabs.tsx +84 -0
  236. package/pkgs/shadcn-snapshot/src/components/ui/textarea.tsx +24 -0
  237. package/pkgs/shadcn-snapshot/src/components/ui/toggle-group.tsx +94 -0
  238. package/pkgs/shadcn-snapshot/src/components/ui/toggle.tsx +50 -0
  239. package/pkgs/shadcn-snapshot/src/components/ui/tooltip.tsx +47 -0
  240. package/pkgs/shadcn-snapshot/src/examples.ts +36 -0
  241. package/pkgs/shadcn-snapshot/src/groups.ts +87 -0
  242. package/pkgs/shadcn-snapshot/src/index.ts +43 -0
  243. package/pkgs/shadcn-snapshot/src/lib/utils.ts +4 -0
  244. package/pkgs/shadcn-snapshot/src/manifest.ts +12 -0
  245. package/pkgs/shadcn-snapshot/src/notes.ts +97 -0
  246. package/pkgs/shadcn-snapshot/src/paths.ts +47 -0
  247. package/pkgs/shadcn-snapshot/src/registry-files.ts +293 -0
  248. package/pkgs/shadcn-snapshot/src/registry.ts +631 -0
  249. package/pkgs/shadcn-snapshot/src/shadcn-tailwind.css +632 -0
  250. package/pkgs/shadcn-snapshot/src/tailwind-entry.css +159 -0
  251. package/pkgs/shadcn-snapshot/src/version.ts +8 -0
  252. package/plugins/claude/velloo/.claude-plugin/plugin.json +6 -0
  253. package/plugins/claude/velloo/agents/velloo-design-reviewer.md +33 -0
  254. package/plugins/claude/velloo/agents/velloo-designer.md +39 -0
  255. package/plugins/claude/velloo/commands/design.md +11 -0
  256. package/plugins/claude/velloo/commands/implement.md +10 -0
  257. package/plugins/claude/velloo/commands/review.md +8 -0
  258. package/plugins/gemini/GEMINI.md +31 -0
  259. package/plugins/gemini/commands/velloo/design.toml +10 -0
  260. package/plugins/gemini/commands/velloo/implement.toml +9 -0
  261. package/plugins/gemini/gemini-extension.json +9 -0
  262. package/skills/velloo-brand/SKILL.md +124 -0
  263. package/skills/velloo-design/SKILL.md +144 -0
  264. package/skills/velloo-design-system/SKILL.md +90 -0
  265. package/skills/velloo-implement/SKILL.md +101 -0
  266. package/skills/velloo-logo/SKILL.md +93 -0
  267. package/skills/velloo-setup/SKILL.md +156 -0
@@ -0,0 +1,690 @@
1
+ // Generated by scripts/build-font-catalog.ts — do not edit by hand.
2
+ //
3
+ // The selection is editorial; the axis specs are read from Google's font
4
+ // metadata, so every entry here is one the css2 API actually answers.
5
+
6
+ /** What a family is good for — drives grouping and the default role name. */
7
+ export type FontRole = "body" | "display" | "mono";
8
+
9
+ /** How the picker groups families. Google's own classification. */
10
+ export type FontCategory = "sans" | "serif" | "display" | "mono" | "handwriting";
11
+
12
+ export interface CatalogFont {
13
+ /** Family name, spelled as Google Fonts spells it. */
14
+ family: string;
15
+ category: FontCategory;
16
+ /**
17
+ * The css2 axis spec — the part after `family=<name>:` — or `true` for a
18
+ * family with nothing worth asking for beyond its single weight. Exactly the
19
+ * shape `set_fonts` takes as its `google` argument.
20
+ */
21
+ google: string | true;
22
+ /** CSS stack tail, following the family's skeleton rather than its role. */
23
+ fallback: string;
24
+ roles: FontRole[];
25
+ note: string;
26
+ }
27
+
28
+ export const FONT_CATALOG: CatalogFont[] = [
29
+ {
30
+ family: "Inter",
31
+ category: "sans",
32
+ google: "ital,opsz,wght@0,14..32,100..900;1,14..32,100..900",
33
+ fallback: "ui-sans-serif, system-ui, sans-serif",
34
+ roles: ["body"],
35
+ note: "The default for a reason — neutral, huge x-height, drawn for screens.",
36
+ },
37
+ {
38
+ family: "Geist",
39
+ category: "sans",
40
+ google: "ital,wght@0,100..900;1,100..900",
41
+ fallback: "ui-sans-serif, system-ui, sans-serif",
42
+ roles: ["body"],
43
+ note: "Tighter and more opinionated than Inter, with a mono twin.",
44
+ },
45
+ {
46
+ family: "Manrope",
47
+ category: "sans",
48
+ google: "wght@200..800",
49
+ fallback: "ui-sans-serif, system-ui, sans-serif",
50
+ roles: ["body", "display"],
51
+ note: "Semi-rounded geometric; friendly without going soft.",
52
+ },
53
+ {
54
+ family: "DM Sans",
55
+ category: "sans",
56
+ google: "ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000",
57
+ fallback: "ui-sans-serif, system-ui, sans-serif",
58
+ roles: ["body", "display"],
59
+ note: "Low-contrast geometric that stays clean when set large.",
60
+ },
61
+ {
62
+ family: "Plus Jakarta Sans",
63
+ category: "sans",
64
+ google: "ital,wght@0,200..800;1,200..800",
65
+ fallback: "ui-sans-serif, system-ui, sans-serif",
66
+ roles: ["body", "display"],
67
+ note: "Geometric bones, humanist warmth; strong at both ends.",
68
+ },
69
+ {
70
+ family: "Work Sans",
71
+ category: "sans",
72
+ google: "ital,wght@0,100..900;1,100..900",
73
+ fallback: "ui-sans-serif, system-ui, sans-serif",
74
+ roles: ["body"],
75
+ note: "Optimised for the middle sizes where most interface text lives.",
76
+ },
77
+ {
78
+ family: "Public Sans",
79
+ category: "sans",
80
+ google: "ital,wght@0,100..900;1,100..900",
81
+ fallback: "ui-sans-serif, system-ui, sans-serif",
82
+ roles: ["body"],
83
+ note: "Unopinionated on purpose — a neutral civic workhorse.",
84
+ },
85
+ {
86
+ family: "Figtree",
87
+ category: "sans",
88
+ google: "ital,wght@0,300..900;1,300..900",
89
+ fallback: "ui-sans-serif, system-ui, sans-serif",
90
+ roles: ["body", "display"],
91
+ note: "Geometric with a little play in it; holds up small.",
92
+ },
93
+ {
94
+ family: "Outfit",
95
+ category: "sans",
96
+ google: "wght@100..900",
97
+ fallback: "ui-sans-serif, system-ui, sans-serif",
98
+ roles: ["display", "body"],
99
+ note: "Purely geometric — strongest as a heading face.",
100
+ },
101
+ {
102
+ family: "Sora",
103
+ category: "sans",
104
+ google: "wght@100..800",
105
+ fallback: "ui-sans-serif, system-ui, sans-serif",
106
+ roles: ["display", "body"],
107
+ note: "Angular and technical; a display sans that isn't generic.",
108
+ },
109
+ {
110
+ family: "Lexend",
111
+ category: "sans",
112
+ google: "wght@100..900",
113
+ fallback: "ui-sans-serif, system-ui, sans-serif",
114
+ roles: ["body"],
115
+ note: "Wide apertures, tuned for reading proficiency.",
116
+ },
117
+ {
118
+ family: "Rubik",
119
+ category: "sans",
120
+ google: "ital,wght@0,300..900;1,300..900",
121
+ fallback: "ui-sans-serif, system-ui, sans-serif",
122
+ roles: ["body", "display"],
123
+ note: "Softened corners; warm and sturdy at any weight.",
124
+ },
125
+ {
126
+ family: "Karla",
127
+ category: "sans",
128
+ google: "ital,wght@0,200..800;1,200..800",
129
+ fallback: "ui-sans-serif, system-ui, sans-serif",
130
+ roles: ["body"],
131
+ note: "A grotesque with quirks — good foil for a serif display.",
132
+ },
133
+ {
134
+ family: "Mulish",
135
+ category: "sans",
136
+ google: "ital,wght@0,200..1000;1,200..1000",
137
+ fallback: "ui-sans-serif, system-ui, sans-serif",
138
+ roles: ["body"],
139
+ note: "Minimal and quiet enough to disappear behind the content.",
140
+ },
141
+ {
142
+ family: "Nunito Sans",
143
+ category: "sans",
144
+ google: "ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000",
145
+ fallback: "ui-sans-serif, system-ui, sans-serif",
146
+ roles: ["body"],
147
+ note: "Rounded terminals; an approachable product voice.",
148
+ },
149
+ {
150
+ family: "Source Sans 3",
151
+ category: "sans",
152
+ google: "ital,wght@0,200..900;1,200..900",
153
+ fallback: "ui-sans-serif, system-ui, sans-serif",
154
+ roles: ["body"],
155
+ note: "Adobe's interface workhorse; very legible small.",
156
+ },
157
+ {
158
+ family: "IBM Plex Sans",
159
+ category: "sans",
160
+ google: "ital,wght@0,100..700;1,100..700",
161
+ fallback: "ui-sans-serif, system-ui, sans-serif",
162
+ roles: ["body"],
163
+ note: "Engineered and faintly technical; pairs with Plex Mono.",
164
+ },
165
+ {
166
+ family: "Barlow",
167
+ category: "sans",
168
+ google: "wght@400;500;600;700",
169
+ fallback: "ui-sans-serif, system-ui, sans-serif",
170
+ roles: ["body"],
171
+ note: "Slightly condensed grotesque when you need the density.",
172
+ },
173
+ {
174
+ family: "Archivo",
175
+ category: "sans",
176
+ google: "ital,wght@0,100..900;1,100..900",
177
+ fallback: "ui-sans-serif, system-ui, sans-serif",
178
+ roles: ["body", "display"],
179
+ note: "Built for high performance; enormous weight range.",
180
+ },
181
+ {
182
+ family: "Epilogue",
183
+ category: "sans",
184
+ google: "ital,wght@0,100..900;1,100..900",
185
+ fallback: "ui-sans-serif, system-ui, sans-serif",
186
+ roles: ["display", "body"],
187
+ note: "Real personality once you get into the heavy weights.",
188
+ },
189
+ {
190
+ family: "Onest",
191
+ category: "sans",
192
+ google: "wght@100..900",
193
+ fallback: "ui-sans-serif, system-ui, sans-serif",
194
+ roles: ["body"],
195
+ note: "Contemporary neutral — a fresher take on the Inter brief.",
196
+ },
197
+ {
198
+ family: "Instrument Sans",
199
+ category: "sans",
200
+ google: "ital,wght@0,400..700;1,400..700",
201
+ fallback: "ui-sans-serif, system-ui, sans-serif",
202
+ roles: ["body"],
203
+ note: "Compact grotesque; sharp and very current.",
204
+ },
205
+ {
206
+ family: "Schibsted Grotesk",
207
+ category: "sans",
208
+ google: "ital,wght@0,400..900;1,400..900",
209
+ fallback: "ui-sans-serif, system-ui, sans-serif",
210
+ roles: ["body", "display"],
211
+ note: "Editorial grotesque, drawn for a news publisher.",
212
+ },
213
+ {
214
+ family: "Space Grotesk",
215
+ category: "sans",
216
+ google: "wght@300..700",
217
+ fallback: "ui-sans-serif, system-ui, sans-serif",
218
+ roles: ["display", "body"],
219
+ note: "Techy proportions; the default startup display face.",
220
+ },
221
+ {
222
+ family: "Albert Sans",
223
+ category: "sans",
224
+ google: "ital,wght@0,100..900;1,100..900",
225
+ fallback: "ui-sans-serif, system-ui, sans-serif",
226
+ roles: ["body", "display"],
227
+ note: "Geometric revival with an elegant lowercase.",
228
+ },
229
+ {
230
+ family: "Bricolage Grotesque",
231
+ category: "sans",
232
+ google: "opsz,wght@12..96,200..800",
233
+ fallback: "ui-sans-serif, system-ui, sans-serif",
234
+ roles: ["display"],
235
+ note: "Deliberately awkward — excellent editorial headers.",
236
+ },
237
+ {
238
+ family: "Hanken Grotesk",
239
+ category: "sans",
240
+ google: "ital,wght@0,100..900;1,100..900",
241
+ fallback: "ui-sans-serif, system-ui, sans-serif",
242
+ roles: ["body"],
243
+ note: "Even-toned grotesque that never draws attention.",
244
+ },
245
+ {
246
+ family: "Be Vietnam Pro",
247
+ category: "sans",
248
+ google: "wght@400;500;600;700",
249
+ fallback: "ui-sans-serif, system-ui, sans-serif",
250
+ roles: ["body"],
251
+ note: "Crisp geometric with unusually good diacritics.",
252
+ },
253
+ {
254
+ family: "Red Hat Display",
255
+ category: "sans",
256
+ google: "ital,wght@0,300..900;1,300..900",
257
+ fallback: "ui-sans-serif, system-ui, sans-serif",
258
+ roles: ["display", "body"],
259
+ note: "Open and geometric; corporate without being cold.",
260
+ },
261
+ {
262
+ family: "Source Serif 4",
263
+ category: "serif",
264
+ google: "ital,opsz,wght@0,8..60,200..900;1,8..60,200..900",
265
+ fallback: "ui-serif, Georgia, serif",
266
+ roles: ["body"],
267
+ note: "Screen-first serif; comfortable over long measures.",
268
+ },
269
+ {
270
+ family: "Lora",
271
+ category: "serif",
272
+ google: "ital,wght@0,400..700;1,400..700",
273
+ fallback: "ui-serif, Georgia, serif",
274
+ roles: ["body"],
275
+ note: "Brushed curves — the safe editorial body serif.",
276
+ },
277
+ {
278
+ family: "Crimson Pro",
279
+ category: "serif",
280
+ google: "ital,wght@0,200..900;1,200..900",
281
+ fallback: "ui-serif, Georgia, serif",
282
+ roles: ["body"],
283
+ note: "Old-style and book-like; low contrast survives screens.",
284
+ },
285
+ {
286
+ family: "Newsreader",
287
+ category: "serif",
288
+ google: "ital,opsz,wght@0,6..72,200..800;1,6..72,200..800",
289
+ fallback: "ui-serif, Georgia, serif",
290
+ roles: ["body", "display"],
291
+ note: "Made for news, so it works as body and as headline.",
292
+ },
293
+ {
294
+ family: "Literata",
295
+ category: "serif",
296
+ google: "ital,opsz,wght@0,7..72,200..900;1,7..72,200..900",
297
+ fallback: "ui-serif, Georgia, serif",
298
+ roles: ["body"],
299
+ note: "Google's ebook serif; sturdy down to small sizes.",
300
+ },
301
+ {
302
+ family: "Spectral",
303
+ category: "serif",
304
+ google: "wght@400;500;600;700",
305
+ fallback: "ui-serif, Georgia, serif",
306
+ roles: ["body"],
307
+ note: "Screen-native serif with generous built-in spacing.",
308
+ },
309
+ {
310
+ family: "EB Garamond",
311
+ category: "serif",
312
+ google: "ital,wght@0,400..800;1,400..800",
313
+ fallback: "ui-serif, Georgia, serif",
314
+ roles: ["body"],
315
+ note: "Classic Garamond — give it size and leading to breathe.",
316
+ },
317
+ {
318
+ family: "Libre Baskerville",
319
+ category: "serif",
320
+ google: "ital,wght@0,400..700;1,400..700",
321
+ fallback: "ui-serif, Georgia, serif",
322
+ roles: ["body"],
323
+ note: "High contrast; set it larger than you think.",
324
+ },
325
+ {
326
+ family: "Bitter",
327
+ category: "serif",
328
+ google: "ital,wght@0,100..900;1,100..900",
329
+ fallback: "ui-serif, Georgia, serif",
330
+ roles: ["body"],
331
+ note: "Contemporary slab that stays strong at small sizes.",
332
+ },
333
+ {
334
+ family: "Merriweather",
335
+ category: "serif",
336
+ google: "ital,opsz,wght@0,18..144,300..900;1,18..144,300..900",
337
+ fallback: "ui-serif, Georgia, serif",
338
+ roles: ["body"],
339
+ note: "Dense and dark; built to be legible on screens.",
340
+ },
341
+ {
342
+ family: "Petrona",
343
+ category: "serif",
344
+ google: "ital,wght@0,100..900;1,100..900",
345
+ fallback: "ui-serif, Georgia, serif",
346
+ roles: ["body", "display"],
347
+ note: "Quirky text serif with a surprisingly wide range.",
348
+ },
349
+ {
350
+ family: "Faustina",
351
+ category: "serif",
352
+ google: "ital,wght@0,300..800;1,300..800",
353
+ fallback: "ui-serif, Georgia, serif",
354
+ roles: ["body"],
355
+ note: "Compact serif that packs a lot of words per line.",
356
+ },
357
+ {
358
+ family: "Frank Ruhl Libre",
359
+ category: "serif",
360
+ google: "wght@300..900",
361
+ fallback: "ui-serif, Georgia, serif",
362
+ roles: ["body", "display"],
363
+ note: "Hebrew-rooted serif with striking heavy weights.",
364
+ },
365
+ {
366
+ family: "Vollkorn",
367
+ category: "serif",
368
+ google: "ital,wght@0,400..900;1,400..900",
369
+ fallback: "ui-serif, Georgia, serif",
370
+ roles: ["body"],
371
+ note: "Warm and slightly rough — good for long-form.",
372
+ },
373
+ {
374
+ family: "Zilla Slab",
375
+ category: "serif",
376
+ google: "wght@400;500;600;700",
377
+ fallback: "ui-serif, Georgia, serif",
378
+ roles: ["body", "display"],
379
+ note: "Mozilla's slab: technical but warm.",
380
+ },
381
+ {
382
+ family: "Roboto Slab",
383
+ category: "serif",
384
+ google: "wght@100..900",
385
+ fallback: "ui-serif, Georgia, serif",
386
+ roles: ["body", "display"],
387
+ note: "Neutral slab; the least surprising choice.",
388
+ },
389
+ {
390
+ family: "Alegreya",
391
+ category: "serif",
392
+ google: "ital,wght@0,400..900;1,400..900",
393
+ fallback: "ui-serif, Georgia, serif",
394
+ roles: ["body"],
395
+ note: "Calligraphic origins, designed for literature.",
396
+ },
397
+ {
398
+ family: "Instrument Serif",
399
+ category: "serif",
400
+ google: "wght@400",
401
+ fallback: "ui-serif, Georgia, serif",
402
+ roles: ["display"],
403
+ note: "High-contrast display serif; everywhere right now.",
404
+ },
405
+ {
406
+ family: "Fraunces",
407
+ category: "serif",
408
+ google: "opsz,wght@9..144,100..900",
409
+ fallback: "ui-serif, Georgia, serif",
410
+ roles: ["display"],
411
+ note: "Wonky and optical-size aware; enormous personality.",
412
+ },
413
+ {
414
+ family: "Playfair Display",
415
+ category: "serif",
416
+ google: "wght@400..900",
417
+ fallback: "ui-serif, Georgia, serif",
418
+ roles: ["display"],
419
+ note: "High-contrast Didone — the classic luxury headline.",
420
+ },
421
+ {
422
+ family: "DM Serif Display",
423
+ category: "serif",
424
+ google: "wght@400",
425
+ fallback: "ui-serif, Georgia, serif",
426
+ roles: ["display"],
427
+ note: "Sharp Didone; the free Playfair alternative.",
428
+ },
429
+ {
430
+ family: "Bodoni Moda",
431
+ category: "serif",
432
+ google: "opsz,wght@6..96,400..900",
433
+ fallback: "ui-serif, Georgia, serif",
434
+ roles: ["display"],
435
+ note: "True Didone with optical sizes; fashion-editorial.",
436
+ },
437
+ {
438
+ family: "Abril Fatface",
439
+ category: "display",
440
+ google: "wght@400",
441
+ fallback: "ui-serif, Georgia, serif",
442
+ roles: ["display"],
443
+ note: "Heavy Didone built for one-word headlines.",
444
+ },
445
+ {
446
+ family: "Unbounded",
447
+ category: "sans",
448
+ google: "wght@200..900",
449
+ fallback: "ui-sans-serif, system-ui, sans-serif",
450
+ roles: ["display"],
451
+ note: "Geometric and loud; unmistakably brand-forward.",
452
+ },
453
+ {
454
+ family: "Syne",
455
+ category: "sans",
456
+ google: "wght@400..800",
457
+ fallback: "ui-sans-serif, system-ui, sans-serif",
458
+ roles: ["display"],
459
+ note: "Deliberately odd widths — art-gallery energy.",
460
+ },
461
+ {
462
+ family: "Anton",
463
+ category: "sans",
464
+ google: "wght@400",
465
+ fallback: "ui-sans-serif, system-ui, sans-serif",
466
+ roles: ["display"],
467
+ note: "Ultra-condensed poster weight, and only that.",
468
+ },
469
+ {
470
+ family: "Bebas Neue",
471
+ category: "sans",
472
+ google: "wght@400",
473
+ fallback: "ui-sans-serif, system-ui, sans-serif",
474
+ roles: ["display"],
475
+ note: "All-caps condensed; posters and scoreboards.",
476
+ },
477
+ {
478
+ family: "Archivo Black",
479
+ category: "sans",
480
+ google: "wght@400",
481
+ fallback: "ui-sans-serif, system-ui, sans-serif",
482
+ roles: ["display"],
483
+ note: "One very heavy weight that fills the line.",
484
+ },
485
+ {
486
+ family: "Big Shoulders",
487
+ category: "display",
488
+ google: "opsz,wght@10..72,100..900",
489
+ fallback: "ui-sans-serif, system-ui, sans-serif",
490
+ roles: ["display"],
491
+ note: "Tall condensed; wayfinding and data-dense headers.",
492
+ },
493
+ {
494
+ family: "Cormorant Garamond",
495
+ category: "serif",
496
+ google: "wght@300..700",
497
+ fallback: "ui-serif, Georgia, serif",
498
+ roles: ["display"],
499
+ note: "Delicate high contrast — set it big or lose it.",
500
+ },
501
+ {
502
+ family: "Marcellus",
503
+ category: "serif",
504
+ google: "wght@400",
505
+ fallback: "ui-serif, Georgia, serif",
506
+ roles: ["display"],
507
+ note: "Roman inscriptional capitals; quietly expensive.",
508
+ },
509
+ {
510
+ family: "Prata",
511
+ category: "serif",
512
+ google: "wght@400",
513
+ fallback: "ui-serif, Georgia, serif",
514
+ roles: ["display"],
515
+ note: "Didone with a single weight and a lot of poise.",
516
+ },
517
+ {
518
+ family: "Gloock",
519
+ category: "serif",
520
+ google: "wght@400",
521
+ fallback: "ui-serif, Georgia, serif",
522
+ roles: ["display"],
523
+ note: "Fat high-contrast serif; editorial and current.",
524
+ },
525
+ {
526
+ family: "Yeseva One",
527
+ category: "display",
528
+ google: "wght@400",
529
+ fallback: "ui-serif, Georgia, serif",
530
+ roles: ["display"],
531
+ note: "Curvy and decorative; strong at very large sizes.",
532
+ },
533
+ {
534
+ family: "Chivo",
535
+ category: "sans",
536
+ google: "ital,wght@0,100..900;1,100..900",
537
+ fallback: "ui-sans-serif, system-ui, sans-serif",
538
+ roles: ["display", "body"],
539
+ note: "Grotesque with heavy weights made for headlines.",
540
+ },
541
+ {
542
+ family: "Darker Grotesque",
543
+ category: "sans",
544
+ google: "wght@300..900",
545
+ fallback: "ui-sans-serif, system-ui, sans-serif",
546
+ roles: ["display"],
547
+ note: "Tall, tight and slightly strange.",
548
+ },
549
+ {
550
+ family: "Familjen Grotesk",
551
+ category: "sans",
552
+ google: "ital,wght@0,400..700;1,400..700",
553
+ fallback: "ui-sans-serif, system-ui, sans-serif",
554
+ roles: ["display", "body"],
555
+ note: "Swedish grotesque with a distinctive g.",
556
+ },
557
+ {
558
+ family: "Caveat",
559
+ category: "handwriting",
560
+ google: "wght@400..700",
561
+ fallback: "ui-rounded, cursive",
562
+ roles: ["display"],
563
+ note: "Handwritten — annotations and margin notes.",
564
+ },
565
+ {
566
+ family: "JetBrains Mono",
567
+ category: "mono",
568
+ google: "wght@100..800",
569
+ fallback: "ui-monospace, SFMono-Regular, monospace",
570
+ roles: ["mono"],
571
+ note: "Tall x-height, drawn for code; the safe default.",
572
+ },
573
+ {
574
+ family: "IBM Plex Mono",
575
+ category: "mono",
576
+ google: "wght@400;500;600;700",
577
+ fallback: "ui-monospace, SFMono-Regular, monospace",
578
+ roles: ["mono"],
579
+ note: "Pairs exactly with Plex Sans and Plex Serif.",
580
+ },
581
+ {
582
+ family: "Space Mono",
583
+ category: "mono",
584
+ google: "wght@400;700",
585
+ fallback: "ui-monospace, SFMono-Regular, monospace",
586
+ roles: ["mono", "display"],
587
+ note: "Retro-technical and quirky; works as display mono.",
588
+ },
589
+ {
590
+ family: "Fira Code",
591
+ category: "mono",
592
+ google: "wght@300..700",
593
+ fallback: "ui-monospace, SFMono-Regular, monospace",
594
+ roles: ["mono"],
595
+ note: "Programming ligatures, if you want them.",
596
+ },
597
+ {
598
+ family: "Source Code Pro",
599
+ category: "mono",
600
+ google: "wght@200..900",
601
+ fallback: "ui-monospace, SFMono-Regular, monospace",
602
+ roles: ["mono"],
603
+ note: "Adobe's code face; wide and unambiguous.",
604
+ },
605
+ {
606
+ family: "Roboto Mono",
607
+ category: "mono",
608
+ google: "wght@100..700",
609
+ fallback: "ui-monospace, SFMono-Regular, monospace",
610
+ roles: ["mono"],
611
+ note: "The neutral default nobody objects to.",
612
+ },
613
+ {
614
+ family: "DM Mono",
615
+ category: "mono",
616
+ google: "wght@400;500",
617
+ fallback: "ui-monospace, SFMono-Regular, monospace",
618
+ roles: ["mono"],
619
+ note: "Low contrast and geometric; unusually pretty.",
620
+ },
621
+ {
622
+ family: "Geist Mono",
623
+ category: "mono",
624
+ google: "wght@100..900",
625
+ fallback: "ui-monospace, SFMono-Regular, monospace",
626
+ roles: ["mono"],
627
+ note: "Geist's twin — use them together.",
628
+ },
629
+ {
630
+ family: "Martian Mono",
631
+ category: "mono",
632
+ google: "wght@100..800",
633
+ fallback: "ui-monospace, SFMono-Regular, monospace",
634
+ roles: ["mono", "display"],
635
+ note: "Wide and semi-condensed; a mono with presence.",
636
+ },
637
+ {
638
+ family: "Azeret Mono",
639
+ category: "mono",
640
+ google: "wght@100..900",
641
+ fallback: "ui-monospace, SFMono-Regular, monospace",
642
+ roles: ["mono"],
643
+ note: "Boxy and deliberate; good for figures and tables.",
644
+ },
645
+ {
646
+ family: "Red Hat Mono",
647
+ category: "mono",
648
+ google: "wght@300..700",
649
+ fallback: "ui-monospace, SFMono-Regular, monospace",
650
+ roles: ["mono"],
651
+ note: "Humanist mono that reads like prose.",
652
+ },
653
+ {
654
+ family: "Inconsolata",
655
+ category: "mono",
656
+ google: "wght@200..900",
657
+ fallback: "ui-monospace, SFMono-Regular, monospace",
658
+ roles: ["mono"],
659
+ note: "Narrow and efficient; fits more columns.",
660
+ },
661
+ ];
662
+
663
+ /** Families offered for a role, best fit first. */
664
+ export function fontsForRole(role: FontRole): CatalogFont[] {
665
+ return FONT_CATALOG.filter((f) => f.roles.includes(role)).sort(
666
+ (a, b) => a.roles.indexOf(role) - b.roles.indexOf(role),
667
+ );
668
+ }
669
+
670
+ /** Look up a curated family by name. */
671
+ export function catalogFont(family: string): CatalogFont | undefined {
672
+ return FONT_CATALOG.find((f) => f.family === family);
673
+ }
674
+
675
+ /** The CSS stack `set_fonts` would write for this family. */
676
+ export function fontStack(font: CatalogFont): string {
677
+ return `"${font.family}", ${font.fallback}`;
678
+ }
679
+
680
+ /**
681
+ * A css2 URL for one family. `text` asks Google for just the glyphs in that
682
+ * string, which is how the picker previews eighty families without pulling
683
+ * eighty full fonts.
684
+ */
685
+ export function googleFontUrl(spec: string | true, family: string, text?: string): string {
686
+ const param = family.replace(/ /g, "+");
687
+ const value = spec === true ? param : `${param}:${spec}`;
688
+ const suffix = text ? `&text=${encodeURIComponent(text)}` : "";
689
+ return `https://fonts.googleapis.com/css2?family=${value}${suffix}&display=swap`;
690
+ }