@nuxtseo/design-system 0.0.0-stage → 0.1.1
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.
- package/README.md +181 -2
- package/app/app.config.ts +592 -0
- package/app/component-attributes.d.ts +8 -0
- package/app/components/README.md +38 -0
- package/app/components/brand/UiBrandMark.vue +63 -0
- package/app/components/brand/UiModuleIcon.vue +45 -0
- package/app/components/brand/UiProLettering.vue +26 -0
- package/app/components/brand/UiRangeTerrain.vue +311 -0
- package/app/components/brand/range-glyphs.ts +18 -0
- package/app/components/container/BrowserTab.vue +145 -0
- package/app/components/container/IntegrationRow.vue +120 -0
- package/app/components/container/KnowDoRails.vue +40 -0
- package/app/components/container/RailPanel.vue +93 -0
- package/app/components/container/UiAlert.vue +120 -0
- package/app/components/container/UiAppShell.vue +165 -0
- package/app/components/container/UiCard.vue +204 -0
- package/app/components/container/UiDisclosure.vue +43 -0
- package/app/components/container/UiEmptyState.vue +102 -0
- package/app/components/container/UiErrorState.vue +61 -0
- package/app/components/container/UiLoadingState.vue +20 -0
- package/app/components/container/UiPageHeader.vue +95 -0
- package/app/components/container/UiPopover.vue +102 -0
- package/app/components/container/UiSampleDataOverlay.vue +47 -0
- package/app/components/container/UiSectionHeader.vue +83 -0
- package/app/components/container/UiSummaryStrip.vue +40 -0
- package/app/components/container/UiToast.vue +230 -0
- package/app/components/container/UiToaster.vue +271 -0
- package/app/components/container/UiWidgetState.vue +143 -0
- package/app/components/container/ui-nav-drawer.ts +48 -0
- package/app/components/data/ActionRail.vue +152 -0
- package/app/components/data/FacetStrip.vue +93 -0
- package/app/components/data/UiBarCell.vue +37 -0
- package/app/components/data/UiBarChart.vue +281 -0
- package/app/components/data/UiChartAnnotationControls.vue +74 -0
- package/app/components/data/UiChartAnnotations.vue +163 -0
- package/app/components/data/UiChartBrushOverlay.vue +96 -0
- package/app/components/data/UiChartDataReveal.vue +81 -0
- package/app/components/data/UiChartFrame.vue +123 -0
- package/app/components/data/UiChartTooltip.vue +81 -0
- package/app/components/data/UiCoverageNote.vue +90 -0
- package/app/components/data/UiCwvSparkline.vue +188 -0
- package/app/components/data/UiDataList.vue +276 -0
- package/app/components/data/UiDataTableSection.vue +233 -0
- package/app/components/data/UiDistributionBars.vue +164 -0
- package/app/components/data/UiDonutChart.vue +132 -0
- package/app/components/data/UiDoorwayColumn.vue +108 -0
- package/app/components/data/UiDoorwayGrid.vue +58 -0
- package/app/components/data/UiDoorwayRow.vue +83 -0
- package/app/components/data/UiEntitySummary.vue +261 -0
- package/app/components/data/UiFactsGrid.vue +107 -0
- package/app/components/data/UiImpactMeter.vue +33 -0
- package/app/components/data/UiIssueRow.vue +149 -0
- package/app/components/data/UiLineChart.vue +431 -0
- package/app/components/data/UiMetricBarPopover.vue +121 -0
- package/app/components/data/UiMetricLabel.vue +30 -0
- package/app/components/data/UiMetricStat.vue +73 -0
- package/app/components/data/UiMetricToggle.vue +156 -0
- package/app/components/data/UiMetricsRow.vue +64 -0
- package/app/components/data/UiPositionMetric.vue +53 -0
- package/app/components/data/UiRelatedFixes.vue +113 -0
- package/app/components/data/UiRichText.vue +325 -0
- package/app/components/data/UiScatterPlot.vue +144 -0
- package/app/components/data/UiSeverityMarker.vue +30 -0
- package/app/components/data/UiSparkColumns.vue +160 -0
- package/app/components/data/UiSparkline.vue +681 -0
- package/app/components/data/UiStat.vue +445 -0
- package/app/components/data/UiStats.vue +76 -0
- package/app/components/data/UiTable.vue +517 -0
- package/app/components/data/UiTableExport.vue +40 -0
- package/app/components/data/UiTableFrame.vue +149 -0
- package/app/components/data/UiTablePagination.vue +37 -0
- package/app/components/data/UiTableShell.vue +48 -0
- package/app/components/data/UiTableTd.vue +39 -0
- package/app/components/data/UiTableTh.vue +61 -0
- package/app/components/data/UiTopEntityStackChart.vue +393 -0
- package/app/components/data/cells/UiTableDash.vue +3 -0
- package/app/components/data/cells/UiTableHeaderCell.vue +72 -0
- package/app/components/data/cells/UiTableMetricCell.vue +72 -0
- package/app/components/data/cells/UiTablePathCell.vue +76 -0
- package/app/components/data/cells/UiTableScoreTile.vue +49 -0
- package/app/components/data/cells/UiTableTrendCell.vue +41 -0
- package/app/components/element/UiAtmosphere.vue +64 -0
- package/app/components/element/UiAuthHeading.vue +24 -0
- package/app/components/element/UiAuthProviders.vue +46 -0
- package/app/components/element/UiBrandIcon.vue +39 -0
- package/app/components/element/UiButton.vue +549 -0
- package/app/components/element/UiCelebration.vue +65 -0
- package/app/components/element/UiChannelChips.vue +62 -0
- package/app/components/element/UiChatQuickLink.vue +319 -0
- package/app/components/element/UiChip.vue +101 -0
- package/app/components/element/UiConfirmDialog.vue +84 -0
- package/app/components/element/UiCopyButton.vue +90 -0
- package/app/components/element/UiDitherAvatar.vue +177 -0
- package/app/components/element/UiDotLabel.vue +19 -0
- package/app/components/element/UiEjectMenu.vue +181 -0
- package/app/components/element/UiEjectOverlay.vue +29 -0
- package/app/components/element/UiFavicon.vue +235 -0
- package/app/components/element/UiFaviconStack.vue +84 -0
- package/app/components/element/UiFilterMenu.vue +324 -0
- package/app/components/element/UiFocusedSurface.vue +129 -0
- package/app/components/element/UiHealthDot.vue +32 -0
- package/app/components/element/UiHelpLabel.vue +28 -0
- package/app/components/element/UiIcon.vue +22 -0
- package/app/components/element/UiInput.vue +35 -0
- package/app/components/element/UiLogoPlausible.vue +12 -0
- package/app/components/element/UiLogoPro.vue +25 -0
- package/app/components/element/UiLogoUmami.vue +12 -0
- package/app/components/element/UiNavIcon.vue +107 -0
- package/app/components/element/UiNavList.vue +156 -0
- package/app/components/element/UiOnboardingAnswers.vue +127 -0
- package/app/components/element/UiOverflowCount.vue +31 -0
- package/app/components/element/UiProBadge.vue +19 -0
- package/app/components/element/UiProgressCircle.vue +79 -0
- package/app/components/element/UiProgressPercent.vue +35 -0
- package/app/components/element/UiRelativeTime.vue +58 -0
- package/app/components/element/UiScrollFade.vue +16 -0
- package/app/components/element/UiSelect.vue +68 -0
- package/app/components/element/UiSeverityDot.vue +27 -0
- package/app/components/element/UiSiteFavicon.vue +24 -0
- package/app/components/element/UiSkeleton.vue +148 -0
- package/app/components/element/UiSlotText.vue +65 -0
- package/app/components/element/UiSourceLogos.vue +113 -0
- package/app/components/element/UiSourceRail.vue +71 -0
- package/app/components/element/UiStaticTerminal.vue +107 -0
- package/app/components/element/UiStatusBadge.vue +62 -0
- package/app/components/element/UiSyncDot.vue +46 -0
- package/app/components/element/UiTabs.vue +245 -0
- package/app/components/element/UiTogglePill.vue +86 -0
- package/app/components/element/UiTooltip.vue +390 -0
- package/app/components/element/UiTooltipGrid.vue +457 -0
- package/app/components/element/UiTrend.vue +184 -0
- package/app/components/element/UiTrialBenefits.vue +71 -0
- package/app/components/element/UiUrlLabel.vue +75 -0
- package/app/components/element/UiValenceDot.vue +37 -0
- package/app/components/element/UiWizardModal.vue +284 -0
- package/app/components/element/UiWizardNav.vue +59 -0
- package/app/components/element/UiWizardProgress.vue +78 -0
- package/app/components/element/UiWizardShell.vue +43 -0
- package/app/components/element/eject-menu.ts +359 -0
- package/app/components/element/eject-tracker.ts +34 -0
- package/app/components/element/onboarding-answers.ts +93 -0
- package/app/components/element/onboarding-intents.ts +19 -0
- package/app/composables/dataVizColors.ts +350 -0
- package/app/composables/formatting.ts +252 -0
- package/app/composables/semanticColors.ts +80 -0
- package/app/composables/useActivityOverlay.ts +55 -0
- package/app/composables/useAppShellNav.ts +21 -0
- package/app/composables/useChartBrush.ts +221 -0
- package/app/composables/useChartHover.ts +153 -0
- package/app/composables/useChartTickPlan.ts +186 -0
- package/app/composables/useCloseOnNavigation.ts +7 -0
- package/app/composables/useConfirm.ts +38 -0
- package/app/composables/useEdgeFade.ts +37 -0
- package/app/composables/useEject.ts +69 -0
- package/app/composables/useEntranceMotion.ts +22 -0
- package/app/composables/useFaviconBacking.ts +164 -0
- package/app/composables/useGridCols.ts +39 -0
- package/app/composables/useIssueOverlay.ts +43 -0
- package/app/composables/useOnboardingService.ts +106 -0
- package/app/composables/useOpenedOnce.ts +19 -0
- package/app/composables/useScrollSpy.ts +178 -0
- package/app/composables/useSquircleFallback.ts +21 -0
- package/app/composables/useWizardRoute.ts +123 -0
- package/app/plugins/favicon.ts +15 -0
- package/app/plugins/lazy-motion.ts +22 -0
- package/app/plugins/selection-highlight.client.ts +176 -0
- package/app/shared/cli.ts +10 -0
- package/app/shared/error-state.ts +150 -0
- package/app/shared/icons.ts +1 -0
- package/app/shared/motion.ts +1 -0
- package/app/shared/nav.ts +38 -0
- package/app/shared/navigation.ts +5 -0
- package/app/shared/static-terminal.ts +14 -0
- package/app/shared/table-export.ts +36 -0
- package/app/shared/table.ts +105 -0
- package/app/shared/ui-icons.ts +1 -0
- package/app/shared/view-state.ts +85 -0
- package/app/shared/widget-failure.ts +126 -0
- package/app/utils/app-favicon.ts +64 -0
- package/app/utils/chartAnnotations.ts +326 -0
- package/app/utils/countries.ts +337 -0
- package/app/utils/export-download.ts +100 -0
- package/app/utils/favicon-fallback.ts +12 -0
- package/app/utils/icon-color.ts +105 -0
- package/app/utils/number.ts +74 -0
- package/app/utils/render-link.ts +14 -0
- package/app/utils/slot-text.ts +18 -0
- package/app/utils/squircle.ts +154 -0
- package/app/utils/topEntityStack.ts +197 -0
- package/app/utils/vue.ts +57 -0
- package/build/design-system.ts +69 -0
- package/build/external-links.ts +38 -0
- package/build/fonts.ts +39 -0
- package/build/icon-server-bundle.ts +276 -0
- package/build/runtime/icon-server-bundle.ts +29 -0
- package/css/global.css +1896 -0
- package/nuxt.config.ts +143 -0
- package/package.json +93 -3
- package/public/fonts/README.md +33 -0
- package/public/fonts/fira-code-OFL.txt +93 -0
- package/public/fonts/fira-code-var-latin.woff2 +0 -0
- package/public/fonts/hubot-sans-OFL.txt +93 -0
- package/public/fonts/hubot-sans-var-latin.woff2 +0 -0
- package/server/api/favicon.get.ts +122 -0
- package/server/utils/favicon-source.ts +51 -0
- package/shared/contracts/feedback.ts +42 -0
- package/shared/contracts/index.ts +1 -0
- package/shared/delivery-channels.ts +53 -0
- package/shared/icons/index.ts +143 -0
- package/shared/icons/registry.ts +347 -0
- package/shared/icons/ui-slots.ts +58 -0
- package/shared/markdown.ts +44 -0
- package/shared/motion.ts +120 -0
- package/shared/rangi.ts +60 -0
- package/shared/rich-text.ts +34 -0
- package/shared/ui-icons.ts +6 -0
- package/shared/utils/urls.ts +64 -0
- package/types/index.ts +1 -0
- package/types/ui.ts +339 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed } from 'vue'
|
|
3
|
+
import { UiFavicon, UiTooltip } from '#components'
|
|
4
|
+
|
|
5
|
+
export interface FaviconStackSite {
|
|
6
|
+
/** Hostname, full URL, or `sc-domain:` property — `UiFavicon` normalises it. */
|
|
7
|
+
domain: string
|
|
8
|
+
/** The visible Site name. Names the Site in the tooltip and the fallback initial. */
|
|
9
|
+
label: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A row's Site identity, as favicons only.
|
|
14
|
+
*
|
|
15
|
+
* List rows repeat their Site on every line. Spelled out, the name is a second
|
|
16
|
+
* text column that competes with the diagnosis and truncates first on narrow
|
|
17
|
+
* screens. A framed favicon carries the same identity in 20px and reads as an
|
|
18
|
+
* icon, not as content. The name is one hover away, and the caller still owns
|
|
19
|
+
* the accessible name of any control the stack sits inside.
|
|
20
|
+
*
|
|
21
|
+
* Mirrors `UiSourceLogos`: the same frame, the same `-ml-1.5` overlap, the same
|
|
22
|
+
* `role="img"` wrapper. Sources say WHERE a finding came from; this says WHICH
|
|
23
|
+
* Site it belongs to. One Site renders as a single frame, which is the same
|
|
24
|
+
* markup with nothing to overlap.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const { sites, size = 'sm', max = 4 } = defineProps<{
|
|
28
|
+
sites: readonly FaviconStackSite[]
|
|
29
|
+
/** Favicon pixel size: `xs` is 14px, `sm` is 16px. */
|
|
30
|
+
size?: 'xs' | 'sm'
|
|
31
|
+
/** Favicons rendered before the rest collapse into a `+N` frame. */
|
|
32
|
+
max?: number
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
const shown = computed(() => sites.slice(0, Math.max(1, max)))
|
|
36
|
+
const overflow = computed(() => sites.length - shown.value.length)
|
|
37
|
+
|
|
38
|
+
// Every Site is named, including the ones behind `+N` — the tooltip is the only
|
|
39
|
+
// place the reader can recover a name the row no longer prints.
|
|
40
|
+
const accessibleLabel = computed(() => sites.map(site => site.label).join(', '))
|
|
41
|
+
|
|
42
|
+
const iconPixels = computed(() => (size === 'xs' ? 14 : 16))
|
|
43
|
+
const frameClass = computed(() => (size === 'xs' ? 'size-5' : 'size-6'))
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<template>
|
|
47
|
+
<!--
|
|
48
|
+
`trigger-as="child"` so the trigger IS the stack. The default `span` wrapper
|
|
49
|
+
would add an inline-block layer inside the row's flex line, and the `button`
|
|
50
|
+
variant would nest a control inside rows that are themselves one button or
|
|
51
|
+
one link.
|
|
52
|
+
-->
|
|
53
|
+
<UiTooltip v-if="sites.length" :text="accessibleLabel" trigger-as="child">
|
|
54
|
+
<span
|
|
55
|
+
class="inline-flex shrink-0 items-center"
|
|
56
|
+
role="img"
|
|
57
|
+
:aria-label="accessibleLabel"
|
|
58
|
+
>
|
|
59
|
+
<span
|
|
60
|
+
v-for="(site, index) in shown"
|
|
61
|
+
:key="site.domain"
|
|
62
|
+
class="inline-flex shrink-0 items-center justify-center rounded-md border border-default bg-default shadow-xs"
|
|
63
|
+
:class="[frameClass, index ? '-ml-1.5' : '']"
|
|
64
|
+
aria-hidden="true"
|
|
65
|
+
>
|
|
66
|
+
<!-- `fallbackLabel` so a domain with no reachable favicon still shows an
|
|
67
|
+
initial rather than an empty frame — the frame IS the identity here. -->
|
|
68
|
+
<UiFavicon
|
|
69
|
+
:domain="site.domain"
|
|
70
|
+
:size="iconPixels"
|
|
71
|
+
:fallback-label="site.label"
|
|
72
|
+
fallback-surface="transparent"
|
|
73
|
+
decorative
|
|
74
|
+
/>
|
|
75
|
+
</span>
|
|
76
|
+
<span
|
|
77
|
+
v-if="overflow > 0"
|
|
78
|
+
class="-ml-1.5 inline-flex shrink-0 items-center justify-center rounded-md border border-default bg-default text-mini font-medium tabular-nums leading-none text-muted shadow-xs"
|
|
79
|
+
:class="frameClass"
|
|
80
|
+
aria-hidden="true"
|
|
81
|
+
>+{{ overflow }}</span>
|
|
82
|
+
</span>
|
|
83
|
+
</UiTooltip>
|
|
84
|
+
</template>
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick, ref, watch } from 'vue'
|
|
3
|
+
import { ToggleGroupItem, ToggleGroupRoot, UiIcon, UiMetricLabel, UInput, UiPopover } from '#components'
|
|
4
|
+
// Presentational filter primitive: a popover combining a dimension search,
|
|
5
|
+
// dimension shortcuts, a branded toggle, saved filters and preset filters.
|
|
6
|
+
// All data is prop-driven and actions are emitted — the consumer owns
|
|
7
|
+
// persistence and what each dimension/preset means.
|
|
8
|
+
|
|
9
|
+
interface DimensionOption {
|
|
10
|
+
label: string
|
|
11
|
+
value: string
|
|
12
|
+
icon: string
|
|
13
|
+
}
|
|
14
|
+
interface SavedFilter {
|
|
15
|
+
id: string
|
|
16
|
+
label: string
|
|
17
|
+
}
|
|
18
|
+
interface PresetFilter {
|
|
19
|
+
id: string
|
|
20
|
+
label: string
|
|
21
|
+
icon?: string
|
|
22
|
+
}
|
|
23
|
+
interface BrandedOption {
|
|
24
|
+
label: string
|
|
25
|
+
value: string
|
|
26
|
+
icon?: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
dimensions = [],
|
|
31
|
+
dimensionValues = {},
|
|
32
|
+
brandedOptions = [],
|
|
33
|
+
savedFilters = [],
|
|
34
|
+
presetFilters = [],
|
|
35
|
+
activePreset = '',
|
|
36
|
+
activeCount = 0,
|
|
37
|
+
label = 'Filter',
|
|
38
|
+
searchable = true,
|
|
39
|
+
searchPlaceholder = 'Search…',
|
|
40
|
+
showSaved = true,
|
|
41
|
+
} = defineProps<{
|
|
42
|
+
/** Dimension shortcuts (Query/Page/Country/Device). Clicking opens that dimension's `#picker` sub-view. */
|
|
43
|
+
dimensions?: DimensionOption[]
|
|
44
|
+
/** Current applied value per dimension (keyed by dimension value) — shown as a trailing label on the row. */
|
|
45
|
+
dimensionValues?: Record<string, string>
|
|
46
|
+
/** Branded segmented toggle options; omit to hide the section. */
|
|
47
|
+
brandedOptions?: BrandedOption[]
|
|
48
|
+
/** User-persisted saved filters. */
|
|
49
|
+
savedFilters?: SavedFilter[]
|
|
50
|
+
/** Built-in preset filters. */
|
|
51
|
+
presetFilters?: PresetFilter[]
|
|
52
|
+
/** The currently-applied preset id — shown active in the list. */
|
|
53
|
+
activePreset?: string
|
|
54
|
+
/** Count shown in the trigger badge. */
|
|
55
|
+
activeCount?: number
|
|
56
|
+
/** Trigger label. */
|
|
57
|
+
label?: string
|
|
58
|
+
/** Show the in-popover search input. Disable when the consumer renders its own. */
|
|
59
|
+
searchable?: boolean
|
|
60
|
+
/** Placeholder for the search input. */
|
|
61
|
+
searchPlaceholder?: string
|
|
62
|
+
/** Show the Saved Filters section (header + Save current + list). Disable on
|
|
63
|
+
* surfaces that don't implement saved filters so no dead "Save current"
|
|
64
|
+
* button renders. */
|
|
65
|
+
showSaved?: boolean
|
|
66
|
+
}>()
|
|
67
|
+
|
|
68
|
+
const emit = defineEmits<{
|
|
69
|
+
selectDimension: [value: string]
|
|
70
|
+
applySaved: [id: string]
|
|
71
|
+
removeSaved: [id: string]
|
|
72
|
+
applyPreset: [id: string]
|
|
73
|
+
save: []
|
|
74
|
+
clear: []
|
|
75
|
+
}>()
|
|
76
|
+
const search = defineModel<string>('search', { default: '' })
|
|
77
|
+
const branded = defineModel<string>('branded', { default: '' })
|
|
78
|
+
|
|
79
|
+
const open = ref(false)
|
|
80
|
+
|
|
81
|
+
// Two-level popover: selecting a dimension row swaps the menu for that
|
|
82
|
+
// dimension's value picker (rendered via the `#picker` slot) with a back
|
|
83
|
+
// affordance, matching the seogets filter UX. Reset when the popover closes.
|
|
84
|
+
const activeDimension = ref('')
|
|
85
|
+
const activeDimensionLabel = computed(() => dimensions.find(d => d.value === activeDimension.value)?.label ?? '')
|
|
86
|
+
const backBtn = ref<HTMLButtonElement | null>(null)
|
|
87
|
+
const dimensionsRoot = ref<HTMLElement | null>(null)
|
|
88
|
+
|
|
89
|
+
function openDimension(value: string) {
|
|
90
|
+
activeDimension.value = value
|
|
91
|
+
emit('selectDimension', value)
|
|
92
|
+
}
|
|
93
|
+
function closeDimension() {
|
|
94
|
+
activeDimension.value = ''
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Keep keyboard focus inside the popover when the sub-view swaps: focus the
|
|
98
|
+
// back affordance on entering a dimension, and return focus to the originating
|
|
99
|
+
// dimension row when leaving (falling back to the dimensions group).
|
|
100
|
+
watch(activeDimension, async (value, prev) => {
|
|
101
|
+
await nextTick()
|
|
102
|
+
if (value) {
|
|
103
|
+
backBtn.value?.focus()
|
|
104
|
+
}
|
|
105
|
+
else if (prev) {
|
|
106
|
+
const row = dimensionsRoot.value?.querySelector<HTMLButtonElement>(`[data-dimension="${prev}"]`)
|
|
107
|
+
;(row ?? dimensionsRoot.value?.querySelector<HTMLButtonElement>('button'))?.focus()
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
watch(open, (v) => {
|
|
112
|
+
if (!v)
|
|
113
|
+
activeDimension.value = ''
|
|
114
|
+
})
|
|
115
|
+
</script>
|
|
116
|
+
|
|
117
|
+
<template>
|
|
118
|
+
<UiPopover v-model:open="open" :content="{ align: 'start', side: 'bottom', sideOffset: 8 }">
|
|
119
|
+
<button
|
|
120
|
+
type="button"
|
|
121
|
+
:aria-label="activeCount ? `Filter (${activeCount} active)` : 'Filter'"
|
|
122
|
+
:aria-expanded="open"
|
|
123
|
+
class="cursor-pointer inline-flex items-center justify-center gap-1.5 px-2 py-1.5 min-h-11 min-w-11 sm:min-h-0 sm:min-w-0 rounded-lg text-xs font-medium transition-colors duration-150 focus-visible:ring-2 focus-visible:ring-primary outline-none"
|
|
124
|
+
:class="open || activeCount ? 'bg-elevated text-default' : 'text-muted hover:text-default hover:bg-[var(--ui-bg-elevated)]/50'"
|
|
125
|
+
>
|
|
126
|
+
<UiIcon name="filter" class="size-3.5" aria-hidden="true" />
|
|
127
|
+
<span data-toolbar-label>{{ label }}</span>
|
|
128
|
+
<span
|
|
129
|
+
v-if="activeCount"
|
|
130
|
+
class="min-w-4 px-1 h-4 inline-flex items-center justify-center rounded-full bg-primary text-inverted text-mini font-semibold tabular-nums"
|
|
131
|
+
>{{ activeCount }}</span>
|
|
132
|
+
</button>
|
|
133
|
+
|
|
134
|
+
<template #panel>
|
|
135
|
+
<div class="w-[270px] py-1.5">
|
|
136
|
+
<!-- Dimension value-picker sub-view: replaces the menu while a
|
|
137
|
+
dimension is active, with a back affordance. -->
|
|
138
|
+
<template v-if="activeDimension">
|
|
139
|
+
<button
|
|
140
|
+
ref="backBtn"
|
|
141
|
+
type="button"
|
|
142
|
+
:aria-label="`Back to filters, from ${activeDimensionLabel}`"
|
|
143
|
+
class="cursor-pointer w-full flex items-center gap-1.5 px-3 py-2.5 sm:py-1 text-xs font-medium text-muted hover:text-default transition-colors outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset"
|
|
144
|
+
@click="closeDimension"
|
|
145
|
+
>
|
|
146
|
+
<UiIcon name="chevron-left" class="size-3.5 shrink-0" aria-hidden="true" />
|
|
147
|
+
<span>{{ activeDimensionLabel }}</span>
|
|
148
|
+
</button>
|
|
149
|
+
<div class="mx-3 my-1.5 border-t border-default/50" role="separator" />
|
|
150
|
+
<div class="px-2.5 pb-1">
|
|
151
|
+
<slot name="picker" :dimension="activeDimension" :close="closeDimension" />
|
|
152
|
+
</div>
|
|
153
|
+
</template>
|
|
154
|
+
|
|
155
|
+
<template v-else>
|
|
156
|
+
<!-- Active-filter header + one-click clear (only when something is applied). -->
|
|
157
|
+
<div v-if="activeCount" class="px-3 pt-0.5 pb-1.5 flex items-center justify-between">
|
|
158
|
+
<UiMetricLabel aria-hidden="true">
|
|
159
|
+
{{ activeCount }} active
|
|
160
|
+
</UiMetricLabel>
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
aria-label="Clear all filters"
|
|
164
|
+
class="min-h-11 cursor-pointer rounded text-mini text-muted outline-none hover:text-default focus-visible:ring-2 focus-visible:ring-primary sm:min-h-0"
|
|
165
|
+
@click="emit('clear')"
|
|
166
|
+
>
|
|
167
|
+
Clear all
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<!-- Search -->
|
|
172
|
+
<div v-if="searchable" class="px-2 pb-1.5">
|
|
173
|
+
<UInput
|
|
174
|
+
v-model="search"
|
|
175
|
+
type="search"
|
|
176
|
+
:placeholder="searchPlaceholder"
|
|
177
|
+
:aria-label="searchPlaceholder"
|
|
178
|
+
icon="search"
|
|
179
|
+
size="xs"
|
|
180
|
+
class="w-full"
|
|
181
|
+
autocomplete="off"
|
|
182
|
+
/>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<!-- Facets slot (consumer-rendered value editors, e.g. country/device) -->
|
|
186
|
+
<div v-if="$slots.facets" class="px-2.5 pb-1.5">
|
|
187
|
+
<slot name="facets" />
|
|
188
|
+
</div>
|
|
189
|
+
|
|
190
|
+
<!-- Dimension shortcuts -->
|
|
191
|
+
<div v-if="dimensions.length" ref="dimensionsRoot" role="group" aria-label="Filter dimensions">
|
|
192
|
+
<button
|
|
193
|
+
v-for="d in dimensions"
|
|
194
|
+
:key="d.value"
|
|
195
|
+
type="button"
|
|
196
|
+
:data-dimension="d.value"
|
|
197
|
+
:aria-label="`${d.label}${dimensionValues[d.value] ? `: ${dimensionValues[d.value]}` : ''}`"
|
|
198
|
+
class="cursor-pointer w-full flex items-center gap-2 px-3 py-2.5 sm:py-1 text-xs text-muted hover:text-default hover:bg-[var(--ui-bg-elevated)]/50 transition-colors outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset"
|
|
199
|
+
@click="openDimension(d.value)"
|
|
200
|
+
>
|
|
201
|
+
<UiIcon :name="d.icon" class="size-3.5 shrink-0 text-dimmed" aria-hidden="true" />
|
|
202
|
+
<span class="flex-1 text-left">{{ d.label }}</span>
|
|
203
|
+
<span v-if="dimensionValues[d.value]" class="text-default font-medium truncate max-w-28">{{ dimensionValues[d.value] }}</span>
|
|
204
|
+
<UiIcon name="chevron-right" class="size-3 shrink-0 text-dimmed" aria-hidden="true" />
|
|
205
|
+
</button>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<!-- Branded toggle -->
|
|
209
|
+
<template v-if="brandedOptions.length">
|
|
210
|
+
<div class="mx-3 my-1.5 border-t border-default/50" role="separator" />
|
|
211
|
+
<div class="px-3 pb-1">
|
|
212
|
+
<UiMetricLabel aria-hidden="true">
|
|
213
|
+
Branded
|
|
214
|
+
</UiMetricLabel>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="px-2.5 pb-1">
|
|
217
|
+
<ToggleGroupRoot
|
|
218
|
+
:model-value="branded"
|
|
219
|
+
type="single"
|
|
220
|
+
aria-label="Branded filter"
|
|
221
|
+
class="flex items-center gap-1 p-0.5 rounded-md bg-[var(--ui-bg-elevated)]/60"
|
|
222
|
+
@update:model-value="(v: unknown) => { if (typeof v === 'string' && v) branded = v }"
|
|
223
|
+
>
|
|
224
|
+
<ToggleGroupItem
|
|
225
|
+
v-for="b in brandedOptions"
|
|
226
|
+
:key="b.value"
|
|
227
|
+
:value="b.value"
|
|
228
|
+
class="cursor-pointer flex-1 inline-flex items-center justify-center gap-1 px-2 py-1 rounded text-mini font-medium whitespace-nowrap transition-colors outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
|
229
|
+
:class="branded === b.value
|
|
230
|
+
? 'bg-default text-default'
|
|
231
|
+
: 'text-muted hover:text-default'"
|
|
232
|
+
>
|
|
233
|
+
<UiIcon v-if="b.icon" :name="b.icon" class="size-3" aria-hidden="true" />
|
|
234
|
+
<span>{{ b.label }}</span>
|
|
235
|
+
</ToggleGroupItem>
|
|
236
|
+
</ToggleGroupRoot>
|
|
237
|
+
</div>
|
|
238
|
+
</template>
|
|
239
|
+
|
|
240
|
+
<!-- Saved filters -->
|
|
241
|
+
<template v-if="showSaved">
|
|
242
|
+
<div class="mx-3 my-1.5 border-t border-default/50" role="separator" />
|
|
243
|
+
<div class="px-3 pb-1 flex items-center justify-between">
|
|
244
|
+
<UiMetricLabel aria-hidden="true">
|
|
245
|
+
Saved Filters
|
|
246
|
+
</UiMetricLabel>
|
|
247
|
+
<button
|
|
248
|
+
type="button"
|
|
249
|
+
class="cursor-pointer text-mini text-primary hover:underline outline-none focus-visible:ring-2 focus-visible:ring-primary rounded"
|
|
250
|
+
@click="emit('save')"
|
|
251
|
+
>
|
|
252
|
+
Save current
|
|
253
|
+
</button>
|
|
254
|
+
</div>
|
|
255
|
+
<div v-if="savedFilters.length" role="group" aria-label="Saved filters">
|
|
256
|
+
<div
|
|
257
|
+
v-for="s in savedFilters"
|
|
258
|
+
:key="s.id"
|
|
259
|
+
class="group w-full flex items-center gap-2 px-3 py-2.5 sm:py-1 text-xs text-muted hover:bg-[var(--ui-bg-elevated)]/50 transition-colors"
|
|
260
|
+
>
|
|
261
|
+
<button
|
|
262
|
+
type="button"
|
|
263
|
+
class="cursor-pointer flex-1 flex items-center gap-2 text-left hover:text-default outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset rounded"
|
|
264
|
+
@click="emit('applySaved', s.id)"
|
|
265
|
+
>
|
|
266
|
+
<UiIcon name="bookmark" class="size-3.5 shrink-0 text-dimmed" aria-hidden="true" />
|
|
267
|
+
<span class="truncate">{{ s.label }}</span>
|
|
268
|
+
</button>
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
:aria-label="`Remove saved filter ${s.label}`"
|
|
272
|
+
class="cursor-pointer opacity-0 group-hover:opacity-100 focus-visible:opacity-100 text-dimmed hover:text-error transition-[color,opacity] outline-none focus-visible:ring-2 focus-visible:ring-primary rounded"
|
|
273
|
+
@click="emit('removeSaved', s.id)"
|
|
274
|
+
>
|
|
275
|
+
<UiIcon name="close" class="size-3" aria-hidden="true" />
|
|
276
|
+
</button>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
<p v-else class="px-3 pb-1 text-mini text-dimmed leading-snug">
|
|
280
|
+
Add filters and save them to quickly access them later.
|
|
281
|
+
</p>
|
|
282
|
+
</template>
|
|
283
|
+
|
|
284
|
+
<!-- Preset filters -->
|
|
285
|
+
<template v-if="presetFilters.length">
|
|
286
|
+
<div class="mx-3 my-1.5 border-t border-default/50" role="separator" />
|
|
287
|
+
<div class="px-3 pb-1">
|
|
288
|
+
<UiMetricLabel aria-hidden="true">
|
|
289
|
+
Preset Filters
|
|
290
|
+
</UiMetricLabel>
|
|
291
|
+
</div>
|
|
292
|
+
<div role="group" aria-label="Preset filters">
|
|
293
|
+
<button
|
|
294
|
+
v-for="p in presetFilters"
|
|
295
|
+
:key="p.id"
|
|
296
|
+
type="button"
|
|
297
|
+
:aria-pressed="activePreset === p.id"
|
|
298
|
+
class="relative flex min-h-11 w-full cursor-pointer items-center gap-2 px-3 py-2.5 text-xs outline-none transition-colors focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-inset sm:min-h-0 sm:py-1"
|
|
299
|
+
:class="activePreset === p.id
|
|
300
|
+
? 'text-default bg-elevated'
|
|
301
|
+
: 'text-muted hover:text-default hover:bg-[var(--ui-bg-elevated)]/50'"
|
|
302
|
+
@click="emit('applyPreset', p.id)"
|
|
303
|
+
>
|
|
304
|
+
<span
|
|
305
|
+
v-if="activePreset === p.id"
|
|
306
|
+
class="absolute left-0 inset-y-0.5 w-[2px] rounded-full bg-primary"
|
|
307
|
+
aria-hidden="true"
|
|
308
|
+
/>
|
|
309
|
+
<UiIcon :name="p.icon ?? 'ai'" class="size-3.5 shrink-0 text-dimmed" aria-hidden="true" />
|
|
310
|
+
<span class="flex-1 text-left">{{ p.label }}</span>
|
|
311
|
+
<UiIcon
|
|
312
|
+
v-if="activePreset === p.id"
|
|
313
|
+
name="check"
|
|
314
|
+
class="size-3 text-primary"
|
|
315
|
+
aria-hidden="true"
|
|
316
|
+
/>
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
</template>
|
|
320
|
+
</template>
|
|
321
|
+
</div>
|
|
322
|
+
</template>
|
|
323
|
+
</UiPopover>
|
|
324
|
+
</template>
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { useHead } from 'nuxt/app'
|
|
3
|
+
import { ClientOnly, UiAtmosphere, UiIcon, ULink } from '#components'
|
|
4
|
+
import { useColorMode } from '#imports'
|
|
5
|
+
// The focused, full-bleed surface shared by the auth screens (login, sign-up)
|
|
6
|
+
// and the onboarding wizard: dashboard-theme scope, a masked dot-grid backdrop,
|
|
7
|
+
// and a top-left exit link. No dashboard chrome. The single source of this
|
|
8
|
+
// substrate — the pro-auth layout and UiWizardShell both compose it, and the
|
|
9
|
+
// brand-kit showcase renders it directly.
|
|
10
|
+
//
|
|
11
|
+
// Height is the consumer's call (attrs fall through to the root <main>): the
|
|
12
|
+
// live pages pass `min-h-screen`; an embedded preview passes a fixed height.
|
|
13
|
+
//
|
|
14
|
+
// - align="center" centres a single child both axes (auth card).
|
|
15
|
+
// - align="top" stacks content from the top (wizard column).
|
|
16
|
+
// - exitTo renders a real link; otherwise the exit affordance emits `exit`.
|
|
17
|
+
const {
|
|
18
|
+
align = 'center',
|
|
19
|
+
exitLabel = 'Home',
|
|
20
|
+
exitTo,
|
|
21
|
+
exitExternal = false,
|
|
22
|
+
showExit = true,
|
|
23
|
+
as = 'main',
|
|
24
|
+
} = defineProps<{
|
|
25
|
+
align?: 'center' | 'top'
|
|
26
|
+
exitLabel?: string
|
|
27
|
+
exitTo?: string
|
|
28
|
+
exitExternal?: boolean
|
|
29
|
+
showExit?: boolean
|
|
30
|
+
/**
|
|
31
|
+
* Root element. Defaults to `main` (the page-level landmark) for the real
|
|
32
|
+
* full-page surfaces. Pass `as="div"` when more than one surface renders on a
|
|
33
|
+
* page (e.g. the brand-kit showcase) to avoid duplicate `<main>` landmarks.
|
|
34
|
+
*/
|
|
35
|
+
as?: string
|
|
36
|
+
}>()
|
|
37
|
+
|
|
38
|
+
const emit = defineEmits<{ exit: [] }>()
|
|
39
|
+
const colorMode = useColorMode()
|
|
40
|
+
|
|
41
|
+
function toggleColorMode() {
|
|
42
|
+
colorMode.preference = colorMode.value === 'dark' ? 'light' : 'dark'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The dashboard palette is keyed on `.dark.dashboard-theme` / `.light.dashboard-theme`
|
|
46
|
+
// — BOTH classes on the same element — and the color-mode class only ever lands
|
|
47
|
+
// on `<html>`. A `dashboard-theme` that sits on this div alone therefore misses
|
|
48
|
+
// every `--ui-bg-*` / `--ui-text-*` override and the surface silently renders on
|
|
49
|
+
// the marketing slate palette: in dark that means page background and outline
|
|
50
|
+
// controls both resolve to the same near-black, so inputs and buttons vanish.
|
|
51
|
+
// Mirror it onto `<html>` whenever this surface IS the page (as="main"); an
|
|
52
|
+
// embedded preview (`as="div"`, brand-kit) must not restyle its host page.
|
|
53
|
+
if (as === 'main')
|
|
54
|
+
useHead({ htmlAttrs: { class: 'dashboard-theme' } })
|
|
55
|
+
</script>
|
|
56
|
+
|
|
57
|
+
<template>
|
|
58
|
+
<component
|
|
59
|
+
:is="as"
|
|
60
|
+
class="ui-focused dashboard-theme relative overflow-hidden flex flex-col items-center px-4 pb-10"
|
|
61
|
+
:class="[
|
|
62
|
+
align === 'center' ? 'justify-center' : '',
|
|
63
|
+
as === 'main' && showExit ? 'pt-20 sm:pt-10' : 'pt-10',
|
|
64
|
+
]"
|
|
65
|
+
:data-align="align"
|
|
66
|
+
>
|
|
67
|
+
<UiAtmosphere preset="front-door" />
|
|
68
|
+
<div aria-hidden="true" class="ui-focused-dots pointer-events-none absolute inset-0" />
|
|
69
|
+
|
|
70
|
+
<ULink
|
|
71
|
+
v-if="showExit && exitTo"
|
|
72
|
+
:to="exitTo"
|
|
73
|
+
:external="exitExternal"
|
|
74
|
+
class="absolute top-3 left-3 z-10 inline-flex min-h-11 items-center gap-1 px-2 text-sm text-muted transition-colors hover:text-highlighted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md sm:top-5 sm:left-5"
|
|
75
|
+
>
|
|
76
|
+
<UiIcon name="back" class="size-3.5 shrink-0" aria-hidden="true" />
|
|
77
|
+
<span class="leading-none">{{ exitLabel }}</span>
|
|
78
|
+
</ULink>
|
|
79
|
+
<button
|
|
80
|
+
v-else-if="showExit"
|
|
81
|
+
type="button"
|
|
82
|
+
class="absolute top-3 left-3 z-10 inline-flex min-h-11 items-center gap-1 px-2 text-sm text-muted transition-colors hover:text-highlighted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-md sm:top-5 sm:left-5"
|
|
83
|
+
@click="emit('exit')"
|
|
84
|
+
>
|
|
85
|
+
<UiIcon name="back" class="size-3.5 shrink-0" aria-hidden="true" />
|
|
86
|
+
<span class="leading-none">{{ exitLabel }}</span>
|
|
87
|
+
</button>
|
|
88
|
+
|
|
89
|
+
<ClientOnly v-if="as === 'main'">
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
class="group/theme absolute top-3 right-3 z-10 inline-flex size-11 items-center justify-center rounded-xl text-muted transition-colors hover:text-highlighted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary sm:top-5 sm:right-5"
|
|
93
|
+
:aria-label="`Switch to ${colorMode.value === 'dark' ? 'light' : 'dark'} mode`"
|
|
94
|
+
:title="`Switch to ${colorMode.value === 'dark' ? 'light' : 'dark'} mode`"
|
|
95
|
+
@click="toggleColorMode"
|
|
96
|
+
>
|
|
97
|
+
<span class="inline-flex size-8 items-center justify-center rounded-lg bg-default/60 ring-1 ring-default backdrop-blur-sm transition-colors group-hover/theme:bg-elevated">
|
|
98
|
+
<UiIcon
|
|
99
|
+
:name="colorMode.value === 'dark' ? 'light' : 'dark'"
|
|
100
|
+
class="size-4"
|
|
101
|
+
aria-hidden="true"
|
|
102
|
+
/>
|
|
103
|
+
</span>
|
|
104
|
+
</button>
|
|
105
|
+
<template #fallback>
|
|
106
|
+
<span class="absolute top-3 right-3 size-11 sm:top-5 sm:right-5" aria-hidden="true" />
|
|
107
|
+
</template>
|
|
108
|
+
</ClientOnly>
|
|
109
|
+
|
|
110
|
+
<slot />
|
|
111
|
+
</component>
|
|
112
|
+
</template>
|
|
113
|
+
|
|
114
|
+
<style scoped>
|
|
115
|
+
.ui-focused-dots {
|
|
116
|
+
background-image: radial-gradient(circle at 1px 1px, color-mix(in oklab, var(--ui-bg-inverted) 8%, transparent) 1px, transparent 0);
|
|
117
|
+
background-size: 28px 28px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.ui-focused[data-align="center"] .ui-focused-dots {
|
|
121
|
+
mask-image: radial-gradient(ellipse 60% 50% at 50% 50%, black 0%, transparent 75%);
|
|
122
|
+
-webkit-mask-image: radial-gradient(ellipse 60% 50% at 50% 50%, black 0%, transparent 75%);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.ui-focused[data-align="top"] .ui-focused-dots {
|
|
126
|
+
mask-image: radial-gradient(ellipse 70% 50% at 50% 30%, black 0%, transparent 75%);
|
|
127
|
+
-webkit-mask-image: radial-gradient(ellipse 70% 50% at 50% 30%, black 0%, transparent 75%);
|
|
128
|
+
}
|
|
129
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { HealthStatus } from '../../composables/semanticColors'
|
|
3
|
+
import { healthColors } from '../../composables/semanticColors'
|
|
4
|
+
|
|
5
|
+
const { health = 'unknown', size = 'sm', pulse = false, label } = defineProps<{
|
|
6
|
+
health?: HealthStatus | null
|
|
7
|
+
size?: 'xs' | 'sm' | 'md'
|
|
8
|
+
pulse?: boolean
|
|
9
|
+
/**
|
|
10
|
+
* Accessible name for the status. Pass when the dot stands alone (no adjacent
|
|
11
|
+
* text), so the color is not the only channel conveying state. Omit when a
|
|
12
|
+
* visible text label sits next to the dot — it stays decorative then.
|
|
13
|
+
*/
|
|
14
|
+
label?: string
|
|
15
|
+
}>()
|
|
16
|
+
|
|
17
|
+
const sizeClass = { xs: 'size-1.5', sm: 'size-2', md: 'size-2.5' } as const
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<span
|
|
22
|
+
class="rounded-full shrink-0"
|
|
23
|
+
:class="[
|
|
24
|
+
sizeClass[size],
|
|
25
|
+
healthColors(health).dot,
|
|
26
|
+
pulse && 'motion-safe:animate-pulse',
|
|
27
|
+
]"
|
|
28
|
+
:role="label ? 'img' : undefined"
|
|
29
|
+
:aria-label="label || undefined"
|
|
30
|
+
:aria-hidden="label ? undefined : 'true'"
|
|
31
|
+
/>
|
|
32
|
+
</template>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { UiIcon, UiTooltip } from '#components'
|
|
3
|
+
/**
|
|
4
|
+
* UiHelpLabel
|
|
5
|
+
*
|
|
6
|
+
* Label text with an inline (?) help icon that shows a tooltip on hover.
|
|
7
|
+
* Used by UiStat, UiDataList, and anywhere a title needs contextual help.
|
|
8
|
+
*/
|
|
9
|
+
const { text, tooltip, tooltipTitle } = defineProps<{
|
|
10
|
+
text: string
|
|
11
|
+
tooltip: string
|
|
12
|
+
/** Bold title in tooltip panel. Defaults to `text`. */
|
|
13
|
+
tooltipTitle?: string
|
|
14
|
+
}>()
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<template>
|
|
18
|
+
<UiTooltip :title="tooltipTitle || text" :description="tooltip" trigger-as="button">
|
|
19
|
+
<!-- The trigger is a button, which centres its text; a label that wraps
|
|
20
|
+
must stay start-aligned like the heading it sits in. `mx-0.5` cancels
|
|
21
|
+
the button's `-m-0.5` hit-area bleed, so the label starts on the same
|
|
22
|
+
edge as a plain heading beside it. -->
|
|
23
|
+
<span class="mx-0.5 inline-flex items-center gap-1 text-left">
|
|
24
|
+
{{ text }}
|
|
25
|
+
<UiIcon name="help" class="size-3 opacity-50 shrink-0" aria-hidden="true" />
|
|
26
|
+
</span>
|
|
27
|
+
</UiTooltip>
|
|
28
|
+
</template>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { UiIcon } from '../../shared/ui-icons'
|
|
3
|
+
import { UIcon } from '#components'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* UiIcon — the icon primitive. A thin, typed wrapper over Nuxt UI's `<UIcon>`.
|
|
7
|
+
*
|
|
8
|
+
* The `name` is a curated semantic role (`refresh`, `next`, `delete`, …) or an
|
|
9
|
+
* explicitly sourced `i-*` id. Roles resolve to the active set's icon via @nuxt/icon's global aliases
|
|
10
|
+
* (registered in the design-system nuxt.config from the registry), so this component
|
|
11
|
+
* passes `name` straight through — no runtime registry, nothing extra shipped to the
|
|
12
|
+
* client. The `UiIcon` type keeps role autocomplete; type-only import is erased at build.
|
|
13
|
+
*
|
|
14
|
+
* The brand-kit set comparison calls `resolveUiIcon(role, set)` and feeds the
|
|
15
|
+
* resulting whitelisted id back through this primitive.
|
|
16
|
+
*/
|
|
17
|
+
const { name } = defineProps<{ name?: UiIcon }>()
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<template>
|
|
21
|
+
<UIcon :name="name" />
|
|
22
|
+
</template>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<script setup lang="ts" generic="T extends InputValue = InputValue">
|
|
2
|
+
import type { InputEmits, InputProps, InputValue } from '@nuxt/ui'
|
|
3
|
+
import { UInput } from '#components'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* UiInput — thin wrapper around Nuxt UI v4's UInput.
|
|
7
|
+
*
|
|
8
|
+
* Tokens (radius, ring, background) are already centralized in
|
|
9
|
+
* `app.config.ts`'s `ui.input` — this component isn't a re-theme, it's the
|
|
10
|
+
* single legal entry point for `<input>`-style fields (raw `<UInput>` is
|
|
11
|
+
* ESLint-banned via `eslint/selectors.ts`) so a future behavioral change
|
|
12
|
+
* lands in one file instead of another repo-wide sweep. Every prop, emit,
|
|
13
|
+
* and slot passes straight through unchanged.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
defineOptions({ inheritAttrs: false })
|
|
17
|
+
|
|
18
|
+
const props = defineProps<Props>()
|
|
19
|
+
const emit = defineEmits<InputEmits<T>>()
|
|
20
|
+
type Props = Omit<InputProps<T>, 'defaultValue'>
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<!-- @vue-generic {T, NonNullable<InputProps<T>['modelModifiers']>} -->
|
|
25
|
+
<UInput
|
|
26
|
+
v-bind="{ ...props, ...$attrs }"
|
|
27
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
28
|
+
@blur="emit('blur', $event)"
|
|
29
|
+
@change="emit('change', $event)"
|
|
30
|
+
>
|
|
31
|
+
<template v-for="(_, name) in ($slots as Record<string, unknown>)" #[name]="slotProps" :key="name">
|
|
32
|
+
<slot :name="name" v-bind="slotProps || {}" />
|
|
33
|
+
</template>
|
|
34
|
+
</UInput>
|
|
35
|
+
</template>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// Official Plausible Analytics brand mark, brand colour #5850EC baked into the
|
|
3
|
+
// asset (single-colour mark — there is no multicolour version). Inline SVG so the
|
|
4
|
+
// colour survives, vs an icon set that would monotone it to currentColor.
|
|
5
|
+
// Sizing/aria fall through from the parent (e.g. class="size-6").
|
|
6
|
+
</script>
|
|
7
|
+
|
|
8
|
+
<template>
|
|
9
|
+
<svg viewBox="0 0 24 24" fill="#5850EC" xmlns="http://www.w3.org/2000/svg">
|
|
10
|
+
<path d="M12.1835.0017c-.6378-.0097-1.2884.022-1.7246.0996C8.601.424 7.035 1.2116 5.7384 2.4782 4.406 3.7806 3.582 5.299 3.1818 7.1929l-.1387.6445c-.0118 5.3872-.0233 10.7744-.035 16.1617.2914.0081.591-.0392.8416-.0606 2.348-.2868 4.3442-1.7083 5.4315-3.8651.2749-.5497.472-1.182.6094-1.9707.1135-.6691.1195-.8915.1016-4.3807l-.0176-3.6737.1425-.3574c.1972-.49.7425-1.0352 1.2324-1.2324l.3574-.1426 3.3457-.0058c1.8401 0 3.4545-.025 3.58-.0489.5854-.1135 1.2118-.6027 1.4628-1.1464.0717-.1494.1671-.4415.209-.6387.0657-.3286.0604-.4186-.0352-.789-.2987-1.0993-1.3503-2.6234-2.4257-3.5136C16.6247 1.1638 15.2798.4887 13.828.1482c-.3824-.0866-1.0067-.1368-1.6445-.1465zm8.5369 6.8006c-.0506.1798-.098.3662-.172.5215-.3358.7278-1.0382 1.2776-1.8221 1.4296-3.6737.0566-2.5392.0561-3.6737.0566l-3.248.0059-.2695.1074c-.3135.1262-.827.6397-.9531.9531l-.1074.2676.0175 3.576c.0149 2.8888.007 3.5821-.0605 4.125a8.9918 8.9918 0 0 0 1.5683.1386c4.9662.0001 8.992-4.0258 8.992-8.992a8.9918 8.9918 0 0 0-.2715-2.1893Z" />
|
|
11
|
+
</svg>
|
|
12
|
+
</template>
|