@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
@@ -0,0 +1,55 @@
1
+ <script setup lang="ts" generic="T extends ArrayOrNested<SelectItem> = ArrayOrNested<SelectItem>, VK extends GetItemKeys<T> = 'value', M extends boolean = false">
2
+ import type { ArrayOrNested, GetItemKeys, SelectEmits, SelectItem, SelectProps } from '@nuxt/ui'
3
+ import { USelect } from '#components'
4
+
5
+ /**
6
+ * UiSelect — thin wrapper around Nuxt UI v4's USelect (the native-style
7
+ * single/multi select, not the overlay `USelectMenu` — that one stays raw
8
+ * per DESIGN.md's overlay-chrome rule).
9
+ *
10
+ * Tokens (radius, ring, full-width) are already centralized in
11
+ * `app.config.ts`'s `ui.select` — this component isn't a re-theme, it's the
12
+ * single legal entry point (raw `<USelect>` is ESLint-banned via
13
+ * `eslint/selectors.ts`) so a future behavioral change lands in one file
14
+ * instead of another repo-wide sweep. Every prop, emit, and slot passes
15
+ * straight through unchanged.
16
+ */
17
+
18
+ defineOptions({ inheritAttrs: false })
19
+
20
+ const props = defineProps<Props>()
21
+ const emit = defineEmits<SelectEmits<T, VK, M>>()
22
+ /**
23
+ * `open`, `defaultOpen` and `portal` are omitted deliberately, and dropping any
24
+ * of them from this list breaks every select in the app.
25
+ *
26
+ * `defineProps<T>()` compiles a bare `boolean` prop into a runtime prop with no
27
+ * default, and Vue resolves an ABSENT boolean prop to `false` rather than
28
+ * `undefined`. `{ ...props }` below then hands `<USelect>` a real, enumerable
29
+ * `open: false` — indistinguishable from a caller writing `:open="false"`.
30
+ * Reka's `SelectRoot` reads that as controlled state and pins the listbox
31
+ * permanently closed: the trigger still emits `update:open`, nothing consumes
32
+ * it, and the menu never renders. Nuxt UI ships `useComponentProps()` to dodge
33
+ * exactly this footgun by inspecting the raw vnode props, and the spread
34
+ * defeats it. `portal` suffers the same forced `false`, which un-teleports the
35
+ * listbox and risks clipping inside scroll-constrained containers.
36
+ *
37
+ * No caller passes any of the three, so omitting them costs nothing.
38
+ */
39
+ type Props = Omit<SelectProps<T, VK, M>, 'defaultValue' | 'modelModifiers' | 'open' | 'defaultOpen' | 'portal'>
40
+ </script>
41
+
42
+ <template>
43
+ <!-- @vue-generic {T, VK, M} -->
44
+ <USelect
45
+ v-bind="{ ...props, ...$attrs }"
46
+ @update:model-value="emit('update:modelValue', $event)"
47
+ @blur="emit('blur', $event)"
48
+ @change="emit('change', $event)"
49
+ @focus="emit('focus', $event)"
50
+ >
51
+ <template v-for="(_, name) in ($slots as Record<string, unknown>)" #[name]="slotProps" :key="name">
52
+ <slot :name="name" v-bind="slotProps || {}" />
53
+ </template>
54
+ </USelect>
55
+ </template>
@@ -0,0 +1,27 @@
1
+ <script setup lang="ts">
2
+ import type { SemanticStatus } from '../../composables/semanticColors'
3
+ import { semanticColors } from '../../composables/semanticColors'
4
+
5
+ const { severity = 'neutral', label } = defineProps<{
6
+ severity?: SemanticStatus
7
+ label?: string
8
+ }>()
9
+
10
+ // When the caller supplies neither a label nor slot content, severity is left to
11
+ // the dot colour alone — give screen readers the severity word as a fallback.
12
+ const SEVERITY_WORD: Record<SemanticStatus, string> = {
13
+ error: 'Error',
14
+ warning: 'Warning',
15
+ info: 'Info',
16
+ success: 'Success',
17
+ neutral: 'Neutral',
18
+ }
19
+ </script>
20
+
21
+ <template>
22
+ <span class="inline-flex items-center gap-1.5 text-xs text-default w-auto">
23
+ <span class="size-1.5 rounded-full shrink-0" :class="semanticColors[severity].dot" aria-hidden="true" />
24
+ <slot>{{ label }}</slot>
25
+ <span v-if="!label && !$slots.default" class="sr-only">{{ SEVERITY_WORD[severity] }}</span>
26
+ </span>
27
+ </template>
@@ -0,0 +1,24 @@
1
+ <script setup lang="ts">
2
+ import { UiFavicon } from '#components'
3
+
4
+ const { domain, tier, size = 16 } = defineProps<{
5
+ domain: string
6
+ /** Product-owned site tier. External identities should use `UiFavicon`. */
7
+ tier: 'growth' | 'monitor'
8
+ size?: number
9
+ }>()
10
+ </script>
11
+
12
+ <template>
13
+ <span
14
+ class="inline-grid shrink-0 place-items-center"
15
+ :style="{
16
+ width: `${size + 2}px`,
17
+ height: `${size + 2}px`,
18
+ }"
19
+ :data-monitoring-tier="tier"
20
+ aria-hidden="true"
21
+ >
22
+ <UiFavicon :domain="domain" :size="size" decorative />
23
+ </span>
24
+ </template>
@@ -0,0 +1,148 @@
1
+ <script setup lang="ts">
2
+ import { useAttrs } from 'vue'
3
+
4
+ defineOptions({ inheritAttrs: false })
5
+
6
+ /**
7
+ * Deterministic skeleton loader that avoids SSR hydration mismatches.
8
+ * Uses index-based pseudo-random sizing instead of Math.random().
9
+ */
10
+ const {
11
+ type = 'text',
12
+ lines = 1,
13
+ index = 0,
14
+ base = 160,
15
+ range = 80,
16
+ } = defineProps<{
17
+ /**
18
+ * Skeleton shape. `text`/`bar`/`circle` size themselves from `base`/`index`;
19
+ * `block` sets no width or height of its own, so the caller's utility classes
20
+ * decide the box — use it whenever the placeholder must match a known layout
21
+ * (a `w-full` row, a fixed-width label). The self-sizing types write an inline
22
+ * `width`/`height`, which silently beats any `w-*`/`h-*` class passed in.
23
+ */
24
+ type?: 'text' | 'bar' | 'circle' | 'block'
25
+ /** Number of skeleton lines (text type only) */
26
+ lines?: number
27
+ /** Seed index for deterministic width/height variation */
28
+ index?: number
29
+ /** Base size in px — text: width, bar: height percent, circle: diameter */
30
+ base?: number
31
+ /** Variation range in px or percent */
32
+ range?: number
33
+ }>()
34
+
35
+ const attrs = useAttrs()
36
+
37
+ // Deterministic pseudo-random: produces consistent values for the same index on server + client
38
+ function seededValue(i: number): number {
39
+ const x = Math.sin(i * 127.1 + 311.7) * 43758.5453
40
+ return x - Math.floor(x) // 0..1
41
+ }
42
+
43
+ function textWidth(i: number): string {
44
+ return `${Math.round(base + seededValue(i + index) * range)}px`
45
+ }
46
+
47
+ function barHeight(i: number): string {
48
+ return `${Math.round(20 + Math.sin(i * 0.3) * 15 + seededValue(i + index) * 30)}%`
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <!-- Text rows (multiple lines = fragment, wrapped in div for class inheritance) -->
54
+ <div v-if="type === 'text' && lines > 1" aria-hidden="true" v-bind="attrs" class="flex flex-col gap-2">
55
+ <div
56
+ v-for="i in lines"
57
+ :key="i"
58
+ class="ui-skeleton rounded h-3 max-w-full"
59
+ :style="{ width: textWidth(i) }"
60
+ />
61
+ </div>
62
+
63
+ <!-- Single text row -->
64
+ <div
65
+ v-else-if="type === 'text'"
66
+ aria-hidden="true"
67
+ v-bind="attrs"
68
+ class="ui-skeleton rounded h-3 max-w-full"
69
+ :style="{ width: textWidth(1) }"
70
+ />
71
+
72
+ <!-- Vertical chart bars -->
73
+ <div
74
+ v-else-if="type === 'bar'"
75
+ aria-hidden="true"
76
+ v-bind="attrs"
77
+ class="ui-skeleton rounded-t w-full mt-auto"
78
+ :style="{ height: barHeight(index) }"
79
+ />
80
+
81
+ <!-- Caller-sized box: no inline width/height, so `w-*`/`h-*` classes win -->
82
+ <div
83
+ v-else-if="type === 'block'"
84
+ aria-hidden="true"
85
+ v-bind="attrs"
86
+ class="ui-skeleton rounded"
87
+ />
88
+
89
+ <!-- Circle -->
90
+ <div
91
+ v-else-if="type === 'circle'"
92
+ aria-hidden="true"
93
+ v-bind="attrs"
94
+ class="ui-skeleton rounded-full"
95
+ :style="{ width: `${base}px`, height: `${base}px` }"
96
+ />
97
+ </template>
98
+
99
+ <!--
100
+ Cross-fade reveal recipe (consumer responsibility):
101
+ <Transition name="ui-skel-fade">
102
+ <UiSkeleton v-if="loading" />
103
+ <RealContent v-else />
104
+ </Transition>
105
+ with `.ui-skel-fade-enter-active{transition:opacity 160ms ease-out}`. Prevents pop-in.
106
+ -->
107
+ <style scoped>
108
+ .ui-skeleton {
109
+ position: relative;
110
+ background:
111
+ linear-gradient(
112
+ 90deg,
113
+ transparent 0%,
114
+ color-mix(in srgb, var(--ui-bg-elevated) 70%, transparent) 50%,
115
+ transparent 100%
116
+ ),
117
+ linear-gradient(
118
+ 90deg,
119
+ var(--ui-bg-accented) 0%,
120
+ var(--ui-bg-elevated) 40%,
121
+ var(--ui-bg-accented) 80%
122
+ );
123
+ background-size: 200% 100%, 200% 100%;
124
+ background-repeat: no-repeat;
125
+ background-position: 200% 0, 200% 0;
126
+ /* Single shimmer pass. A second, slower animation on the same
127
+ `background-position` was fully overridden (last-declared animation wins)
128
+ so it only doubled paint work with zero visual effect. */
129
+ animation: ui-shimmer 1.6s ease-in-out infinite;
130
+ box-shadow: var(--elevation-inset);
131
+ }
132
+
133
+ @keyframes ui-shimmer {
134
+ 0% { background-position: 200% 0, 0 0; }
135
+ 100% { background-position: -200% 0, 0 0; }
136
+ }
137
+
138
+ @media (prefers-reduced-motion: reduce) {
139
+ .ui-skeleton {
140
+ animation: ui-skel-pulse 1.6s ease-in-out infinite;
141
+ background: var(--ui-bg-accented);
142
+ }
143
+ @keyframes ui-skel-pulse {
144
+ 0%, 100% { opacity: 0.6; }
145
+ 50% { opacity: 1; }
146
+ }
147
+ }
148
+ </style>
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ import type { SlotTextOptions } from '../../utils/slot-text'
3
+ import { useReducedMotion } from 'motion-v'
4
+ import { computed, onMounted, ref, watch } from 'vue'
5
+
6
+ const { text, options } = defineProps<{
7
+ text: string
8
+ options?: SlotTextOptions
9
+ }>()
10
+
11
+ const reduced = useReducedMotion()
12
+ // SSR and the first hydrated frame stay plain text. Character-slot markup only
13
+ // appears for a real post-mount update, after the global animation CSS is ready.
14
+ // This prevents a slow stylesheet/hydration path from exposing split glyphs.
15
+ const mounted = ref(false)
16
+ const animateUpdates = ref(false)
17
+ onMounted(() => {
18
+ mounted.value = true
19
+ })
20
+ watch(() => text, (next, previous) => {
21
+ if (mounted.value && next !== previous)
22
+ animateUpdates.value = true
23
+ })
24
+
25
+ const resolvedOptions = computed<SlotTextOptions>(() => ({
26
+ direction: 'up',
27
+ stagger: 32,
28
+ duration: 260,
29
+ exitOffset: 38,
30
+ ...options,
31
+ }))
32
+
33
+ const chars = computed(() => Array.from(text))
34
+ const directionClass = computed(() => resolvedOptions.value.direction === 'down' ? 'ui-slot-text--down' : 'ui-slot-text--up')
35
+
36
+ function glyph(char: string) {
37
+ return char === ' ' ? '\u00A0' : char
38
+ }
39
+
40
+ function slotStyle(index: number) {
41
+ return {
42
+ '--ui-slot-delay': `${index * (resolvedOptions.value.stagger ?? 32)}ms`,
43
+ '--ui-slot-duration': `${resolvedOptions.value.duration ?? 260}ms`,
44
+ '--ui-slot-easing': resolvedOptions.value.easing ?? 'cubic-bezier(0.34, 1.56, 0.64, 1)',
45
+ }
46
+ }
47
+ </script>
48
+
49
+ <template>
50
+ <span v-if="reduced || !animateUpdates" class="ui-slot-text-host">{{ text }}</span>
51
+ <span v-else class="ui-slot-text-host ui-slot-text" :class="directionClass" :aria-label="text">
52
+ <span
53
+ v-for="(char, index) in chars"
54
+ :key="index"
55
+ class="ui-slot-text__slot"
56
+ :style="slotStyle(index)"
57
+ aria-hidden="true"
58
+ >
59
+ <span class="ui-slot-text__sizer">{{ glyph(char) }}</span>
60
+ <Transition name="ui-slot-text-roll">
61
+ <span :key="`${index}:${char}`" class="ui-slot-text__face">{{ glyph(char) }}</span>
62
+ </Transition>
63
+ </span>
64
+ </span>
65
+ </template>
@@ -0,0 +1,113 @@
1
+ <script setup lang="ts">
2
+ import { computed } from 'vue'
3
+ import { UiBrandIcon, UiIcon } from '#components'
4
+
5
+ export interface SourceLogo {
6
+ id: string
7
+ label: string
8
+ icon: string
9
+ image?: string
10
+ brand?: boolean
11
+ invertDark?: boolean
12
+ tint?: string
13
+ mono?: boolean
14
+ }
15
+
16
+ const { sources, size = 'sm', dither = false } = defineProps<{
17
+ sources: readonly SourceLogo[]
18
+ /**
19
+ * `inline` drops the frame so the mark sits inside a line of text at 12px,
20
+ * the way a byline credits its Source. `xs` and `sm` frame the mark as a
21
+ * badge for a row's edge.
22
+ */
23
+ size?: 'inline' | 'xs' | 'sm'
24
+ /**
25
+ * Quiet provenance mark for a card corner that must not compete with the
26
+ * number beside it: desaturated, softened, stippled with a fine dot mask.
27
+ * Same treatment `UiFavicon` uses for its `dither` prop, so a Source logo
28
+ * and a site favicon recede the same way. Hovering the enclosing `.group`,
29
+ * link or button restores the full logo.
30
+ */
31
+ dither?: boolean
32
+ }>()
33
+
34
+ const accessibleLabel = computed(() => sources.map(source => source.label).join(' and '))
35
+ const framed = computed(() => size !== 'inline')
36
+ const frameClass = computed(() => size === 'inline' ? '' : `border border-default bg-default shadow-xs rounded-md ${size === 'xs' ? 'size-5' : 'size-6'}`)
37
+ const iconClass = computed(() => size === 'inline' ? 'size-3' : size === 'xs' ? 'size-3.5' : 'size-4')
38
+ const brandClass = computed(() => size === 'inline' ? '!size-3' : size === 'xs' ? '!size-3.5' : '!size-4')
39
+ const iconPixels = computed(() => size === 'inline' ? 12 : size === 'xs' ? 14 : 16)
40
+ </script>
41
+
42
+ <template>
43
+ <span
44
+ v-if="sources.length"
45
+ class="inline-flex shrink-0 items-center"
46
+ role="img"
47
+ :aria-label="accessibleLabel"
48
+ :title="accessibleLabel"
49
+ >
50
+ <span
51
+ v-for="(source, index) in sources"
52
+ :key="source.id"
53
+ class="relative inline-flex shrink-0 items-center justify-center"
54
+ :class="[frameClass, index ? (framed ? '-ml-1.5' : 'ml-0.5') : '', dither ? 'ui-source-logo-dithered' : '']"
55
+ aria-hidden="true"
56
+ >
57
+ <img
58
+ v-if="source.image"
59
+ :src="source.image"
60
+ alt=""
61
+ :width="iconPixels"
62
+ :height="iconPixels"
63
+ :class="[iconClass, dither ? 'ui-source-logo-dither' : '']"
64
+ class="rounded-full"
65
+ >
66
+ <UiBrandIcon v-else-if="source.brand" size="md" :class="[brandClass, dither ? 'ui-source-logo-dither' : '']" />
67
+ <UiIcon
68
+ v-else
69
+ :name="source.icon"
70
+ :class="[iconClass, source.invertDark ? 'dark:invert' : '', source.tint ?? '', source.mono ? 'text-highlighted' : '', dither ? 'ui-source-logo-dither' : '']"
71
+ aria-hidden="true"
72
+ />
73
+ </span>
74
+ </span>
75
+ </template>
76
+
77
+ <style scoped>
78
+ /* Mirrors `UiFavicon`'s dither: the frame gets the dot mask, the mark gets the
79
+ desaturation, and the enclosing hover target restores both. */
80
+ .ui-source-logo-dithered::after {
81
+ content: '';
82
+ position: absolute;
83
+ inset: 0;
84
+ border-radius: inherit;
85
+ background-image: radial-gradient(circle, var(--ui-bg) 38%, transparent 44%);
86
+ background-size: 2px 2px;
87
+ opacity: .55;
88
+ pointer-events: none;
89
+ transition: opacity 180ms var(--ease-standard, ease);
90
+ }
91
+
92
+ .ui-source-logo-dither {
93
+ filter: saturate(.35) contrast(.9) opacity(.85);
94
+ transition: filter 180ms var(--ease-standard, ease);
95
+ }
96
+
97
+ /* Hover restores the full mark, but never on a disabled control: a Source that
98
+ cannot be read is dithered to SAY so, and lighting it up contradicts that. */
99
+ :is(a, button, .group):not(:disabled):hover .ui-source-logo-dithered::after {
100
+ opacity: 0;
101
+ }
102
+
103
+ :is(a, button, .group):not(:disabled):hover .ui-source-logo-dither {
104
+ filter: none;
105
+ }
106
+
107
+ @media (prefers-reduced-motion: reduce) {
108
+ .ui-source-logo-dithered::after,
109
+ .ui-source-logo-dither {
110
+ transition: none;
111
+ }
112
+ }
113
+ </style>
@@ -0,0 +1,71 @@
1
+ <script setup lang="ts">
2
+ import type { SourceLogo } from './UiSourceLogos.vue'
3
+ import { UiSourceLogos } from '#components'
4
+
5
+ // The Source rail: the left column of any "pick a Source, then the thing"
6
+ // surface (the Analytics add dialog, the Alert trigger picker). One Source per
7
+ // row, wearing the catalogue's mark. A Source that cannot be read here stays
8
+ // listed, greyed, with its reason: the rail is where a user learns what
9
+ // connecting Bing or an analytics property would buy them. It never decides
10
+ // availability itself; the caller passes `reason` from the Site's own state.
11
+ export interface SourceRailItem {
12
+ identity: SourceLogo
13
+ /**
14
+ * What to call the Source. Usually the identity's own name; Web Analytics
15
+ * passes its Source name while `identity` carries the linked Provider's
16
+ * logo, because a Source and a Provider are different things (GLOSSARY).
17
+ */
18
+ label: string
19
+ /** Non-null when the Source is listed but cannot be read here. */
20
+ reason: string | null
21
+ /**
22
+ * False hides the reason line under the row (it stays in the title). The
23
+ * caller sets it when several rows share one reason and says it once
24
+ * instead, so the rail does not repeat a sentence four times.
25
+ */
26
+ caption?: boolean
27
+ /** Trailing count, e.g. rows already added or rules on this Source. Empty hides it. */
28
+ count?: number | string | null
29
+ }
30
+
31
+ const { sources, quiet = false } = defineProps<{
32
+ sources: readonly SourceRailItem[]
33
+ /**
34
+ * True while a search spans every Source: the selection is kept but not
35
+ * shown as current, so the rail does not claim a scope the rows ignore.
36
+ */
37
+ quiet?: boolean
38
+ }>()
39
+ const emit = defineEmits<{ pick: [id: string] }>()
40
+ const selected = defineModel<string | null>({ default: null })
41
+ function pick(id: string) {
42
+ selected.value = id
43
+ emit('pick', id)
44
+ }
45
+ </script>
46
+
47
+ <template>
48
+ <ul class="flex flex-row gap-1 overflow-x-auto sm:flex-col sm:overflow-visible" aria-label="Sources">
49
+ <li v-for="s in sources" :key="s.identity.id">
50
+ <button
51
+ type="button"
52
+ class="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left text-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-primary"
53
+ :class="[
54
+ s.reason ? 'cursor-not-allowed text-dimmed' : 'text-default hover:bg-elevated',
55
+ !quiet && selected === s.identity.id ? 'bg-elevated text-highlighted' : '',
56
+ ]"
57
+ :disabled="!!s.reason"
58
+ :aria-current="!quiet && selected === s.identity.id ? 'true' : undefined"
59
+ :title="s.reason ?? undefined"
60
+ @click="pick(s.identity.id)"
61
+ >
62
+ <UiSourceLogos :sources="[s.identity]" size="xs" :dither="!!s.reason" />
63
+ <span class="min-w-0 flex-1 truncate">{{ s.label }}</span>
64
+ <span v-if="!s.reason && s.count" class="font-mono text-mini text-dimmed">{{ s.count }}</span>
65
+ </button>
66
+ <p v-if="s.reason && s.caption !== false" class="hidden px-2 pb-1 text-mini text-dimmed sm:block">
67
+ {{ s.reason }}
68
+ </p>
69
+ </li>
70
+ </ul>
71
+ </template>
@@ -0,0 +1,107 @@
1
+ <script setup lang="ts">
2
+ import type { UiStaticTerminalLine } from '../../shared/static-terminal'
3
+ import { useClipboard } from '@vueuse/core'
4
+ import { computed } from 'vue'
5
+ import { UiButton } from '#components'
6
+ import { highlightCode } from '../../../shared/rangi'
7
+
8
+ const {
9
+ lines,
10
+ copyLines,
11
+ title = 'zsh',
12
+ cwd = '~',
13
+ language = 'bash',
14
+ } = defineProps<{
15
+ lines: UiStaticTerminalLine[]
16
+ /** Plaintext commands to copy when the visible commands mask a secret. */
17
+ copyLines?: UiStaticTerminalLine[]
18
+ title?: string
19
+ cwd?: string
20
+ language?: string
21
+ }>()
22
+
23
+ const { copy, copied } = useClipboard({ legacy: true })
24
+ const copyText = computed(() => (copyLines ?? lines).map(line => line.command).join('\n'))
25
+ const renderedLines = computed(() => lines.map(line => ({
26
+ ...line,
27
+ noteHtml: line.note ? highlightCode(`# ${line.note}`, 'bash').html : null,
28
+ commandHtml: highlightCode(line.command, line.language ?? language).html,
29
+ outputHtml: line.output ? highlightCode(line.output, line.outputLanguage ?? 'plain').html : null,
30
+ })))
31
+ </script>
32
+
33
+ <template>
34
+ <div class="ui-static-terminal overflow-hidden rounded-xl border shadow-sm">
35
+ <div class="ui-static-terminal__chrome flex items-center gap-2 border-b px-3 py-1.5">
36
+ <span class="flex items-center gap-1.5" aria-hidden="true">
37
+ <span class="ui-static-terminal__dot size-2.5 rounded-full" />
38
+ <span class="ui-static-terminal__dot size-2.5 rounded-full" />
39
+ <span class="ui-static-terminal__dot size-2.5 rounded-full" />
40
+ </span>
41
+ <span class="ui-static-terminal__muted ml-1 min-w-0 flex-1 truncate font-mono text-mini">{{ cwd }} · {{ title }}</span>
42
+ <UiButton
43
+ :icon="copied ? 'check' : 'copy'"
44
+ purpose="quiet"
45
+ size="xs"
46
+ class="ui-static-terminal__copy min-h-11 min-w-11 sm:min-h-8 sm:min-w-8"
47
+ :aria-label="copied ? 'Copied' : 'Copy commands'"
48
+ @click="copy(copyText)"
49
+ />
50
+ </div>
51
+
52
+ <div tabindex="0" class="focus-visible:ring-2 focus-visible:ring-primary overflow-x-auto px-3 py-2.5 font-mono text-xs leading-relaxed">
53
+ <div v-for="(line, index) in renderedLines" :key="index" :class="index > 0 ? 'mt-3' : ''">
54
+ <!-- Rangi escapes source text before creating this token markup. -->
55
+ <!-- eslint-disable-next-line vue/no-v-html -->
56
+ <pre v-if="line.noteHtml" class="ui-static-terminal__muted m-0 whitespace-pre"><code class="rangi shj-lang-bash" v-html="line.noteHtml" /></pre>
57
+ <pre class="m-0 whitespace-pre"><span v-if="line.prompt !== false" class="select-none text-primary-500 dark:text-primary-400" aria-hidden="true">$ </span><!-- eslint-disable-next-line vue/no-v-html --><code class="rangi" :class="`shj-lang-${line.language ?? language}`" v-html="line.commandHtml" /></pre>
58
+ <!-- eslint-disable-next-line vue/no-v-html -->
59
+ <pre v-if="line.outputHtml" class="ui-static-terminal__output m-0 whitespace-pre"><code class="rangi" :class="`shj-lang-${line.outputLanguage ?? 'plain'}`" v-html="line.outputHtml" /></pre>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </template>
64
+
65
+ <style scoped>
66
+ .ui-static-terminal {
67
+ --terminal-bg: light-dark(oklch(0.985 0.002 286), oklch(0.145 0.008 326));
68
+ --terminal-chrome: light-dark(oklch(0.965 0.004 286), oklch(0.18 0.01 326));
69
+ --terminal-fg: light-dark(oklch(0.21 0.008 286), oklch(0.985 0 0));
70
+ --terminal-muted: light-dark(oklch(0.48 0.012 286), oklch(0.69 0.018 326));
71
+ --terminal-output: light-dark(oklch(0.39 0.01 286), oklch(0.76 0.014 326));
72
+ --terminal-border: light-dark(oklch(0.89 0.008 286), oklch(0.28 0.014 326));
73
+ background: var(--terminal-bg);
74
+ border-color: var(--terminal-border);
75
+ color: var(--terminal-fg);
76
+ }
77
+
78
+ .ui-static-terminal__chrome {
79
+ background: var(--terminal-chrome);
80
+ border-color: var(--terminal-border);
81
+ }
82
+
83
+ .ui-static-terminal__dot {
84
+ background: oklch(0.66 0.18 25);
85
+ }
86
+
87
+ .ui-static-terminal__dot:nth-child(2) {
88
+ background: oklch(0.78 0.15 82);
89
+ }
90
+
91
+ .ui-static-terminal__dot:nth-child(3) {
92
+ background: oklch(0.7 0.15 145);
93
+ }
94
+
95
+ .ui-static-terminal__muted {
96
+ color: var(--terminal-muted);
97
+ }
98
+
99
+ .ui-static-terminal__output,
100
+ .ui-static-terminal__copy {
101
+ color: var(--terminal-output) !important;
102
+ }
103
+
104
+ .ui-static-terminal__copy:hover {
105
+ color: var(--terminal-fg) !important;
106
+ }
107
+ </style>
@@ -0,0 +1,62 @@
1
+ <script setup lang="ts">
2
+ import type { SemanticStatus } from '../../composables/semanticColors'
3
+ import type { UiIcon as UiIconName } from '../../shared/ui-icons'
4
+ import { computed } from 'vue'
5
+ import { UiChip, UiIcon } from '#components'
6
+ import { semanticColors } from '../../composables/semanticColors'
7
+
8
+ /**
9
+ * UiStatusBadge — the semantic status entry.
10
+ *
11
+ * Default (sm) is a colored dot + neutral text: color rides the 6px dot, the
12
+ * label stays `text-default`. This is the calm, stackable treatment for tables
13
+ * and lists where many statuses appear at once — the expected state shouldn't
14
+ * spend the page's color budget (see the color-budget ladder in the brand-kit).
15
+ *
16
+ * - `prominent` escalate to a tinted UiChip fill. Reserve for a SINGLE
17
+ * prominent status (a card header, a hero callout), never in a
18
+ * repeated row.
19
+ * - `size="md"` icon container for empty / error states.
20
+ */
21
+
22
+ const { status = 'neutral', icon, label, size = 'sm', prominent = false, readable = false } = defineProps<{
23
+ status?: SemanticStatus
24
+ icon?: UiIconName
25
+ label?: string
26
+ /** 'sm' = inline indicator, 'md' = icon container (empty/error states) */
27
+ size?: 'sm' | 'md'
28
+ /** Escalate to a tinted chip fill. Reserve for a single prominent status. */
29
+ prominent?: boolean
30
+ /** Keep a report-visible inline label at the 14px readable tier. */
31
+ readable?: boolean
32
+ }>()
33
+
34
+ const colors = computed(() => semanticColors[status])
35
+ </script>
36
+
37
+ <template>
38
+ <!-- md: icon container (empty / error states) -->
39
+ <div v-if="size === 'md'" class="flex flex-col items-center gap-2">
40
+ <div class="size-12 rounded-2xl flex items-center justify-center" :class="colors.bg">
41
+ <UiIcon v-if="icon" :name="icon" class="size-6" :class="colors.text" aria-hidden="true" />
42
+ </div>
43
+ <slot>
44
+ <span v-if="label" class="text-sm font-medium">{{ label }}</span>
45
+ </slot>
46
+ </div>
47
+ <!-- sm + prominent: tinted chip — singular emphasis only -->
48
+ <UiChip
49
+ v-else-if="prominent"
50
+ purpose="status"
51
+ :status="status"
52
+ :icon="icon"
53
+ :size="readable ? 'md' : 'sm'"
54
+ >
55
+ <slot>{{ label }}</slot>
56
+ </UiChip>
57
+ <!-- sm default: colored dot + neutral text — calm, stackable -->
58
+ <span v-else class="inline-flex items-center gap-1.5 whitespace-nowrap" :class="readable ? 'text-sm' : 'text-xs'">
59
+ <span class="size-1.5 rounded-full shrink-0" :class="colors.dot" aria-hidden="true" />
60
+ <span class="text-default"><slot>{{ label }}</slot></span>
61
+ </span>
62
+ </template>