@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.
- package/README.md +101 -2
- package/app/app.config.ts +596 -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 +232 -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 +125 -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 +55 -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 +68 -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 +29 -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/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 +47 -0
- package/build/icon-server-bundle.ts +276 -0
- package/build/runtime/icon-server-bundle.ts +29 -0
- package/css/global.css +1892 -0
- package/nuxt.config.ts +132 -0
- package/package.json +86 -3
- package/public/brand-icon-dark.png +0 -0
- package/public/brand-icon-light.png +0 -0
- package/public/favicon-dark.png +0 -0
- package/public/favicon-light.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/fonts/README.md +31 -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/public/icon-dark.png +0 -0
- package/public/icon.png +0 -0
- package/public/touch-icon-dark.png +0 -0
- package/public/touch-icon.png +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,150 @@
|
|
|
1
|
+
import type { UiIcon } from './ui-icons'
|
|
2
|
+
import { toWidgetFailure, WIDGET_FAILURE_COPY } from './widget-failure'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* What `UiErrorState` is allowed to say, and where the words came from.
|
|
6
|
+
*
|
|
7
|
+
* The old prop was `string | { message: string }`. Both arms rendered their
|
|
8
|
+
* string, so a caught query error and a hand-written sentence were the same
|
|
9
|
+
* type. Six pro pages passed the caught error, and ofetch writes the request
|
|
10
|
+
* into `message`: `[GET] "/api/pro/v1/sites/<id>/search/status": 500 Internal
|
|
11
|
+
* Server Error`. That put an internal route and a site id on a customer's
|
|
12
|
+
* screen, and credentials have reached error strings the same way before.
|
|
13
|
+
*
|
|
14
|
+
* The tag now records provenance, which is the thing the two arms never agreed
|
|
15
|
+
* on. A `wire` value is a caught object. It is typed `unknown`, so no template
|
|
16
|
+
* can read a string off it; only the HTTP status code is read, and only to pick
|
|
17
|
+
* copy this file owns. A `copy` value is a sentence the caller wrote, which is
|
|
18
|
+
* safe to render because a person chose the words.
|
|
19
|
+
*
|
|
20
|
+
* A caller can no longer pass a caught error where copy is expected: the free
|
|
21
|
+
* tools keep rendering their own vetted copy through `errorCopy`, and a pro
|
|
22
|
+
* page must say `wireError` and accept classified copy instead.
|
|
23
|
+
*/
|
|
24
|
+
export type ErrorStateSource
|
|
25
|
+
= | { readonly _tag: 'wire', readonly error: unknown }
|
|
26
|
+
| { readonly _tag: 'copy', readonly title: string, readonly detail?: string }
|
|
27
|
+
| {
|
|
28
|
+
readonly _tag: 'rate-limited'
|
|
29
|
+
readonly detail: string
|
|
30
|
+
readonly resetAt?: Date
|
|
31
|
+
readonly retryAfter?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Classify a caught value. The component reads its status code and nothing else.
|
|
36
|
+
*
|
|
37
|
+
* Pass the raw error. Wrapping it in `new Error(someMessage)` throws away the
|
|
38
|
+
* status code the classifier needs and buys no safety.
|
|
39
|
+
*/
|
|
40
|
+
export function wireError(error: unknown): ErrorStateSource {
|
|
41
|
+
return { _tag: 'wire', error }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Render a sentence the caller wrote. Use it for copy a person authored.
|
|
46
|
+
*
|
|
47
|
+
* Never pass an error's `message` through here. Use `wireError` for a caught
|
|
48
|
+
* value, so the status code picks the copy.
|
|
49
|
+
*/
|
|
50
|
+
export function errorCopy(title: string, detail?: string): ErrorStateSource {
|
|
51
|
+
return { _tag: 'copy', title, detail }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A quota refusal the caller already parsed, with the reset it read from the
|
|
56
|
+
* response headers. `wireError` covers a 429 too, but it cannot state a reset
|
|
57
|
+
* time, because a status code carries none.
|
|
58
|
+
*/
|
|
59
|
+
export function rateLimited(
|
|
60
|
+
detail: string,
|
|
61
|
+
reset?: { resetAt?: Date, retryAfter?: number },
|
|
62
|
+
): ErrorStateSource {
|
|
63
|
+
return { _tag: 'rate-limited', detail, resetAt: reset?.resetAt, retryAfter: reset?.retryAfter }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The next-step line under the message.
|
|
68
|
+
*
|
|
69
|
+
* Tagged because the two kinds behave differently: a `timing` line states a
|
|
70
|
+
* real reset the caller measured and always shows, while a `generic` line is
|
|
71
|
+
* filler that an `#action` slot replaces with something better.
|
|
72
|
+
*/
|
|
73
|
+
export type ErrorStateNextStep
|
|
74
|
+
= | { readonly _tag: 'timing', readonly text: string }
|
|
75
|
+
| { readonly _tag: 'generic', readonly text: string }
|
|
76
|
+
|
|
77
|
+
/** Everything the template renders. No field holds a caught value. */
|
|
78
|
+
export interface ErrorStateView {
|
|
79
|
+
readonly tone: 'error' | 'warning'
|
|
80
|
+
readonly icon: UiIcon
|
|
81
|
+
/** What happened. */
|
|
82
|
+
readonly title: string
|
|
83
|
+
/** What to do about it, when the failure is specific enough to say. */
|
|
84
|
+
readonly detail: string | null
|
|
85
|
+
readonly nextStep: ErrorStateNextStep | null
|
|
86
|
+
/** Whether retrying the same request can succeed. */
|
|
87
|
+
readonly retryable: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const GENERIC_NEXT_STEP = 'Try again in a moment. If it keeps happening, contact support.'
|
|
91
|
+
|
|
92
|
+
function resetHint(source: { resetAt?: Date, retryAfter?: number }, now: number): string | null {
|
|
93
|
+
if (source.retryAfter)
|
|
94
|
+
return `Try again in ${source.retryAfter} seconds`
|
|
95
|
+
|
|
96
|
+
if (!source.resetAt)
|
|
97
|
+
return null
|
|
98
|
+
|
|
99
|
+
const remaining = source.resetAt.getTime() - now
|
|
100
|
+
if (remaining <= 0)
|
|
101
|
+
return null
|
|
102
|
+
if (remaining < 60_000)
|
|
103
|
+
return `Resets in ${Math.ceil(remaining / 1000)} seconds`
|
|
104
|
+
if (remaining < 3_600_000)
|
|
105
|
+
return `Resets in ${Math.ceil(remaining / 60_000)} minutes`
|
|
106
|
+
return 'Resets at midnight UTC'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Turn a source into the words on screen. Pure: pass the clock in, so the
|
|
111
|
+
* reset hint is testable and the component owns no time.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveErrorState(source: ErrorStateSource, now: number): ErrorStateView {
|
|
114
|
+
if (source._tag === 'copy') {
|
|
115
|
+
return {
|
|
116
|
+
tone: 'error',
|
|
117
|
+
icon: 'caution',
|
|
118
|
+
title: source.title,
|
|
119
|
+
detail: source.detail ?? null,
|
|
120
|
+
nextStep: { _tag: 'generic', text: GENERIC_NEXT_STEP },
|
|
121
|
+
retryable: true,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (source._tag === 'rate-limited') {
|
|
126
|
+
const hint = resetHint(source, now)
|
|
127
|
+
return {
|
|
128
|
+
tone: 'warning',
|
|
129
|
+
icon: 'clock',
|
|
130
|
+
title: WIDGET_FAILURE_COPY['rate-limited'].title,
|
|
131
|
+
detail: source.detail,
|
|
132
|
+
nextStep: hint
|
|
133
|
+
? { _tag: 'timing', text: hint }
|
|
134
|
+
: { _tag: 'generic', text: WIDGET_FAILURE_COPY['rate-limited'].body },
|
|
135
|
+
retryable: true,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const failure = toWidgetFailure(source.error)
|
|
140
|
+
const copy = WIDGET_FAILURE_COPY[failure._tag]
|
|
141
|
+
const limited = failure._tag === 'rate-limited'
|
|
142
|
+
return {
|
|
143
|
+
tone: limited ? 'warning' : 'error',
|
|
144
|
+
icon: limited ? 'clock' : 'caution',
|
|
145
|
+
title: copy.title,
|
|
146
|
+
detail: copy.body,
|
|
147
|
+
nextStep: null,
|
|
148
|
+
retryable: copy.retryable,
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../shared/icons'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../shared/motion'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface UiNavStability {
|
|
2
|
+
icon: string
|
|
3
|
+
tooltip: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface UiNavSetup {
|
|
7
|
+
/**
|
|
8
|
+
* One short imperative word naming what the user owes this row — "Connect",
|
|
9
|
+
* "Confirm". Rendered verbatim in an outline chip, so anything longer than a
|
|
10
|
+
* single word is a layout bug; the full state goes in `tooltip`.
|
|
11
|
+
*/
|
|
12
|
+
verb: string
|
|
13
|
+
tooltip: string
|
|
14
|
+
tone?: 'primary' | 'warning'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UiNavLink {
|
|
18
|
+
label: string
|
|
19
|
+
to: string
|
|
20
|
+
icon?: string
|
|
21
|
+
disabled?: boolean
|
|
22
|
+
title?: string
|
|
23
|
+
badge?: string
|
|
24
|
+
badgeColor?: 'primary' | 'warning' | 'neutral'
|
|
25
|
+
setup?: UiNavSetup | null
|
|
26
|
+
stability?: UiNavStability | null
|
|
27
|
+
pending?: boolean
|
|
28
|
+
pendingTooltip?: string
|
|
29
|
+
/** Optional text-tone lift for a small subset of peer navigation items. */
|
|
30
|
+
textTone?: 'highlighted'
|
|
31
|
+
active?: (path: string) => boolean
|
|
32
|
+
/**
|
|
33
|
+
* Opt out of NuxtLink's idle route-chunk prefetch. Set `false` on links to
|
|
34
|
+
* explorer-sized routes (hundreds of KB) that sit in an always-visible nav —
|
|
35
|
+
* the sidebar would otherwise pull them on every page view.
|
|
36
|
+
*/
|
|
37
|
+
prefetch?: boolean
|
|
38
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { RouteLocationAsPathGeneric, RouteLocationAsRelative } from 'vue-router'
|
|
2
|
+
|
|
3
|
+
// Keep named routes and their params typed. URL paths stay open, like string URLs.
|
|
4
|
+
// Vue props cannot expand every generated path variant in a large RouteMap.
|
|
5
|
+
export type NavigationDestination = string | RouteLocationAsRelative | RouteLocationAsPathGeneric
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface UiStaticTerminalLine {
|
|
2
|
+
/** The command without the shell prompt. */
|
|
3
|
+
command: string
|
|
4
|
+
/** Show a shell prompt. Set false for source code or configuration. */
|
|
5
|
+
prompt?: boolean
|
|
6
|
+
/** A shell comment shown before the command. */
|
|
7
|
+
note?: string
|
|
8
|
+
/** Static output shown after the command. */
|
|
9
|
+
output?: string
|
|
10
|
+
/** Rangi language for the command. */
|
|
11
|
+
language?: string
|
|
12
|
+
/** Rangi language for the output. */
|
|
13
|
+
outputLanguage?: string
|
|
14
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ExportColumn } from '../utils/export-download'
|
|
2
|
+
|
|
3
|
+
/** The part of a TanStack row an export reads. */
|
|
4
|
+
export interface ExportableRow {
|
|
5
|
+
getValue: <TValue>(columnId: string) => TValue
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** The part of a TanStack table an export reads. */
|
|
9
|
+
export interface ExportableTable<R extends ExportableRow> {
|
|
10
|
+
getPrePaginatedRowModel: () => { rows: R[] }
|
|
11
|
+
getVisibleLeafColumns: () => ReadonlyArray<{
|
|
12
|
+
id: string
|
|
13
|
+
accessorFn?: unknown
|
|
14
|
+
columnDef: { header?: unknown }
|
|
15
|
+
}>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Every row of a client-side table after its filters and sort, before paging,
|
|
20
|
+
* with one export column per visible data column. A display column with no
|
|
21
|
+
* accessor, such as row actions, is left out. A column whose header is not a
|
|
22
|
+
* plain string exports under its column id.
|
|
23
|
+
*
|
|
24
|
+
* Pagination is the only thing an export skips: a user who exports page 1 of 4
|
|
25
|
+
* expects the whole table.
|
|
26
|
+
*/
|
|
27
|
+
export function uiTableExport<R extends ExportableRow>(table: ExportableTable<R>): { rows: R[], columns: ExportColumn<R>[] } {
|
|
28
|
+
const columns = table.getVisibleLeafColumns()
|
|
29
|
+
.filter(column => column.accessorFn !== undefined)
|
|
30
|
+
.map((column): ExportColumn<R> => ({
|
|
31
|
+
key: column.id,
|
|
32
|
+
header: typeof column.columnDef.header === 'string' && column.columnDef.header !== '' ? column.columnDef.header : column.id,
|
|
33
|
+
value: row => row.getValue(column.id),
|
|
34
|
+
}))
|
|
35
|
+
return { rows: table.getPrePaginatedRowModel().rows, columns }
|
|
36
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ColumnDef } from '@tanstack/table-core'
|
|
2
|
+
import {
|
|
3
|
+
columnFilteringFeature,
|
|
4
|
+
columnOrderingFeature,
|
|
5
|
+
columnVisibilityFeature,
|
|
6
|
+
createExpandedRowModel,
|
|
7
|
+
createFilteredRowModel,
|
|
8
|
+
createPaginatedRowModel,
|
|
9
|
+
createSortedRowModel,
|
|
10
|
+
rowExpandingFeature,
|
|
11
|
+
rowPaginationFeature,
|
|
12
|
+
rowSelectionFeature,
|
|
13
|
+
rowSortingFeature,
|
|
14
|
+
sortFn_alphanumeric,
|
|
15
|
+
sortFn_datetime,
|
|
16
|
+
sortFn_text,
|
|
17
|
+
tableFeatures,
|
|
18
|
+
} from '@tanstack/table-core'
|
|
19
|
+
|
|
20
|
+
export type UiTableSize = 'xs' | 'sm' | 'md'
|
|
21
|
+
export type UiTableAlign = 'left' | 'center' | 'right'
|
|
22
|
+
|
|
23
|
+
export const uiTableCellSizeClass: Record<UiTableSize, string> = {
|
|
24
|
+
xs: 'h-11 py-1 sm:h-8',
|
|
25
|
+
sm: 'h-11 py-1 sm:h-10',
|
|
26
|
+
md: 'h-11 py-2 sm:h-10',
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const uiTableSkeletonSizeClass: Record<UiTableSize, string> = {
|
|
30
|
+
xs: 'h-4',
|
|
31
|
+
sm: 'h-4',
|
|
32
|
+
md: 'h-6',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type UiTableVisibleFrom = 'sm' | 'md' | 'lg' | 'xl' | '2xl'
|
|
36
|
+
|
|
37
|
+
export interface UiTableCellProps {
|
|
38
|
+
align?: UiTableAlign
|
|
39
|
+
numeric?: boolean
|
|
40
|
+
visibleFrom?: UiTableVisibleFrom
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface UiTableColumnMeta extends UiTableCellProps {
|
|
44
|
+
noPadding?: boolean
|
|
45
|
+
stableData?: boolean
|
|
46
|
+
tooltip?: string
|
|
47
|
+
headClass?: string
|
|
48
|
+
cellClass?: string
|
|
49
|
+
rowHeader?: boolean
|
|
50
|
+
ui?: { td?: { base?: string } }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const uiTableFeatures = tableFeatures({
|
|
54
|
+
columnFilteringFeature,
|
|
55
|
+
filteredRowModel: createFilteredRowModel(),
|
|
56
|
+
columnOrderingFeature,
|
|
57
|
+
columnVisibilityFeature,
|
|
58
|
+
rowExpandingFeature,
|
|
59
|
+
expandedRowModel: createExpandedRowModel(),
|
|
60
|
+
rowPaginationFeature,
|
|
61
|
+
paginatedRowModel: createPaginatedRowModel(),
|
|
62
|
+
rowSelectionFeature,
|
|
63
|
+
rowSortingFeature,
|
|
64
|
+
sortedRowModel: createSortedRowModel(),
|
|
65
|
+
// Auto sorting picks one of these names for a string or Date column. Left
|
|
66
|
+
// unregistered, it falls back to a plain `<` compare, so "Page2" sorts
|
|
67
|
+
// before "page1" and "page10" before "page2".
|
|
68
|
+
sortFns: {
|
|
69
|
+
alphanumeric: sortFn_alphanumeric,
|
|
70
|
+
datetime: sortFn_datetime,
|
|
71
|
+
text: sortFn_text,
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
export type UiTableFeatures = typeof uiTableFeatures
|
|
76
|
+
export type UiTableColumn<T extends object> = ColumnDef<UiTableFeatures, T, unknown> & UiTableColumnMeta
|
|
77
|
+
export type UiTableRowId<T> = string | ((row: T) => string)
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A row's id: the `rowId` key or function, else the row's own `id`. A row with
|
|
81
|
+
* no usable id falls back to its index. Without that fallback every such row
|
|
82
|
+
* shared the id "", so selection, expansion and keys collided across rows. The
|
|
83
|
+
* fallback is prefixed: a bare index collided with a real id such as "1".
|
|
84
|
+
*/
|
|
85
|
+
export function resolveUiTableRowId<T extends object>(row: T, index: number, rowId?: UiTableRowId<T>): string {
|
|
86
|
+
const record = row as Record<string, unknown>
|
|
87
|
+
const id = rowId === undefined
|
|
88
|
+
? record.id
|
|
89
|
+
: typeof rowId === 'function'
|
|
90
|
+
? rowId(row)
|
|
91
|
+
: record[rowId]
|
|
92
|
+
if (typeof id === 'string' && id !== '')
|
|
93
|
+
return id
|
|
94
|
+
if (typeof id === 'number')
|
|
95
|
+
return String(id)
|
|
96
|
+
return `__index:${index}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const uiTableVisibleFromClass: Record<UiTableVisibleFrom, string> = {
|
|
100
|
+
'sm': 'hidden sm:table-cell',
|
|
101
|
+
'md': 'hidden md:table-cell',
|
|
102
|
+
'lg': 'hidden lg:table-cell',
|
|
103
|
+
'xl': 'hidden xl:table-cell',
|
|
104
|
+
'2xl': 'hidden 2xl:table-cell',
|
|
105
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '../../shared/ui-icons'
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an async data region shows: one of four states, decided in one place.
|
|
3
|
+
*
|
|
4
|
+
* A region used to take `loading` and `items` and render its empty copy for
|
|
5
|
+
* anything else. A failed read left `items` empty, so the Search Console
|
|
6
|
+
* Overview told a user "No queries in this window" while the server returned a
|
|
7
|
+
* 503. A read that had not started yet did the same. The empty copy is a claim
|
|
8
|
+
* about the user's data, and only a successful answer can make it.
|
|
9
|
+
*
|
|
10
|
+
* So a region takes this union instead. `empty` exists only when a read
|
|
11
|
+
* succeeded and held nothing, and a template switches on `_tag`, so no branch
|
|
12
|
+
* can show the empty copy for a failure or for a read still in flight.
|
|
13
|
+
*/
|
|
14
|
+
export type ViewState<T>
|
|
15
|
+
= | { readonly _tag: 'loading' }
|
|
16
|
+
| { readonly _tag: 'error', readonly error: unknown }
|
|
17
|
+
| { readonly _tag: 'empty' }
|
|
18
|
+
| {
|
|
19
|
+
readonly _tag: 'data'
|
|
20
|
+
readonly data: T
|
|
21
|
+
/**
|
|
22
|
+
* A later read failed while an earlier answer is still on screen. The
|
|
23
|
+
* region keeps the rows and says the refresh failed, so a failed refresh
|
|
24
|
+
* never throws away evidence and never passes for fresh data.
|
|
25
|
+
*/
|
|
26
|
+
readonly refreshError: unknown
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ViewStateTag = ViewState<unknown>['_tag']
|
|
30
|
+
|
|
31
|
+
export interface ViewStateInput<T> {
|
|
32
|
+
/** A read is in flight. */
|
|
33
|
+
readonly pending: boolean
|
|
34
|
+
/** The last read's failure. Any falsy value means it did not fail. */
|
|
35
|
+
readonly error: unknown
|
|
36
|
+
/**
|
|
37
|
+
* The last successful answer. `null` or `undefined` means no read has
|
|
38
|
+
* succeeded yet, which covers a read that has not started: on the server, or
|
|
39
|
+
* before its inputs resolve.
|
|
40
|
+
*/
|
|
41
|
+
readonly data: T | null | undefined
|
|
42
|
+
/** Whether a successful answer holds nothing to show. */
|
|
43
|
+
readonly isEmpty: (data: T) => boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const LOADING: ViewState<never> = { _tag: 'loading' }
|
|
47
|
+
const EMPTY: ViewState<never> = { _tag: 'empty' }
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Decide the state of a region. Pure.
|
|
51
|
+
*
|
|
52
|
+
* The order is the contract:
|
|
53
|
+
* 1. A read in flight is `loading`.
|
|
54
|
+
* 2. A failure with nothing to show is `error`, never `empty`.
|
|
55
|
+
* 3. No answer yet is `loading`, never `empty`.
|
|
56
|
+
* 4. A successful answer with nothing in it is `empty`.
|
|
57
|
+
* 5. Anything else is `data`, with the failure of a later read, if any.
|
|
58
|
+
*/
|
|
59
|
+
export function viewState<T>(input: ViewStateInput<T>): ViewState<T> {
|
|
60
|
+
if (input.pending)
|
|
61
|
+
return LOADING
|
|
62
|
+
const failed = Boolean(input.error)
|
|
63
|
+
const answer = input.data
|
|
64
|
+
if (answer === null || answer === undefined)
|
|
65
|
+
return failed ? { _tag: 'error', error: input.error } : LOADING
|
|
66
|
+
if (input.isEmpty(answer))
|
|
67
|
+
return failed ? { _tag: 'error', error: input.error } : EMPTY
|
|
68
|
+
return { _tag: 'data', data: answer, refreshError: failed ? input.error : null }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** `viewState` for a list: an answer with no rows is `empty`. */
|
|
72
|
+
export function listViewState<L extends readonly unknown[]>(input: Omit<ViewStateInput<L>, 'isEmpty'>): ViewState<L> {
|
|
73
|
+
return viewState<L>({ ...input, isEmpty: rows => rows.length === 0 })
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Reshape the answer of a `data` state and keep every other state. A list that
|
|
78
|
+
* maps its rows for display, such as table rows built from query rows, keeps
|
|
79
|
+
* the state its read decided.
|
|
80
|
+
*/
|
|
81
|
+
export function mapViewState<A, B>(state: ViewState<A>, map: (data: A) => B): ViewState<B> {
|
|
82
|
+
return state._tag === 'data'
|
|
83
|
+
? { _tag: 'data', data: map(state.data), refreshError: state.refreshError }
|
|
84
|
+
: state
|
|
85
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why an async region failed, in the only terms the UI is allowed to state.
|
|
3
|
+
*
|
|
4
|
+
* The tag is derived from an HTTP status code, which is a number. It is never
|
|
5
|
+
* derived from an error's `message`. `message` on a failed fetch is developer
|
|
6
|
+
* text that quotes the request: ofetch renders `[GET] "/api/pro/sites/42/…":
|
|
7
|
+
* 500 Internal Server Error`. Rendering that puts an internal route, a site id,
|
|
8
|
+
* and any query string on a customer's screen. Credentials have leaked this way
|
|
9
|
+
* before, so the widget layer holds no path from a wire string to the DOM.
|
|
10
|
+
*
|
|
11
|
+
* `unknown` covers every input this cannot place: a network drop, an abort, a
|
|
12
|
+
* response-schema mismatch, a plain `Error`, `null`. Those carry no status.
|
|
13
|
+
*/
|
|
14
|
+
export type WidgetFailure
|
|
15
|
+
= | { readonly _tag: 'signed-out' }
|
|
16
|
+
| { readonly _tag: 'no-access' }
|
|
17
|
+
| { readonly _tag: 'missing' }
|
|
18
|
+
| { readonly _tag: 'rate-limited' }
|
|
19
|
+
| { readonly _tag: 'rejected' }
|
|
20
|
+
| { readonly _tag: 'server' }
|
|
21
|
+
| { readonly _tag: 'unknown' }
|
|
22
|
+
|
|
23
|
+
export type WidgetFailureTag = WidgetFailure['_tag']
|
|
24
|
+
|
|
25
|
+
export interface WidgetFailureCopy {
|
|
26
|
+
/** What happened. */
|
|
27
|
+
readonly title: string
|
|
28
|
+
/** What to do next. */
|
|
29
|
+
readonly body: string
|
|
30
|
+
/**
|
|
31
|
+
* Whether retrying the same read can succeed. A retry button on a 401 or a
|
|
32
|
+
* 403 is advice that cannot work, so those branches hide it.
|
|
33
|
+
*/
|
|
34
|
+
readonly retryable: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The copy for every failure the UI can show.
|
|
39
|
+
*
|
|
40
|
+
* `satisfies Record<WidgetFailureTag, WidgetFailureCopy>` is the guard: a new
|
|
41
|
+
* tag fails the typecheck here until someone writes its copy, so no failure can
|
|
42
|
+
* reach a customer without one.
|
|
43
|
+
*/
|
|
44
|
+
export const WIDGET_FAILURE_COPY = {
|
|
45
|
+
'signed-out': {
|
|
46
|
+
title: 'Your session ended',
|
|
47
|
+
body: 'Sign in again to load this data.',
|
|
48
|
+
retryable: false,
|
|
49
|
+
},
|
|
50
|
+
'no-access': {
|
|
51
|
+
title: 'Access denied',
|
|
52
|
+
body: 'Ask a teammate to grant you access.',
|
|
53
|
+
retryable: false,
|
|
54
|
+
},
|
|
55
|
+
'missing': {
|
|
56
|
+
title: 'Not found',
|
|
57
|
+
body: 'This data moved or was removed.',
|
|
58
|
+
retryable: false,
|
|
59
|
+
},
|
|
60
|
+
'rate-limited': {
|
|
61
|
+
title: 'Too many requests',
|
|
62
|
+
body: 'Wait a moment, then retry.',
|
|
63
|
+
retryable: true,
|
|
64
|
+
},
|
|
65
|
+
'rejected': {
|
|
66
|
+
title: 'The request was rejected',
|
|
67
|
+
body: 'Reload the page, then try again.',
|
|
68
|
+
retryable: true,
|
|
69
|
+
},
|
|
70
|
+
'server': {
|
|
71
|
+
title: 'The server failed',
|
|
72
|
+
body: 'We logged this failure. Retry in a moment.',
|
|
73
|
+
retryable: true,
|
|
74
|
+
},
|
|
75
|
+
'unknown': {
|
|
76
|
+
title: 'This data failed to load',
|
|
77
|
+
body: 'Check your connection, then retry.',
|
|
78
|
+
retryable: true,
|
|
79
|
+
},
|
|
80
|
+
} as const satisfies Record<WidgetFailureTag, WidgetFailureCopy>
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* HTTP status from any error shape.
|
|
84
|
+
*
|
|
85
|
+
* Twin of `queryErrorStatusCode` in `layers/core/app/utils/rpc.ts`. It is copied
|
|
86
|
+
* rather than imported because ADR-0042 lets `design-system` import `core` for
|
|
87
|
+
* types only. Both read the same four fields; keep them in step.
|
|
88
|
+
*/
|
|
89
|
+
function statusCodeOf(error: unknown): number | undefined {
|
|
90
|
+
if (!error || typeof error !== 'object')
|
|
91
|
+
return undefined
|
|
92
|
+
const e = error as {
|
|
93
|
+
statusCode?: unknown
|
|
94
|
+
status?: unknown
|
|
95
|
+
data?: { statusCode?: unknown }
|
|
96
|
+
response?: { status?: unknown }
|
|
97
|
+
}
|
|
98
|
+
const candidate = [e.statusCode, e.status, e.data?.statusCode, e.response?.status]
|
|
99
|
+
.find(value => typeof value === 'number')
|
|
100
|
+
return typeof candidate === 'number' ? candidate : undefined
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Parse an arbitrary caught value once, at the widget boundary, into the tag the
|
|
105
|
+
* template trusts. Everything inward of this function is safe to render.
|
|
106
|
+
*/
|
|
107
|
+
export function toWidgetFailure(error: unknown): WidgetFailure {
|
|
108
|
+
const status = statusCodeOf(error)
|
|
109
|
+
if (status === 401)
|
|
110
|
+
return { _tag: 'signed-out' }
|
|
111
|
+
if (status === 403)
|
|
112
|
+
return { _tag: 'no-access' }
|
|
113
|
+
if (status === 404)
|
|
114
|
+
return { _tag: 'missing' }
|
|
115
|
+
if (status === 429)
|
|
116
|
+
return { _tag: 'rate-limited' }
|
|
117
|
+
if (status !== undefined && status >= 400 && status < 500)
|
|
118
|
+
return { _tag: 'rejected' }
|
|
119
|
+
if (status !== undefined && status >= 500)
|
|
120
|
+
return { _tag: 'server' }
|
|
121
|
+
return { _tag: 'unknown' }
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function widgetFailureCopy(error: unknown): WidgetFailureCopy {
|
|
125
|
+
return WIDGET_FAILURE_COPY[toWidgetFailure(error)._tag]
|
|
126
|
+
}
|