@fragments-sdk/viewer 0.2.1 → 0.2.3
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/package.json +10 -3
- package/src/components/App.tsx +67 -4
- package/src/components/BottomPanel.tsx +31 -1
- package/src/components/ComponentGraph.tsx +1 -1
- package/src/components/EmptyVariantMessage.tsx +1 -1
- package/src/components/ErrorBoundary.tsx +2 -4
- package/src/components/FragmentRenderer.tsx +27 -4
- package/src/components/HeaderSearch.tsx +1 -1
- package/src/components/InteractionsPanel.tsx +5 -5
- package/src/components/LoadErrorMessage.tsx +26 -30
- package/src/components/NoVariantsMessage.tsx +1 -1
- package/src/components/PanelShell.tsx +1 -1
- package/src/components/PerformancePanel.tsx +4 -4
- package/src/components/PreviewArea.tsx +6 -0
- package/src/components/PropsEditor.tsx +33 -17
- package/src/components/SkeletonLoader.tsx +0 -1
- package/src/components/TokenStylePanel.tsx +3 -3
- package/src/components/TopToolbar.tsx +1 -1
- package/src/components/VariantMatrix.tsx +11 -1
- package/src/components/ViewerHeader.tsx +1 -1
- package/src/components/WebMCPDevTools.tsx +2 -2
- package/src/entry.tsx +4 -6
- package/src/hooks/useAppState.ts +1 -1
- package/src/preview-frame-entry.tsx +0 -2
- package/src/shared/DocsHeaderBar.module.scss +174 -0
- package/src/shared/DocsHeaderBar.tsx +149 -14
- package/src/shared/DocsSidebarNav.tsx +3 -3
- package/src/shared/index.ts +4 -1
- package/src/shared/types.ts +29 -3
- package/src/style-utils.ts +1 -1
- package/src/webmcp/runtime-tools.ts +1 -1
- package/tsconfig.json +2 -2
- package/src/assets/fragments-logo.ts +0 -4
- package/src/components/ActionsPanel.tsx +0 -332
- package/src/components/ComponentHeader.tsx +0 -88
- package/src/components/ContractPanel.tsx +0 -241
- package/src/components/FragmentEditor.tsx +0 -525
- package/src/components/HmrStatusIndicator.tsx +0 -61
- package/src/components/LandingPage.tsx +0 -420
- package/src/components/PropsTable.tsx +0 -111
- package/src/components/RelationsSection.tsx +0 -88
- package/src/components/ResizablePanel.tsx +0 -271
- package/src/components/RightSidebar.tsx +0 -102
- package/src/components/ScreenshotButton.tsx +0 -90
- package/src/components/Sidebar.tsx +0 -169
- package/src/components/UsageSection.tsx +0 -95
- package/src/components/VariantTabs.tsx +0 -40
- package/src/components/ViewportSelector.tsx +0 -172
- package/src/components/_future/CreatePage.tsx +0 -835
- package/src/composition-renderer.ts +0 -381
- package/src/constants/index.ts +0 -1
- package/src/hooks/index.ts +0 -2
- package/src/hooks/useHmrStatus.ts +0 -109
- package/src/hooks/useScrollSpy.ts +0 -78
- package/src/intelligence/healthReport.ts +0 -505
- package/src/intelligence/styleDrift.ts +0 -340
- package/src/intelligence/usageScanner.ts +0 -309
- package/src/utils/actionExport.ts +0 -372
- package/src/utils/colorSchemes.ts +0 -201
- package/src/webmcp/index.ts +0 -3
|
@@ -332,7 +332,7 @@ export function TokenStylePanel({
|
|
|
332
332
|
<Stack direction="row" align="center" gap="sm">
|
|
333
333
|
{tokenData && (
|
|
334
334
|
<Badge
|
|
335
|
-
variant={compliancePercent >= 80 ? 'success' : compliancePercent >= 50 ? 'warning' : '
|
|
335
|
+
variant={compliancePercent >= 80 ? 'success' : compliancePercent >= 50 ? 'warning' : 'error'}
|
|
336
336
|
>
|
|
337
337
|
{compliancePercent}% token compliance
|
|
338
338
|
</Badge>
|
|
@@ -343,7 +343,7 @@ export function TokenStylePanel({
|
|
|
343
343
|
</Badge>
|
|
344
344
|
|
|
345
345
|
{hardcodedCount > 0 && (
|
|
346
|
-
<Badge variant="
|
|
346
|
+
<Badge variant="error">
|
|
347
347
|
{hardcodedCount} hardcoded
|
|
348
348
|
</Badge>
|
|
349
349
|
)}
|
|
@@ -414,7 +414,7 @@ export function TokenStylePanel({
|
|
|
414
414
|
{prop.rendered}
|
|
415
415
|
</span>
|
|
416
416
|
{prop.isHardcoded && (
|
|
417
|
-
<Badge variant="
|
|
417
|
+
<Badge variant="error">HC</Badge>
|
|
418
418
|
)}
|
|
419
419
|
</div>
|
|
420
420
|
</td>
|
|
@@ -32,7 +32,7 @@ interface TopToolbarProps {
|
|
|
32
32
|
figmaUrl?: string;
|
|
33
33
|
searchQuery: string;
|
|
34
34
|
onSearchChange: (value: string) => void;
|
|
35
|
-
searchInputRef: RefObject<HTMLInputElement>;
|
|
35
|
+
searchInputRef: RefObject<HTMLInputElement | null>;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export function TopToolbar({
|
|
@@ -33,6 +33,10 @@ interface VariantMatrixProps {
|
|
|
33
33
|
previewTheme: "light" | "dark";
|
|
34
34
|
/** Whether to use iframe isolation */
|
|
35
35
|
useIframeIsolation?: boolean;
|
|
36
|
+
/** Variant index currently focused in the main viewer */
|
|
37
|
+
activeVariantIndex?: number;
|
|
38
|
+
/** Args overrides to apply to the focused variant only */
|
|
39
|
+
variantArgsOverrides?: Record<string, unknown>;
|
|
36
40
|
/** Callback when a variant is clicked to focus on it */
|
|
37
41
|
onSelectVariant?: (index: number) => void;
|
|
38
42
|
}
|
|
@@ -63,6 +67,8 @@ export function VariantMatrix({
|
|
|
63
67
|
zoom,
|
|
64
68
|
previewTheme,
|
|
65
69
|
useIframeIsolation = true,
|
|
70
|
+
activeVariantIndex,
|
|
71
|
+
variantArgsOverrides,
|
|
66
72
|
onSelectVariant,
|
|
67
73
|
}: VariantMatrixProps) {
|
|
68
74
|
const [gridSize, setGridSize] = useState<GridSize>("medium");
|
|
@@ -173,6 +179,7 @@ export function VariantMatrix({
|
|
|
173
179
|
isHovered={hoveredIndex === index}
|
|
174
180
|
onHover={() => setHoveredIndex(index)}
|
|
175
181
|
onLeave={() => setHoveredIndex(null)}
|
|
182
|
+
argsOverrides={index === activeVariantIndex ? variantArgsOverrides : undefined}
|
|
176
183
|
onClick={() => onSelectVariant?.(index)}
|
|
177
184
|
/>
|
|
178
185
|
);
|
|
@@ -204,6 +211,7 @@ export function VariantMatrix({
|
|
|
204
211
|
isHovered={hoveredIndex === index}
|
|
205
212
|
onHover={() => setHoveredIndex(index)}
|
|
206
213
|
onLeave={() => setHoveredIndex(null)}
|
|
214
|
+
argsOverrides={index === activeVariantIndex ? variantArgsOverrides : undefined}
|
|
207
215
|
onClick={() => onSelectVariant?.(index)}
|
|
208
216
|
/>
|
|
209
217
|
))}
|
|
@@ -226,6 +234,7 @@ interface VariantCardProps {
|
|
|
226
234
|
isHovered: boolean;
|
|
227
235
|
onHover: () => void;
|
|
228
236
|
onLeave: () => void;
|
|
237
|
+
argsOverrides?: Record<string, unknown>;
|
|
229
238
|
onClick: () => void;
|
|
230
239
|
}
|
|
231
240
|
|
|
@@ -241,6 +250,7 @@ function VariantCard({
|
|
|
241
250
|
isHovered,
|
|
242
251
|
onHover,
|
|
243
252
|
onLeave,
|
|
253
|
+
argsOverrides,
|
|
244
254
|
onClick,
|
|
245
255
|
}: VariantCardProps) {
|
|
246
256
|
return (
|
|
@@ -355,7 +365,7 @@ function VariantCard({
|
|
|
355
365
|
</Text>
|
|
356
366
|
}
|
|
357
367
|
>
|
|
358
|
-
<FragmentRenderer variant={variant}>
|
|
368
|
+
<FragmentRenderer variant={variant} argsOverrides={argsOverrides}>
|
|
359
369
|
{(content, isLoading, error) => {
|
|
360
370
|
if (isLoading) {
|
|
361
371
|
return (
|
|
@@ -17,7 +17,7 @@ interface ViewerHeaderProps {
|
|
|
17
17
|
showHealth: boolean;
|
|
18
18
|
searchQuery: string;
|
|
19
19
|
onSearchChange: (value: string) => void;
|
|
20
|
-
searchInputRef: RefObject<HTMLInputElement>;
|
|
20
|
+
searchInputRef: RefObject<HTMLInputElement | null>;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export function ViewerHeader({
|
|
@@ -151,7 +151,7 @@ export function WebMCPDevTools() {
|
|
|
151
151
|
<>
|
|
152
152
|
<Select
|
|
153
153
|
value={selectedTool}
|
|
154
|
-
onValueChange={setSelectedTool}
|
|
154
|
+
onValueChange={(value) => setSelectedTool(value ?? "")}
|
|
155
155
|
placeholder="Select a tool"
|
|
156
156
|
>
|
|
157
157
|
<Select.Trigger />
|
|
@@ -246,7 +246,7 @@ export function WebMCPDevTools() {
|
|
|
246
246
|
<Stack direction="column" gap="md" style={{ paddingTop: "12px" }}>
|
|
247
247
|
<Select
|
|
248
248
|
value={selectedTool}
|
|
249
|
-
onValueChange={setSelectedTool}
|
|
249
|
+
onValueChange={(value) => setSelectedTool(value ?? "")}
|
|
250
250
|
placeholder="Select a tool"
|
|
251
251
|
>
|
|
252
252
|
<Select.Trigger />
|
package/src/entry.tsx
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createRoot, type Root } from "react-dom/client";
|
|
2
2
|
import { Component, type ReactNode, type ErrorInfo } from "react";
|
|
3
|
+
import type { FragmentDefinition, FragmentsConfig } from "@fragments-sdk/core";
|
|
3
4
|
import { App } from "./components/App.js";
|
|
4
5
|
import { ThemeProvider } from "./components/ThemeProvider.js";
|
|
5
6
|
import { ToastProvider } from "./components/Toast.js";
|
|
@@ -134,9 +135,9 @@ declare global {
|
|
|
134
135
|
interface Window {
|
|
135
136
|
__FRAGMENTS__?: Array<{
|
|
136
137
|
path: string;
|
|
137
|
-
fragment:
|
|
138
|
+
fragment: FragmentDefinition;
|
|
138
139
|
}>;
|
|
139
|
-
__FRAGMENTS_CONFIG__?:
|
|
140
|
+
__FRAGMENTS_CONFIG__?: FragmentsConfig;
|
|
140
141
|
__FRAGMENTS_PACKAGE_NAME__?: string | null;
|
|
141
142
|
__FRAGMENTS_ERROR__?: string;
|
|
142
143
|
}
|
|
@@ -144,7 +145,7 @@ declare global {
|
|
|
144
145
|
|
|
145
146
|
type FragmentItem = {
|
|
146
147
|
path: string;
|
|
147
|
-
fragment:
|
|
148
|
+
fragment: FragmentDefinition;
|
|
148
149
|
};
|
|
149
150
|
|
|
150
151
|
// Initialize fragments from window or empty array
|
|
@@ -309,13 +310,10 @@ if (rootElement) {
|
|
|
309
310
|
}
|
|
310
311
|
|
|
311
312
|
// HMR support for entry file and a11y invalidation
|
|
312
|
-
// @ts-expect-error Vite HMR types
|
|
313
313
|
if (import.meta.hot) {
|
|
314
|
-
// @ts-expect-error Vite HMR types
|
|
315
314
|
import.meta.hot.accept();
|
|
316
315
|
|
|
317
316
|
// Subscribe to a11y invalidation events from Vite plugin
|
|
318
|
-
// @ts-expect-error Vite HMR types
|
|
319
317
|
import.meta.hot.on("fragments:a11y-invalidate", (data: {
|
|
320
318
|
components: string[];
|
|
321
319
|
file: string;
|
package/src/hooks/useAppState.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { useReducer, useCallback, useMemo } from 'react';
|
|
7
7
|
|
|
8
|
-
export type ActivePanel = 'styles' | 'accessibility' | 'interactions' | 'graph' | 'performance';
|
|
8
|
+
export type ActivePanel = 'styles' | 'controls' | 'accessibility' | 'interactions' | 'graph' | 'performance';
|
|
9
9
|
|
|
10
10
|
interface AppUIState {
|
|
11
11
|
activePanel: ActivePanel;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// Mega-Menu Grid
|
|
3
|
+
// ============================================
|
|
4
|
+
|
|
5
|
+
.megaMenuGrid {
|
|
6
|
+
display: grid;
|
|
7
|
+
gap: 0; // divider handles visual separation
|
|
8
|
+
padding: var(--fui-space-5) var(--fui-space-5);
|
|
9
|
+
min-width: 540px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// ============================================
|
|
13
|
+
// Sections & Divider
|
|
14
|
+
// ============================================
|
|
15
|
+
|
|
16
|
+
.megaMenuSection {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column;
|
|
19
|
+
padding: 0 var(--fui-space-2);
|
|
20
|
+
|
|
21
|
+
&:first-child {
|
|
22
|
+
padding-left: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
&:last-child {
|
|
26
|
+
padding-right: 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.megaMenuDivider {
|
|
31
|
+
width: 1px;
|
|
32
|
+
background-color: var(--fui-border-default);
|
|
33
|
+
align-self: stretch;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.megaMenuSectionTitle {
|
|
37
|
+
font-size: 0.714rem; // ~10px — smaller than xs for strong hierarchy
|
|
38
|
+
font-weight: var(--fui-font-weight-semibold);
|
|
39
|
+
color: var(--fui-text-tertiary);
|
|
40
|
+
text-transform: uppercase;
|
|
41
|
+
letter-spacing: 0.08em;
|
|
42
|
+
padding: var(--fui-space-0-5) var(--fui-space-2) var(--fui-space-2);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ============================================
|
|
46
|
+
// Menu Items (overrides NavigationMenu .link)
|
|
47
|
+
// Specificity: .megaMenuSection .megaMenuItem = 0,2,0
|
|
48
|
+
// NavigationMenu .link = 0,1,0 — we always win
|
|
49
|
+
// ============================================
|
|
50
|
+
|
|
51
|
+
.megaMenuSection .megaMenuItem {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
gap: var(--fui-space-3);
|
|
55
|
+
padding: var(--fui-space-1) var(--fui-space-2);
|
|
56
|
+
border-radius: var(--fui-radius-md);
|
|
57
|
+
text-decoration: none;
|
|
58
|
+
white-space: normal;
|
|
59
|
+
min-height: auto;
|
|
60
|
+
color: inherit;
|
|
61
|
+
transition: background-color 150ms ease, color 150ms ease;
|
|
62
|
+
|
|
63
|
+
&:hover {
|
|
64
|
+
background-color: var(--fui-bg-hover);
|
|
65
|
+
|
|
66
|
+
.megaMenuItemDesc {
|
|
67
|
+
color: var(--fui-text-secondary);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.packageLogo {
|
|
71
|
+
transform: translateY(-1px);
|
|
72
|
+
box-shadow:
|
|
73
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
|
74
|
+
0 3px 8px -2px rgba(0, 0, 0, 0.4),
|
|
75
|
+
0 0 0 1px rgba(255, 255, 255, 0.08);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
&:focus-visible {
|
|
80
|
+
outline: 2px solid var(--fui-color-accent, #6366f1);
|
|
81
|
+
outline-offset: -2px;
|
|
82
|
+
border-radius: var(--fui-radius-md);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@media (prefers-reduced-motion: reduce) {
|
|
86
|
+
transition: none;
|
|
87
|
+
|
|
88
|
+
&:hover .packageLogo {
|
|
89
|
+
transform: none;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Active state (current page) — override NavigationMenu .linkActive too
|
|
95
|
+
.megaMenuSection .megaMenuItemActive {
|
|
96
|
+
background-color: var(--fui-bg-secondary);
|
|
97
|
+
|
|
98
|
+
&:hover {
|
|
99
|
+
background-color: var(--fui-bg-secondary);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================
|
|
104
|
+
// Item Internals
|
|
105
|
+
// ============================================
|
|
106
|
+
|
|
107
|
+
.megaMenuItemBody {
|
|
108
|
+
display: flex;
|
|
109
|
+
flex-direction: column;
|
|
110
|
+
gap: 2px; // tight coupling of title + description
|
|
111
|
+
min-width: 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.megaMenuItemTitle {
|
|
115
|
+
font-size: var(--fui-font-size-sm);
|
|
116
|
+
font-weight: var(--fui-font-weight-medium);
|
|
117
|
+
color: var(--fui-text-primary);
|
|
118
|
+
line-height: 1.3;
|
|
119
|
+
display: flex;
|
|
120
|
+
align-items: center;
|
|
121
|
+
gap: var(--fui-space-1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.megaMenuItemDesc {
|
|
125
|
+
font-size: var(--fui-font-size-xs);
|
|
126
|
+
color: var(--fui-text-tertiary);
|
|
127
|
+
line-height: 1.4;
|
|
128
|
+
transition: color 150ms ease;
|
|
129
|
+
|
|
130
|
+
@media (prefers-reduced-motion: reduce) {
|
|
131
|
+
transition: none;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ============================================
|
|
136
|
+
// Badge — tiny subordinate pill
|
|
137
|
+
// ============================================
|
|
138
|
+
|
|
139
|
+
.megaMenuBadge {
|
|
140
|
+
display: inline-flex;
|
|
141
|
+
align-items: center;
|
|
142
|
+
font-size: 9px;
|
|
143
|
+
font-weight: var(--fui-font-weight-semibold);
|
|
144
|
+
color: var(--fui-text-tertiary);
|
|
145
|
+
text-transform: uppercase;
|
|
146
|
+
letter-spacing: 0.04em;
|
|
147
|
+
line-height: 1;
|
|
148
|
+
padding: 2px 5px;
|
|
149
|
+
border-radius: var(--fui-radius-full);
|
|
150
|
+
border: 1px solid var(--fui-border-default);
|
|
151
|
+
white-space: nowrap;
|
|
152
|
+
flex-shrink: 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============================================
|
|
156
|
+
// Package Logo (wave gradient + abbreviation)
|
|
157
|
+
// ============================================
|
|
158
|
+
|
|
159
|
+
.packageLogo {
|
|
160
|
+
flex-shrink: 0;
|
|
161
|
+
width: 36px;
|
|
162
|
+
height: 36px;
|
|
163
|
+
border-radius: var(--fui-radius-md);
|
|
164
|
+
overflow: hidden;
|
|
165
|
+
box-shadow:
|
|
166
|
+
inset 0 1px 0 rgba(255, 255, 255, 0.15),
|
|
167
|
+
0 2px 6px -1px rgba(0, 0, 0, 0.3),
|
|
168
|
+
0 0 0 1px rgba(255, 255, 255, 0.06);
|
|
169
|
+
transition: box-shadow 150ms ease, transform 150ms ease;
|
|
170
|
+
|
|
171
|
+
@media (prefers-reduced-motion: reduce) {
|
|
172
|
+
transition: none;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Header, NavigationMenu } from '@fragments-sdk/ui';
|
|
4
|
-
import type
|
|
4
|
+
import { Fragment, useId, type ReactNode } from 'react';
|
|
5
5
|
import { DocsSearchCommand } from './DocsSearchCommand';
|
|
6
6
|
import type { DocsNavLinkRenderer, HeaderNavEntry, NavSection, SearchItem } from './types';
|
|
7
|
-
import { isDropdown } from './types';
|
|
7
|
+
import { isDropdown, isMegaMenu } from './types';
|
|
8
|
+
import megaStyles from './DocsHeaderBar.module.scss';
|
|
8
9
|
|
|
9
10
|
interface DocsHeaderBarProps {
|
|
10
11
|
brand: ReactNode;
|
|
@@ -20,9 +21,9 @@ interface DocsHeaderBarProps {
|
|
|
20
21
|
navAriaLabel?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick }) => (
|
|
24
|
-
<a href={href} onClick={onClick}>
|
|
25
|
-
{label}
|
|
24
|
+
const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick, className, children }) => (
|
|
25
|
+
<a href={href} onClick={onClick} className={className}>
|
|
26
|
+
{children ?? label}
|
|
26
27
|
</a>
|
|
27
28
|
);
|
|
28
29
|
|
|
@@ -30,6 +31,54 @@ function defaultIsActive(href: string, currentPath: string): boolean {
|
|
|
30
31
|
return currentPath === href || currentPath.startsWith(`${href}/`);
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
function PackageLogo({ abbr, gradient }: { abbr: string; gradient: [string, string] }) {
|
|
35
|
+
const gradientId = `pkg-grad-${abbr}-${useId().replace(/[:]/g, '')}`;
|
|
36
|
+
return (
|
|
37
|
+
<svg
|
|
38
|
+
className={megaStyles.packageLogo}
|
|
39
|
+
viewBox="0 0 36 36"
|
|
40
|
+
width={36}
|
|
41
|
+
height={36}
|
|
42
|
+
aria-hidden
|
|
43
|
+
>
|
|
44
|
+
<defs>
|
|
45
|
+
<linearGradient id={gradientId} x1="0" y1="0" x2="1" y2="1">
|
|
46
|
+
<stop offset="0%" stopColor={gradient[0]} />
|
|
47
|
+
<stop offset="100%" stopColor={gradient[1]} />
|
|
48
|
+
</linearGradient>
|
|
49
|
+
</defs>
|
|
50
|
+
<rect width="36" height="36" rx="2" fill={`url(#${gradientId})`} />
|
|
51
|
+
<path
|
|
52
|
+
d="M0 25 Q9 18 18 25 Q27 32 36 25 L36 36 L0 36 Z"
|
|
53
|
+
fill="white"
|
|
54
|
+
fillOpacity={0.15}
|
|
55
|
+
/>
|
|
56
|
+
<path
|
|
57
|
+
d="M0 30 Q9 23 18 30 Q27 37 36 30 L36 36 L0 36 Z"
|
|
58
|
+
fill="white"
|
|
59
|
+
fillOpacity={0.1}
|
|
60
|
+
/>
|
|
61
|
+
<text
|
|
62
|
+
x="18"
|
|
63
|
+
y="19"
|
|
64
|
+
textAnchor="middle"
|
|
65
|
+
dominantBaseline="central"
|
|
66
|
+
fill="white"
|
|
67
|
+
fontSize="12"
|
|
68
|
+
fontWeight="700"
|
|
69
|
+
fontFamily="'Geist Mono', 'SF Mono', SFMono-Regular, ui-monospace, monospace"
|
|
70
|
+
letterSpacing="-0.02em"
|
|
71
|
+
>
|
|
72
|
+
{abbr}
|
|
73
|
+
</text>
|
|
74
|
+
</svg>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function MegaMenuBadge({ text }: { text: string }) {
|
|
79
|
+
return <span className={megaStyles.megaMenuBadge}>{text}</span>;
|
|
80
|
+
}
|
|
81
|
+
|
|
33
82
|
export function DocsHeaderBar({
|
|
34
83
|
brand,
|
|
35
84
|
headerNav,
|
|
@@ -51,7 +100,78 @@ export function DocsHeaderBar({
|
|
|
51
100
|
<NavigationMenu aria-label={navAriaLabel}>
|
|
52
101
|
<NavigationMenu.List>
|
|
53
102
|
{headerNav.map((entry) =>
|
|
54
|
-
|
|
103
|
+
isMegaMenu(entry) ? (
|
|
104
|
+
<NavigationMenu.Item key={entry.label} value={entry.label}>
|
|
105
|
+
<NavigationMenu.Trigger>{entry.label}</NavigationMenu.Trigger>
|
|
106
|
+
<NavigationMenu.Content>
|
|
107
|
+
<div
|
|
108
|
+
className={megaStyles.megaMenuGrid}
|
|
109
|
+
data-columns={entry.sections.length}
|
|
110
|
+
style={{
|
|
111
|
+
gridTemplateColumns: entry.sections
|
|
112
|
+
.map((_, i) => {
|
|
113
|
+
const parts: string[] = [];
|
|
114
|
+
if (i > 0) parts.push('1px'); // divider column
|
|
115
|
+
parts.push('1fr');
|
|
116
|
+
return parts.join(' ');
|
|
117
|
+
})
|
|
118
|
+
.join(' '),
|
|
119
|
+
}}
|
|
120
|
+
>
|
|
121
|
+
{entry.sections.map((section, sectionIdx) => (
|
|
122
|
+
<Fragment key={section.title}>
|
|
123
|
+
{sectionIdx > 0 && <div className={megaStyles.megaMenuDivider} />}
|
|
124
|
+
<div className={megaStyles.megaMenuSection}>
|
|
125
|
+
<div className={megaStyles.megaMenuSectionTitle}>{section.title}</div>
|
|
126
|
+
{section.items.map((item) => {
|
|
127
|
+
const active = isActive(item.href, currentPath);
|
|
128
|
+
const className = [
|
|
129
|
+
megaStyles.megaMenuItem,
|
|
130
|
+
active ? megaStyles.megaMenuItemActive : '',
|
|
131
|
+
]
|
|
132
|
+
.filter(Boolean)
|
|
133
|
+
.join(' ');
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<NavigationMenu.Link
|
|
137
|
+
key={item.href}
|
|
138
|
+
href={item.href}
|
|
139
|
+
active={active}
|
|
140
|
+
asChild
|
|
141
|
+
>
|
|
142
|
+
{renderLink({
|
|
143
|
+
href: item.href,
|
|
144
|
+
label: item.label,
|
|
145
|
+
className,
|
|
146
|
+
children: (
|
|
147
|
+
<>
|
|
148
|
+
{item.abbr && item.gradient ? (
|
|
149
|
+
<PackageLogo abbr={item.abbr} gradient={item.gradient} />
|
|
150
|
+
) : null}
|
|
151
|
+
<span className={megaStyles.megaMenuItemBody}>
|
|
152
|
+
<span className={megaStyles.megaMenuItemTitle}>
|
|
153
|
+
{item.label}
|
|
154
|
+
{item.badge ? <MegaMenuBadge text={item.badge} /> : null}
|
|
155
|
+
</span>
|
|
156
|
+
{item.description ? (
|
|
157
|
+
<span className={megaStyles.megaMenuItemDesc}>
|
|
158
|
+
{item.description}
|
|
159
|
+
</span>
|
|
160
|
+
) : null}
|
|
161
|
+
</span>
|
|
162
|
+
</>
|
|
163
|
+
),
|
|
164
|
+
})}
|
|
165
|
+
</NavigationMenu.Link>
|
|
166
|
+
);
|
|
167
|
+
})}
|
|
168
|
+
</div>
|
|
169
|
+
</Fragment>
|
|
170
|
+
))}
|
|
171
|
+
</div>
|
|
172
|
+
</NavigationMenu.Content>
|
|
173
|
+
</NavigationMenu.Item>
|
|
174
|
+
) : isDropdown(entry) ? (
|
|
55
175
|
<NavigationMenu.Item key={entry.label} value={entry.label}>
|
|
56
176
|
<NavigationMenu.Trigger>{entry.label}</NavigationMenu.Trigger>
|
|
57
177
|
<NavigationMenu.Content>
|
|
@@ -88,23 +208,38 @@ export function DocsHeaderBar({
|
|
|
88
208
|
<NavigationMenu.MobileBrand>{brand}</NavigationMenu.MobileBrand>
|
|
89
209
|
|
|
90
210
|
<NavigationMenu.MobileContent>
|
|
91
|
-
{/*
|
|
211
|
+
{/* Top-level nav items (skip mega-menus — they get their own sections below) */}
|
|
92
212
|
<NavigationMenu.MobileSection>
|
|
93
|
-
{headerNav.
|
|
94
|
-
|
|
95
|
-
|
|
213
|
+
{headerNav.flatMap((entry) => {
|
|
214
|
+
if (isMegaMenu(entry)) return [];
|
|
215
|
+
if (isDropdown(entry)) {
|
|
216
|
+
return entry.items.map((child) => (
|
|
96
217
|
<NavigationMenu.Link key={child.href} href={child.href} asChild>
|
|
97
218
|
{renderLink({ href: child.href, label: child.label })}
|
|
98
219
|
</NavigationMenu.Link>
|
|
99
|
-
))
|
|
100
|
-
|
|
220
|
+
));
|
|
221
|
+
}
|
|
222
|
+
return [(
|
|
101
223
|
<NavigationMenu.Link key={entry.href} href={entry.href} asChild>
|
|
102
224
|
{renderLink({ href: entry.href, label: entry.label })}
|
|
103
225
|
</NavigationMenu.Link>
|
|
104
|
-
)
|
|
105
|
-
)}
|
|
226
|
+
)];
|
|
227
|
+
})}
|
|
106
228
|
</NavigationMenu.MobileSection>
|
|
107
229
|
|
|
230
|
+
{/* Mega-menu sections with labels */}
|
|
231
|
+
{headerNav.filter(isMegaMenu).flatMap((entry) =>
|
|
232
|
+
entry.sections.map((section) => (
|
|
233
|
+
<NavigationMenu.MobileSection key={section.title} label={section.title}>
|
|
234
|
+
{section.items.map((item) => (
|
|
235
|
+
<NavigationMenu.Link key={item.href} href={item.href} asChild>
|
|
236
|
+
{renderLink({ href: item.href, label: item.label })}
|
|
237
|
+
</NavigationMenu.Link>
|
|
238
|
+
))}
|
|
239
|
+
</NavigationMenu.MobileSection>
|
|
240
|
+
))
|
|
241
|
+
)}
|
|
242
|
+
|
|
108
243
|
{mobileSections.map((section) => (
|
|
109
244
|
<NavigationMenu.MobileSection key={section.title} label={section.title}>
|
|
110
245
|
{section.items.map((item) => (
|
|
@@ -18,9 +18,9 @@ function defaultIsActive(href: string, currentPath: string): boolean {
|
|
|
18
18
|
return currentPath === href;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick }) => (
|
|
22
|
-
<a href={href} onClick={onClick}>
|
|
23
|
-
{label}
|
|
21
|
+
const defaultLinkRenderer: DocsNavLinkRenderer = ({ href, label, onClick, className, children }) => (
|
|
22
|
+
<a href={href} onClick={onClick} className={className}>
|
|
23
|
+
{children ?? label}
|
|
24
24
|
</a>
|
|
25
25
|
);
|
|
26
26
|
|
package/src/shared/index.ts
CHANGED
|
@@ -22,8 +22,11 @@ export type {
|
|
|
22
22
|
HeaderNavEntry,
|
|
23
23
|
HeaderNavLink,
|
|
24
24
|
HeaderNavDropdown,
|
|
25
|
+
MegaMenuItem,
|
|
26
|
+
MegaMenuSection,
|
|
27
|
+
HeaderNavMegaMenu,
|
|
25
28
|
} from './types';
|
|
26
|
-
export { isDropdown } from './types';
|
|
29
|
+
export { isDropdown, isMegaMenu } from './types';
|
|
27
30
|
|
|
28
31
|
export { VariantPreviewCard } from './VariantPreviewCard';
|
|
29
32
|
export type { VariantPreviewCardProps } from './VariantPreviewCard';
|
package/src/shared/types.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactElement } from 'react';
|
|
1
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
export interface NavItem {
|
|
4
4
|
label: string;
|
|
@@ -20,6 +20,8 @@ export interface DocsNavLinkRenderProps {
|
|
|
20
20
|
href: string;
|
|
21
21
|
label: string;
|
|
22
22
|
onClick?: () => void;
|
|
23
|
+
className?: string;
|
|
24
|
+
children?: ReactNode;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export type DocsNavLinkRenderer = (props: DocsNavLinkRenderProps) => ReactElement;
|
|
@@ -34,10 +36,34 @@ export interface HeaderNavDropdown {
|
|
|
34
36
|
items: NavItem[];
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
export
|
|
39
|
+
export interface MegaMenuItem extends NavItem {
|
|
40
|
+
description?: string;
|
|
41
|
+
/** Two-letter abbreviation for the package logo (e.g. "CL", "MC") */
|
|
42
|
+
abbr?: string;
|
|
43
|
+
/** Gradient color pair for the package logo [from, to] */
|
|
44
|
+
gradient?: [string, string];
|
|
45
|
+
/** Optional badge text (e.g. "Coming Soon") */
|
|
46
|
+
badge?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MegaMenuSection {
|
|
50
|
+
title: string;
|
|
51
|
+
items: MegaMenuItem[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface HeaderNavMegaMenu {
|
|
55
|
+
label: string;
|
|
56
|
+
sections: MegaMenuSection[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export type HeaderNavEntry = HeaderNavLink | HeaderNavDropdown | HeaderNavMegaMenu;
|
|
38
60
|
|
|
39
61
|
export function isDropdown(entry: HeaderNavEntry): entry is HeaderNavDropdown {
|
|
40
|
-
return 'items' in entry;
|
|
62
|
+
return 'items' in entry && !('sections' in entry);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function isMegaMenu(entry: HeaderNavEntry): entry is HeaderNavMegaMenu {
|
|
66
|
+
return 'sections' in entry;
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
/**
|
package/src/style-utils.ts
CHANGED
|
@@ -93,7 +93,7 @@ export function createRuntimeWebMCPTools(options: RuntimeToolsOptions = {}): Web
|
|
|
93
93
|
|
|
94
94
|
const entry: { name: string; value: string; computed?: string; category?: string } = {
|
|
95
95
|
name: token.name,
|
|
96
|
-
value: token.value,
|
|
96
|
+
value: String(token.value),
|
|
97
97
|
category: cat,
|
|
98
98
|
};
|
|
99
99
|
|