@nuxtseo/design-system 0.0.0-stage → 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 (225) hide show
  1. package/README.md +101 -2
  2. package/app/app.config.ts +596 -0
  3. package/app/component-attributes.d.ts +8 -0
  4. package/app/components/README.md +38 -0
  5. package/app/components/brand/UiBrandMark.vue +63 -0
  6. package/app/components/brand/UiModuleIcon.vue +45 -0
  7. package/app/components/brand/UiProLettering.vue +26 -0
  8. package/app/components/brand/UiRangeTerrain.vue +311 -0
  9. package/app/components/brand/range-glyphs.ts +18 -0
  10. package/app/components/container/BrowserTab.vue +145 -0
  11. package/app/components/container/IntegrationRow.vue +120 -0
  12. package/app/components/container/KnowDoRails.vue +40 -0
  13. package/app/components/container/RailPanel.vue +93 -0
  14. package/app/components/container/UiAlert.vue +120 -0
  15. package/app/components/container/UiAppShell.vue +165 -0
  16. package/app/components/container/UiCard.vue +204 -0
  17. package/app/components/container/UiDisclosure.vue +43 -0
  18. package/app/components/container/UiEmptyState.vue +102 -0
  19. package/app/components/container/UiErrorState.vue +61 -0
  20. package/app/components/container/UiLoadingState.vue +20 -0
  21. package/app/components/container/UiPageHeader.vue +95 -0
  22. package/app/components/container/UiPopover.vue +102 -0
  23. package/app/components/container/UiSampleDataOverlay.vue +47 -0
  24. package/app/components/container/UiSectionHeader.vue +83 -0
  25. package/app/components/container/UiSummaryStrip.vue +40 -0
  26. package/app/components/container/UiToast.vue +230 -0
  27. package/app/components/container/UiToaster.vue +271 -0
  28. package/app/components/container/UiWidgetState.vue +143 -0
  29. package/app/components/container/ui-nav-drawer.ts +48 -0
  30. package/app/components/data/ActionRail.vue +152 -0
  31. package/app/components/data/FacetStrip.vue +93 -0
  32. package/app/components/data/UiBarCell.vue +37 -0
  33. package/app/components/data/UiBarChart.vue +281 -0
  34. package/app/components/data/UiChartAnnotationControls.vue +74 -0
  35. package/app/components/data/UiChartAnnotations.vue +163 -0
  36. package/app/components/data/UiChartBrushOverlay.vue +96 -0
  37. package/app/components/data/UiChartDataReveal.vue +81 -0
  38. package/app/components/data/UiChartFrame.vue +123 -0
  39. package/app/components/data/UiChartTooltip.vue +81 -0
  40. package/app/components/data/UiCoverageNote.vue +90 -0
  41. package/app/components/data/UiCwvSparkline.vue +188 -0
  42. package/app/components/data/UiDataList.vue +276 -0
  43. package/app/components/data/UiDataTableSection.vue +232 -0
  44. package/app/components/data/UiDistributionBars.vue +164 -0
  45. package/app/components/data/UiDonutChart.vue +132 -0
  46. package/app/components/data/UiDoorwayColumn.vue +108 -0
  47. package/app/components/data/UiDoorwayGrid.vue +58 -0
  48. package/app/components/data/UiDoorwayRow.vue +83 -0
  49. package/app/components/data/UiEntitySummary.vue +261 -0
  50. package/app/components/data/UiFactsGrid.vue +107 -0
  51. package/app/components/data/UiImpactMeter.vue +33 -0
  52. package/app/components/data/UiIssueRow.vue +149 -0
  53. package/app/components/data/UiLineChart.vue +431 -0
  54. package/app/components/data/UiMetricBarPopover.vue +121 -0
  55. package/app/components/data/UiMetricLabel.vue +30 -0
  56. package/app/components/data/UiMetricStat.vue +73 -0
  57. package/app/components/data/UiMetricToggle.vue +156 -0
  58. package/app/components/data/UiMetricsRow.vue +64 -0
  59. package/app/components/data/UiPositionMetric.vue +53 -0
  60. package/app/components/data/UiRelatedFixes.vue +113 -0
  61. package/app/components/data/UiRichText.vue +325 -0
  62. package/app/components/data/UiScatterPlot.vue +144 -0
  63. package/app/components/data/UiSeverityMarker.vue +30 -0
  64. package/app/components/data/UiSparkColumns.vue +160 -0
  65. package/app/components/data/UiSparkline.vue +681 -0
  66. package/app/components/data/UiStat.vue +445 -0
  67. package/app/components/data/UiStats.vue +76 -0
  68. package/app/components/data/UiTable.vue +517 -0
  69. package/app/components/data/UiTableExport.vue +40 -0
  70. package/app/components/data/UiTableFrame.vue +149 -0
  71. package/app/components/data/UiTablePagination.vue +37 -0
  72. package/app/components/data/UiTableShell.vue +48 -0
  73. package/app/components/data/UiTableTd.vue +39 -0
  74. package/app/components/data/UiTableTh.vue +61 -0
  75. package/app/components/data/UiTopEntityStackChart.vue +393 -0
  76. package/app/components/data/cells/UiTableDash.vue +3 -0
  77. package/app/components/data/cells/UiTableHeaderCell.vue +72 -0
  78. package/app/components/data/cells/UiTableMetricCell.vue +72 -0
  79. package/app/components/data/cells/UiTablePathCell.vue +76 -0
  80. package/app/components/data/cells/UiTableScoreTile.vue +49 -0
  81. package/app/components/data/cells/UiTableTrendCell.vue +41 -0
  82. package/app/components/element/UiAtmosphere.vue +64 -0
  83. package/app/components/element/UiAuthHeading.vue +24 -0
  84. package/app/components/element/UiAuthProviders.vue +46 -0
  85. package/app/components/element/UiBrandIcon.vue +39 -0
  86. package/app/components/element/UiButton.vue +549 -0
  87. package/app/components/element/UiCelebration.vue +65 -0
  88. package/app/components/element/UiChannelChips.vue +62 -0
  89. package/app/components/element/UiChatQuickLink.vue +319 -0
  90. package/app/components/element/UiChip.vue +101 -0
  91. package/app/components/element/UiConfirmDialog.vue +84 -0
  92. package/app/components/element/UiCopyButton.vue +90 -0
  93. package/app/components/element/UiDitherAvatar.vue +177 -0
  94. package/app/components/element/UiDotLabel.vue +19 -0
  95. package/app/components/element/UiEjectMenu.vue +181 -0
  96. package/app/components/element/UiEjectOverlay.vue +29 -0
  97. package/app/components/element/UiFavicon.vue +235 -0
  98. package/app/components/element/UiFaviconStack.vue +84 -0
  99. package/app/components/element/UiFilterMenu.vue +324 -0
  100. package/app/components/element/UiFocusedSurface.vue +129 -0
  101. package/app/components/element/UiHealthDot.vue +32 -0
  102. package/app/components/element/UiHelpLabel.vue +28 -0
  103. package/app/components/element/UiIcon.vue +22 -0
  104. package/app/components/element/UiInput.vue +35 -0
  105. package/app/components/element/UiLogoPlausible.vue +12 -0
  106. package/app/components/element/UiLogoPro.vue +25 -0
  107. package/app/components/element/UiLogoUmami.vue +12 -0
  108. package/app/components/element/UiNavIcon.vue +107 -0
  109. package/app/components/element/UiNavList.vue +156 -0
  110. package/app/components/element/UiOnboardingAnswers.vue +125 -0
  111. package/app/components/element/UiOverflowCount.vue +31 -0
  112. package/app/components/element/UiProBadge.vue +19 -0
  113. package/app/components/element/UiProgressCircle.vue +79 -0
  114. package/app/components/element/UiProgressPercent.vue +35 -0
  115. package/app/components/element/UiRelativeTime.vue +58 -0
  116. package/app/components/element/UiScrollFade.vue +16 -0
  117. package/app/components/element/UiSelect.vue +55 -0
  118. package/app/components/element/UiSeverityDot.vue +27 -0
  119. package/app/components/element/UiSiteFavicon.vue +24 -0
  120. package/app/components/element/UiSkeleton.vue +148 -0
  121. package/app/components/element/UiSlotText.vue +65 -0
  122. package/app/components/element/UiSourceLogos.vue +113 -0
  123. package/app/components/element/UiSourceRail.vue +71 -0
  124. package/app/components/element/UiStaticTerminal.vue +107 -0
  125. package/app/components/element/UiStatusBadge.vue +62 -0
  126. package/app/components/element/UiSyncDot.vue +46 -0
  127. package/app/components/element/UiTabs.vue +245 -0
  128. package/app/components/element/UiTogglePill.vue +86 -0
  129. package/app/components/element/UiTooltip.vue +390 -0
  130. package/app/components/element/UiTooltipGrid.vue +457 -0
  131. package/app/components/element/UiTrend.vue +184 -0
  132. package/app/components/element/UiTrialBenefits.vue +71 -0
  133. package/app/components/element/UiUrlLabel.vue +75 -0
  134. package/app/components/element/UiValenceDot.vue +37 -0
  135. package/app/components/element/UiWizardModal.vue +284 -0
  136. package/app/components/element/UiWizardNav.vue +59 -0
  137. package/app/components/element/UiWizardProgress.vue +78 -0
  138. package/app/components/element/UiWizardShell.vue +43 -0
  139. package/app/components/element/eject-menu.ts +359 -0
  140. package/app/components/element/eject-tracker.ts +34 -0
  141. package/app/components/element/onboarding-answers.ts +68 -0
  142. package/app/components/element/onboarding-intents.ts +19 -0
  143. package/app/composables/dataVizColors.ts +350 -0
  144. package/app/composables/formatting.ts +252 -0
  145. package/app/composables/semanticColors.ts +80 -0
  146. package/app/composables/useActivityOverlay.ts +55 -0
  147. package/app/composables/useAppShellNav.ts +21 -0
  148. package/app/composables/useChartBrush.ts +221 -0
  149. package/app/composables/useChartHover.ts +153 -0
  150. package/app/composables/useChartTickPlan.ts +186 -0
  151. package/app/composables/useCloseOnNavigation.ts +7 -0
  152. package/app/composables/useConfirm.ts +38 -0
  153. package/app/composables/useEdgeFade.ts +37 -0
  154. package/app/composables/useEject.ts +69 -0
  155. package/app/composables/useEntranceMotion.ts +22 -0
  156. package/app/composables/useFaviconBacking.ts +164 -0
  157. package/app/composables/useGridCols.ts +39 -0
  158. package/app/composables/useIssueOverlay.ts +43 -0
  159. package/app/composables/useOnboardingService.ts +106 -0
  160. package/app/composables/useOpenedOnce.ts +19 -0
  161. package/app/composables/useScrollSpy.ts +178 -0
  162. package/app/composables/useSquircleFallback.ts +21 -0
  163. package/app/composables/useWizardRoute.ts +123 -0
  164. package/app/plugins/favicon.ts +29 -0
  165. package/app/plugins/lazy-motion.ts +22 -0
  166. package/app/plugins/selection-highlight.client.ts +176 -0
  167. package/app/shared/cli.ts +10 -0
  168. package/app/shared/error-state.ts +150 -0
  169. package/app/shared/icons.ts +1 -0
  170. package/app/shared/motion.ts +1 -0
  171. package/app/shared/nav.ts +38 -0
  172. package/app/shared/navigation.ts +5 -0
  173. package/app/shared/static-terminal.ts +14 -0
  174. package/app/shared/table-export.ts +36 -0
  175. package/app/shared/table.ts +105 -0
  176. package/app/shared/ui-icons.ts +1 -0
  177. package/app/shared/view-state.ts +85 -0
  178. package/app/shared/widget-failure.ts +126 -0
  179. package/app/utils/chartAnnotations.ts +326 -0
  180. package/app/utils/countries.ts +337 -0
  181. package/app/utils/export-download.ts +100 -0
  182. package/app/utils/favicon-fallback.ts +12 -0
  183. package/app/utils/icon-color.ts +105 -0
  184. package/app/utils/number.ts +74 -0
  185. package/app/utils/render-link.ts +14 -0
  186. package/app/utils/slot-text.ts +18 -0
  187. package/app/utils/squircle.ts +154 -0
  188. package/app/utils/topEntityStack.ts +197 -0
  189. package/app/utils/vue.ts +57 -0
  190. package/build/design-system.ts +47 -0
  191. package/build/icon-server-bundle.ts +276 -0
  192. package/build/runtime/icon-server-bundle.ts +29 -0
  193. package/css/global.css +1892 -0
  194. package/nuxt.config.ts +132 -0
  195. package/package.json +86 -3
  196. package/public/brand-icon-dark.png +0 -0
  197. package/public/brand-icon-light.png +0 -0
  198. package/public/favicon-dark.png +0 -0
  199. package/public/favicon-light.png +0 -0
  200. package/public/favicon.ico +0 -0
  201. package/public/fonts/README.md +31 -0
  202. package/public/fonts/fira-code-OFL.txt +93 -0
  203. package/public/fonts/fira-code-var-latin.woff2 +0 -0
  204. package/public/fonts/hubot-sans-OFL.txt +93 -0
  205. package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
  206. package/public/icon-dark.png +0 -0
  207. package/public/icon.png +0 -0
  208. package/public/touch-icon-dark.png +0 -0
  209. package/public/touch-icon.png +0 -0
  210. package/server/api/favicon.get.ts +122 -0
  211. package/server/utils/favicon-source.ts +51 -0
  212. package/shared/contracts/feedback.ts +42 -0
  213. package/shared/contracts/index.ts +1 -0
  214. package/shared/delivery-channels.ts +53 -0
  215. package/shared/icons/index.ts +143 -0
  216. package/shared/icons/registry.ts +347 -0
  217. package/shared/icons/ui-slots.ts +58 -0
  218. package/shared/markdown.ts +44 -0
  219. package/shared/motion.ts +120 -0
  220. package/shared/rangi.ts +60 -0
  221. package/shared/rich-text.ts +34 -0
  222. package/shared/ui-icons.ts +6 -0
  223. package/shared/utils/urls.ts +64 -0
  224. package/types/index.ts +1 -0
  225. package/types/ui.ts +339 -0
package/nuxt.config.ts ADDED
@@ -0,0 +1,132 @@
1
+ import { defineNuxtConfig } from 'nuxt/config'
2
+ import { join } from 'pathe'
3
+ import { UI_ICON_SLOTS } from './shared/icons/ui-slots'
4
+
5
+ const currentDir = import.meta.dirname
6
+
7
+ // Design system layer: design language extended by every app — UI primitives, tokens, fonts,
8
+ // motion, chart helpers, formatters, design vocabularies (Status, severity), cross-cutting UI
9
+ // affordances (eject, feedback widget contracts).
10
+ //
11
+ // Per ADR-0042 (pragmatic rule): MAY know domain shapes; MUST NOT import runtime services
12
+ // (no useFetch, Drizzle, Caller, billing, auth, useRuntimeConfig). Storybook test: renders with
13
+ // prop fixtures alone. Placement contract for components: ADR-0029. Components MUST NOT be
14
+ // prefixed Pro/Admin/Tool/Docs/Learn — the layer name is the scope. Internal taxonomy:
15
+ // components/{container,data,element}/.
16
+
17
+ export default defineNuxtConfig({
18
+ imports: { autoImport: false },
19
+
20
+ nitro: {
21
+ imports: { autoImport: false },
22
+ alias: {
23
+ // `@nuxt/image@2.1.0` reaches `useRuntimeConfig` through
24
+ // `nitro/runtime-config` — the nitro v3 package — in
25
+ // `runtime/server/routes/_ipx.js` and `runtime/server/utils/image.js`. It
26
+ // declares no peer dependency for it, and this project runs Nuxt 4.5 on
27
+ // nitropack v2, where that specifier does not exist. Without this alias
28
+ // every app builds a server bundle importing a package that is not
29
+ // installed and boots to `Cannot find package 'nitro'`.
30
+ //
31
+ // A `patchedDependencies` entry cannot express this. `pnpm patch`
32
+ // extracts a PRISTINE copy that imports from `#imports`, so the diff comes
33
+ // out empty — the bad bytes live in pnpm's unpacked store layer and
34
+ // re-materialise on any install, three times in one session, machine-wide
35
+ // across every checkout sharing the store. An alias is immune to that
36
+ // because it never depends on what is on disk.
37
+ //
38
+ // Remove when the app moves to nitro v3, or when @nuxt/image ships the v2
39
+ // compatibility path its sibling @nuxt/scripts@2.0.0-beta.2 already
40
+ // carries (`resolveNitroImport` probing `nitro/app`, `nitro/cache`,
41
+ // `nitro/h3`, `nitro/runtime-config`, falling back to `nitropack/runtime`).
42
+ 'nitro/runtime-config': 'nitropack/runtime',
43
+ },
44
+ },
45
+
46
+ // Nuxt UI collects its app-config slots into the icon client bundle during
47
+ // module setup. Declare the semantic mapping here as well as in app.config
48
+ // so its default Lucide slots never become a second implicit allowlist.
49
+ appConfig: {
50
+ ui: { icons: UI_ICON_SLOTS },
51
+ },
52
+
53
+ modules: [
54
+ // Before `@nuxt/ui`: it writes the icon aliases and client bundle that
55
+ // @nuxt/icon reads. `designSystem` options live in build/design-system.ts.
56
+ join(currentDir, './build/design-system'),
57
+ '@comark/nuxt',
58
+ '@nuxt/fonts',
59
+ '@nuxt/image',
60
+ '@nuxt/scripts',
61
+ 'motion-v/nuxt',
62
+ 'reka-ui/nuxt',
63
+ '@nuxt/ui',
64
+ ],
65
+
66
+ css: [
67
+ join(currentDir, './css/global.css'),
68
+ ],
69
+
70
+ components: {
71
+ dirs: [
72
+ {
73
+ path: join(currentDir, './app/components'),
74
+ pathPrefix: false,
75
+ // Higher priority so design-system primitives win shadow conflicts against root
76
+ // app/components/* of the same basename (e.g. content/Alert.vue, owned by the
77
+ // content team — left in place; design-system's Alert is the canonical one).
78
+ priority: 10,
79
+ },
80
+ ],
81
+ },
82
+
83
+ // Canonical type stack for every surface that extends this layer (root app
84
+ // + apps/brand-kit brand-kit). Keep in sync via this single block —
85
+ // don't redeclare in root or app configs.
86
+ fonts: {
87
+ // Both families are global. CSS variable discovery cannot add another
88
+ // face, so avoid parsing every component style during each Vite build.
89
+ processCSSVariables: false,
90
+ families: [
91
+ // Keep build inputs in this layer. Provider downloads make every app
92
+ // build depend on Google Fonts availability after compilation finishes.
93
+ // Hubot Sans keeps variable weight and width axes for the design tokens.
94
+ {
95
+ name: 'Hubot Sans',
96
+ src: '/fonts/hubot-sans-var-latin.woff2',
97
+ weight: '200 900',
98
+ stretch: '75% 125%',
99
+ style: 'normal',
100
+ global: true,
101
+ },
102
+ {
103
+ name: 'Fira Code',
104
+ src: '/fonts/fira-code-var-latin.woff2',
105
+ weight: '400 500',
106
+ style: 'normal',
107
+ global: true,
108
+ },
109
+ ],
110
+ },
111
+
112
+ icon: {
113
+ // build/design-system.ts adds the role aliases and the chosen set's client
114
+ // bundle. Ship only that set's resolved role icons. Explicit raw ids outside
115
+ // that curated bundle resolve through Iconify in the browser. An app that
116
+ // installs `build/icon-server-bundle.ts` renders them on the server too.
117
+ provider: 'iconify',
118
+ serverBundle: false,
119
+ clientBundle: {
120
+ scan: false,
121
+ },
122
+ },
123
+
124
+ ui: {
125
+ experimental: { componentDetection: true },
126
+ content: true,
127
+ theme: {
128
+ colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error', 'pro'],
129
+ },
130
+ },
131
+
132
+ })
package/package.json CHANGED
@@ -1,6 +1,89 @@
1
1
  {
2
2
  "name": "@nuxtseo/design-system",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "description": "Nuxt SEO design system layer: UI primitives, tokens, fonts, motion, and chart helpers. Extend it from a Nuxt app.",
6
+ "license": "MIT",
7
+ "homepage": "https://nuxtseo.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/harlan-zw/nuxtseo.com.git",
11
+ "directory": "layers/design-system"
12
+ },
13
+ "keywords": [
14
+ "nuxt",
15
+ "nuxt-layer",
16
+ "design-system",
17
+ "nuxt-ui"
18
+ ],
19
+ "exports": {
20
+ ".": "./nuxt.config.ts",
21
+ "./*": "./*",
22
+ "./app/*": "./app/*.ts",
23
+ "./app/*.vue": "./app/*.vue",
24
+ "./components/*": "./app/components/*.ts",
25
+ "./components/*.vue": "./app/components/*.vue",
26
+ "./composables/*": "./app/composables/*.ts",
27
+ "./package.json": "./package.json",
28
+ "./server/*": "./server/*.ts",
29
+ "./shared/*": "./shared/*.ts"
30
+ },
31
+ "main": "./nuxt.config.ts",
32
+ "files": [
33
+ "app",
34
+ "build",
35
+ "css",
36
+ "nuxt.config.ts",
37
+ "public",
38
+ "server",
39
+ "shared",
40
+ "types"
41
+ ],
42
+ "peerDependencies": {
43
+ "nuxt": "^4.6.0",
44
+ "typescript": ">=5.8.0 <7",
45
+ "vue": "^3.5.0"
46
+ },
47
+ "dependencies": {
48
+ "@comark/nuxt": "0.7.0",
49
+ "@iconify-json/hugeicons": "^1.2.34",
50
+ "@iconify-json/logos": "^1.2.14",
51
+ "@iconify-json/lucide": "^1.2.130",
52
+ "@iconify-json/simple-icons": "^1.2.95",
53
+ "@iconify-json/svg-spinners": "^1.2.4",
54
+ "@iconify/vue": "^5.0.1",
55
+ "@nuxt/fonts": "^1.0.1",
56
+ "@nuxt/icon": "^2.5.1",
57
+ "@nuxt/image": "^2.1.0",
58
+ "@nuxt/scripts": "2.0.0-beta.14",
59
+ "@nuxt/ui": "^4.11.1",
60
+ "@tanstack/table-core": "^9.2.4",
61
+ "@tanstack/vue-table": "^9.2.4",
62
+ "@unovis/ts": "^1.6.7",
63
+ "@unovis/vue": "^1.6.7",
64
+ "@vueuse/core": "^14.4.0",
65
+ "motion-v": "2.4.2",
66
+ "nuxtseo-shared": "^6.0.2",
67
+ "pathe": "^2.0.3",
68
+ "rangi": "^2.2.0",
69
+ "reka-ui": "^2.10.4",
70
+ "tailwindcss": "^4.3.3",
71
+ "zod": "^4.6.7"
72
+ },
73
+ "devDependencies": {
74
+ "ai": "^7.0.122",
75
+ "nuxt": "4.6.1",
76
+ "sharp": "^0.35.4",
77
+ "site-typecheck": "0.0.0",
78
+ "vue": "^3.5.43"
79
+ },
80
+ "publishConfig": {
81
+ "access": "public"
82
+ },
83
+ "scripts": {
84
+ "nuxt:prepare": "nuxt prepare",
85
+ "typecheck": "nuxt prepare && site-typecheck -b --noEmit --force",
86
+ "brand:assets": "node --experimental-strip-types scripts/brand-assets.ts",
87
+ "typecheck:legacy": "nuxt typecheck"
88
+ }
6
89
  }
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,31 @@
1
+ # Build fonts
2
+
3
+ The design system owns these font files. Nuxt copies them into every app that
4
+ extends this layer. Builds never download fonts from an external provider.
5
+
6
+ The Hubot Sans file is a latin/latin-ext subset of the official v1.0.1 release
7
+ face (`HubotSans[slnt,wdth,wght].woff2`). It keeps the weight axis (200 to 900)
8
+ and the width axis (75% to 125%) for the design tokens. The slant axis is
9
+ pinned to 0 and dropped, and glyphs outside latin and latin-ext are removed,
10
+ so the transfer payload stays under 100KB
11
+ (`tests/design-system-hubot-font-subset.feature.test.ts` holds this contract).
12
+
13
+ To regenerate it, run a HarfBuzz subset over the release face with
14
+ `variationAxes: { wght: { min: 200, max: 900 }, wdth: { min: 75, max: 125 }, slnt: 0 }`
15
+ and the Google Fonts latin plus latin-ext unicode ranges.
16
+
17
+ The subset is a Modified Version under the OFL, so it must not present the
18
+ Reserved Font Name "Hubot". Override name records 1, 4, and 6 (plus 16 when
19
+ present) to a family name without "Hubot", for example "Nuxt SEO Sans" with
20
+ PostScript name "NuxtSEOSans-ExtraLight". Keep the copyright record verbatim;
21
+ it carries the reservation notice. The subset test holds this contract.
22
+
23
+ The Fira Code file contains the Latin subset. Nuxt exposes weights 400 through
24
+ 500 and the normal style.
25
+
26
+ The alert email shell also uses Hubot Sans from the stable URL
27
+ `https://nuxtseo.com/fonts/hubot-sans-var-latin.woff2`.
28
+
29
+ Both fonts use the SIL Open Font License 1.1. Keep each font with its matching
30
+ license file. Hubot Sans release source:
31
+ `https://github.com/github/hubot-sans/releases/tag/v1.0.1`.
@@ -0,0 +1,93 @@
1
+ Copyright (c) 2014, The Fira Code Project Authors (https://github.com/tonsky/FiraCode)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ https://openfontlicense.org
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,93 @@
1
+ Copyright 2021 The Hubot Sans Project Authors (https://github.com/github/hubot-sans), with Reserved Font Name "Hubot"
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ https://openfontlicense.org
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,122 @@
1
+ import { createError, defineEventHandler, getQuery, getRequestHeader, setResponseHeader, setResponseStatus } from 'h3'
2
+ import { resolveStoredFavicon } from '../utils/favicon-source'
3
+ /**
4
+ * Same-origin favicon proxy. UiFavicon renders this instead of hitting Google's
5
+ * s2 endpoint directly so the image is (a) cacheable on our edge, (b) decoupled
6
+ * from leaking every user's site list to Google per render, and crucially
7
+ * (c) same-origin — which lets the client sample the favicon on a canvas without
8
+ * a cross-origin taint, the basis for UiFavicon's adaptive backing (see
9
+ * useFaviconBacking). We only ever fetch Google with the domain as a query
10
+ * param, never the supplied host directly, so there is no SSRF surface; the
11
+ * format guard just rejects junk input.
12
+ *
13
+ * TWO SOURCES, in order. A Site the Crawl has reached is served from our own
14
+ * store — the Crawl fetches the home page's icon links anyway, so the bytes are
15
+ * the ones the Site actually serves today. Everything else (a competitor domain,
16
+ * a Site added minutes ago, any domain a marketing page names) falls back to
17
+ * Google's `s2` copy, which is the only source we have for a domain we do not
18
+ * crawl.
19
+ *
20
+ * The fallback is where staleness lives and it is not ours to fix: measured
21
+ * 2026-09-23, Google's copy of gscdump.com's icon and the icon that Site serves
22
+ * are different files, and our 30-day edge copy matched Google's exactly. No
23
+ * cache purge moves it. Crawling the Site is the fix, which is what the store
24
+ * branch is.
25
+ */
26
+ // 1×1 FULLY TRANSPARENT PNG. The previous constant claimed to be this and was
27
+ // not: its single pixel decoded to `R=0 G=255 B=0 A=127`, so every mail client
28
+ // scaled a half-opaque pure-green swatch to 16px and painted it beside the
29
+ // hostname — under a warning headline, where it read as a health dot. Verify
30
+ // with a decode, never by looking: at 1×1 the difference is invisible in source
31
+ // and obvious in an inbox.
32
+ // `?fallback=blank` callers get this with a 200 instead of
33
+ // a 404 — an <img> that 404s paints a broken-image glyph in clients with no
34
+ // scripted onerror to fall back with (mail, notably), and blank space beats a
35
+ // broken icon there. UiFavicon also requests the blank response, then detects
36
+ // its 1×1 size and swaps in the initials box without logging a failed request.
37
+ const BLANK_PNG = Uint8Array.from(
38
+ atob('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR4nGNgAAIAAAUAAXpeqz8AAAAASUVORK5CYII='),
39
+ c => c.charCodeAt(0),
40
+ )
41
+
42
+ export default defineEventHandler(async (event) => {
43
+ const query = getQuery(event)
44
+ const blankOnMiss = query.fallback === 'blank'
45
+ const contentHash = query.hash
46
+ const raw = String(query.domain ?? '')
47
+ .replace(/^sc-domain:/, '')
48
+ .replace(/^https?:\/\//, '')
49
+ .replace(/\/.*$/, '')
50
+ .trim()
51
+ .toLowerCase()
52
+
53
+ function miss() {
54
+ if (!blankOnMiss) {
55
+ setResponseStatus(event, 404)
56
+ return null
57
+ }
58
+ setResponseHeader(event, 'Content-Type', 'image/png')
59
+ setResponseHeader(event, 'Cache-Control', 'public, max-age=604800, s-maxage=2592000, immutable')
60
+ return BLANK_PNG
61
+ }
62
+
63
+ if (!raw || !/^[a-z0-9.-]+\.[a-z]{2,}$/.test(raw)) {
64
+ if (blankOnMiss)
65
+ return miss()
66
+ throw createError({ statusCode: 400, statusMessage: 'invalid domain' })
67
+ }
68
+ if (contentHash !== undefined && (typeof contentHash !== 'string' || !/^[a-f0-9]{64}$/.test(contentHash)))
69
+ throw createError({ statusCode: 400, statusMessage: 'invalid favicon hash' })
70
+
71
+ // Our own capture wins whenever we have one. A failed lookup is not an error
72
+ // worth surfacing: the Google fallback below is a complete answer, so a store
73
+ // that is down shows the icon everything showed before the store existed.
74
+ const stored = await resolveStoredFavicon(event, raw, contentHash).catch((silentCatchError: unknown) => {
75
+ console.warn('[silent-catch] layers/design-system/server/api/favicon.get.ts:stored-lookup', silentCatchError)
76
+ return null
77
+ })
78
+ if (stored) {
79
+ const etag = `"${stored.contentHash}"`
80
+ setResponseHeader(event, 'ETag', etag)
81
+ // NOT `immutable`. These bytes can change the day a Site rebrands, and the
82
+ // whole point of storing them is that the change reaches the reader. The
83
+ // ETag makes the revalidation a 304 rather than a re-download.
84
+ setResponseHeader(event, 'Cache-Control', contentHash
85
+ ? 'public, max-age=31536000, s-maxage=31536000, immutable'
86
+ : 'public, max-age=3600, s-maxage=86400, must-revalidate')
87
+ if (getRequestHeader(event, 'if-none-match') === etag) {
88
+ setResponseStatus(event, 304)
89
+ return null
90
+ }
91
+ setResponseHeader(event, 'Content-Type', stored.contentType)
92
+ return stored.bytes
93
+ }
94
+
95
+ // A dated request must answer from its captured bytes. Google's current
96
+ // copy would show the wrong mark as the "before" state.
97
+ if (contentHash) {
98
+ setResponseStatus(event, 404)
99
+ return null
100
+ }
101
+
102
+ const upstream = `https://www.google.com/s2/favicons?domain=${encodeURIComponent(raw)}&sz=128`
103
+
104
+ // A missing favicon is an expected outcome, not an infra failure: plenty of
105
+ // domains have none, and the upstream fetch can also fail on DNS/network. In
106
+ // every such case we want UiFavicon's <img> onerror to fall through to the
107
+ // initials box — so we set a non-2xx status and return empty rather than
108
+ // throwing, which would surface as an unhandled error in Sentry.
109
+ const res = await fetch(upstream).catch((silentCatchError: unknown) => {
110
+ console.warn('[silent-catch] layers/design-system/server/api/favicon.get.ts:30', silentCatchError)
111
+ return null
112
+ })
113
+ if (!res?.ok)
114
+ return miss()
115
+
116
+ setResponseHeader(event, 'Content-Type', res.headers.get('content-type') ?? 'image/png')
117
+ // Google's copy of a domain's icon changes on Google's schedule, which we
118
+ // cannot see or influence, so caching it hard costs nothing we could have
119
+ // served better. A Site we crawl never reaches this branch.
120
+ setResponseHeader(event, 'Cache-Control', 'public, max-age=604800, s-maxage=2592000, immutable')
121
+ return new Uint8Array(await res.arrayBuffer())
122
+ })
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Where `/api/favicon` looks before it falls back to Google.
3
+ *
4
+ * The endpoint ships in the design system, so both the site worker and the pro
5
+ * worker build it, and only pro holds the Sites tables and the R2 bucket that
6
+ * the Crawl writes captured marks into. Rather than import pro from here (the
7
+ * dependency runs the other way), the pro worker registers a resolver at cold
8
+ * start and the endpoint asks for one. A worker that registers nothing keeps
9
+ * exactly the behaviour this endpoint had before the store existed.
10
+ *
11
+ * Same registration shape as the Timeline's `defineEntryKind`: a nitro plugin
12
+ * in the owning layer fills a registry the shared code reads.
13
+ */
14
+
15
+ import type { H3Event } from 'h3'
16
+
17
+ export interface StoredFavicon {
18
+ bytes: Uint8Array
19
+ contentType: string
20
+ /** SHA-256 of the bytes, served as the `ETag` so a re-fetch after a rebrand is one 304. */
21
+ contentHash: string
22
+ }
23
+
24
+ /**
25
+ * Takes the request event, not just the domain.
26
+ *
27
+ * The resolver reads D1, and this codebase's `useDb()` only attaches the
28
+ * session, the instruments and the 2-second first-query guard when it is given
29
+ * an event. A signature that could not carry one would have put an
30
+ * unguarded, uninstrumented read on a public endpoint that fires once per
31
+ * avatar on a portfolio page.
32
+ */
33
+ export type FaviconSource = (event: H3Event, domain: string, contentHash?: string) => Promise<StoredFavicon | null>
34
+
35
+ let registered: FaviconSource | null = null
36
+
37
+ /** Called once per worker from the owning layer's nitro plugin. */
38
+ export function registerFaviconSource(source: FaviconSource): void {
39
+ registered = source
40
+ }
41
+
42
+ /**
43
+ * The stored mark for a domain, or null when this worker has no store, the
44
+ * domain is not a Site we have crawled, or the lookup failed. Never throws: a
45
+ * store that is down must degrade to the Google fallback, not to a broken image.
46
+ */
47
+ export async function resolveStoredFavicon(event: H3Event, domain: string, contentHash?: string): Promise<StoredFavicon | null> {
48
+ if (!registered)
49
+ return null
50
+ return registered(event, domain, contentHash)
51
+ }