@terrafirmagreg-team/mdx 1.2.7 → 1.2.8
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 +2 -2
- package/src/components/story-fields.tsx +33 -2
- package/src/components/story-panel.tsx +104 -26
- package/src/lib/story-remark.ts +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@terrafirmagreg-team/mdx",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MDX/markdown pipeline",
|
|
6
6
|
"license": "LGPL-3.0-only",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"@fumadocs/story": "^1.5.3",
|
|
24
24
|
"@fumari/stf": "^1.1.2",
|
|
25
25
|
"@terrafirmagreg-team/translate": "^1.2.8",
|
|
26
|
-
"@terrafirmagreg-team/ui": "^1.2.
|
|
26
|
+
"@terrafirmagreg-team/ui": "^1.2.10",
|
|
27
27
|
"fumadocs-core": "16.17.10",
|
|
28
28
|
"fumadocs-docgen": "^3.2.0",
|
|
29
29
|
"fumadocs-twoslash": "^4.0.4",
|
|
@@ -15,8 +15,17 @@ import {
|
|
|
15
15
|
SelectValue,
|
|
16
16
|
} from '@terrafirmagreg-team/ui/components/ui/select';
|
|
17
17
|
import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
|
|
18
|
-
import { X } from 'lucide-react';
|
|
19
|
-
import { useState } from 'react';
|
|
18
|
+
import { Pin, PinOff, X } from 'lucide-react';
|
|
19
|
+
import { createContext, useContext, useState } from 'react';
|
|
20
|
+
|
|
21
|
+
export const PinnedContext = createContext<{
|
|
22
|
+
pinned: ReadonlySet<string>;
|
|
23
|
+
toggle: (name: string) => void;
|
|
24
|
+
} | null>(null);
|
|
25
|
+
|
|
26
|
+
export function usePinned() {
|
|
27
|
+
return useContext(PinnedContext);
|
|
28
|
+
}
|
|
20
29
|
|
|
21
30
|
function resolveNode(node: TypeNode, value: unknown): TypeNode {
|
|
22
31
|
if (node.type === 'intersection') return resolveNode(node.intersection, value);
|
|
@@ -234,6 +243,8 @@ function Field({
|
|
|
234
243
|
const [value, setValue] = useFieldValue([name]);
|
|
235
244
|
const resolved = resolveNode(node, value);
|
|
236
245
|
const members = selectMembers(resolved);
|
|
246
|
+
const pinnedCtx = usePinned();
|
|
247
|
+
const isPinned = pinnedCtx?.pinned.has(name) ?? false;
|
|
237
248
|
|
|
238
249
|
let editor: React.ReactNode;
|
|
239
250
|
if (members) {
|
|
@@ -275,6 +286,26 @@ function Field({
|
|
|
275
286
|
<span className="hidden w-24 shrink-0 truncate px-2 font-mono text-xs text-fd-muted-foreground/70 @3xl:block">
|
|
276
287
|
{typeToString(resolved)}
|
|
277
288
|
</span>
|
|
289
|
+
{pinnedCtx && !required && (
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
aria-label={`${isPinned ? 'Unpin' : 'Pin'} ${name}`}
|
|
293
|
+
title={
|
|
294
|
+
isPinned
|
|
295
|
+
? 'Unpin — reset with preset'
|
|
296
|
+
: 'Pin — keep value across presets'
|
|
297
|
+
}
|
|
298
|
+
onClick={() => pinnedCtx.toggle(name)}
|
|
299
|
+
className={cn(
|
|
300
|
+
'me-1 flex size-6 shrink-0 items-center justify-center rounded-md hover:bg-fd-accent',
|
|
301
|
+
isPinned
|
|
302
|
+
? 'text-fd-primary'
|
|
303
|
+
: 'text-fd-muted-foreground hover:text-fd-foreground',
|
|
304
|
+
)}
|
|
305
|
+
>
|
|
306
|
+
{isPinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
|
|
307
|
+
</button>
|
|
308
|
+
)}
|
|
278
309
|
{!required && (
|
|
279
310
|
<button
|
|
280
311
|
type="button"
|
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
useStoryArgs,
|
|
7
7
|
type WithControlProps,
|
|
8
8
|
} from '@fumadocs/story';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
9
|
+
import { useDataEngine } from '@fumari/stf';
|
|
10
|
+
import { PinnedContext, StoryFields, usePinned } from './story-fields';
|
|
11
|
+
import { directiveMeta, remarkFor, tweakControls } from '../lib/story-remark';
|
|
11
12
|
import { highlightRemark } from '../lib/remark-highlight';
|
|
12
13
|
import { useCopyButton } from '@terrafirmagreg-team/ui/hooks/use-copy-button';
|
|
13
14
|
import { Check, Clipboard } from 'lucide-react';
|
|
@@ -22,22 +23,34 @@ import {
|
|
|
22
23
|
Component,
|
|
23
24
|
createElement,
|
|
24
25
|
useMemo,
|
|
26
|
+
useState,
|
|
25
27
|
type ErrorInfo,
|
|
26
28
|
type ReactNode,
|
|
27
29
|
} from 'react';
|
|
28
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
ChevronLeft,
|
|
32
|
+
ChevronRight,
|
|
33
|
+
CircleAlert,
|
|
34
|
+
RotateCcw,
|
|
35
|
+
} from 'lucide-react';
|
|
29
36
|
|
|
30
37
|
function RemarkHint() {
|
|
31
38
|
const { displayName, preset, variant } = useStory();
|
|
32
39
|
const args = useStoryArgs();
|
|
40
|
+
const suppressed =
|
|
41
|
+
directiveMeta(displayName ?? '')?.remark === false;
|
|
33
42
|
const text = useMemo(
|
|
34
|
-
() =>
|
|
35
|
-
|
|
43
|
+
() =>
|
|
44
|
+
suppressed
|
|
45
|
+
? ''
|
|
46
|
+
: remarkFor(displayName ?? 'component', args, preset?.controls, variant),
|
|
47
|
+
[suppressed, displayName, args, preset, variant],
|
|
36
48
|
);
|
|
37
49
|
const tokens = useMemo(() => highlightRemark(text), [text]);
|
|
38
50
|
const { checked, onClick } = useCopyButton(() =>
|
|
39
51
|
navigator.clipboard.writeText(text),
|
|
40
52
|
);
|
|
53
|
+
if (suppressed) return null;
|
|
41
54
|
return (
|
|
42
55
|
<div className="relative border-t">
|
|
43
56
|
<pre className="overflow-x-auto px-4 py-2.5 pe-9 font-mono text-xs leading-relaxed text-fd-muted-foreground whitespace-pre-wrap">
|
|
@@ -93,34 +106,83 @@ class PreviewBoundary extends Component<
|
|
|
93
106
|
}
|
|
94
107
|
}
|
|
95
108
|
|
|
109
|
+
function useApplyVariant() {
|
|
110
|
+
const { setVariant } = useStory();
|
|
111
|
+
const engine = useDataEngine();
|
|
112
|
+
const pinned = usePinned();
|
|
113
|
+
return (value: string) => {
|
|
114
|
+
const keep = new Map(
|
|
115
|
+
[...(pinned?.pinned ?? [])].map(
|
|
116
|
+
(name) =>
|
|
117
|
+
[name, (engine.getData() as Record<string, unknown>)[name]] as const,
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
setVariant(value);
|
|
121
|
+
for (const [name, value] of keep) {
|
|
122
|
+
if (value === undefined) engine.delete([name]);
|
|
123
|
+
else if (!engine.update([name], value)) engine.init([name], value);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
96
128
|
function VariantSelect() {
|
|
97
|
-
const { presets, variant
|
|
129
|
+
const { presets, variant } = useStory();
|
|
130
|
+
const applyVariant = useApplyVariant();
|
|
131
|
+
const index = presets.findIndex((p) => p.variant === variant);
|
|
132
|
+
const step = (delta: number) => {
|
|
133
|
+
const next =
|
|
134
|
+
presets[(index + delta + presets.length) % presets.length];
|
|
135
|
+
if (next) applyVariant(next.variant);
|
|
136
|
+
};
|
|
137
|
+
const stepClass =
|
|
138
|
+
'rounded-md p-1.5 text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-foreground';
|
|
98
139
|
return (
|
|
99
|
-
<
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
140
|
+
<div className="flex items-center gap-0.5">
|
|
141
|
+
<button
|
|
142
|
+
type="button"
|
|
143
|
+
aria-label="Previous variant"
|
|
144
|
+
title="Previous variant"
|
|
145
|
+
onClick={() => step(-1)}
|
|
146
|
+
className={stepClass}
|
|
147
|
+
>
|
|
148
|
+
<ChevronLeft className="size-3.5" />
|
|
149
|
+
</button>
|
|
150
|
+
<Select value={variant} onValueChange={(v) => v && applyVariant(v)}>
|
|
151
|
+
<SelectTrigger className="h-7 w-auto max-w-64 gap-1.5 border-0 bg-transparent px-2 text-xs hover:bg-fd-accent focus:ring-0 focus-visible:ring-2">
|
|
152
|
+
<span className="text-fd-muted-foreground">Variant</span>
|
|
153
|
+
<SelectValue className="truncate font-medium" />
|
|
154
|
+
</SelectTrigger>
|
|
155
|
+
<SelectContent align="end">
|
|
156
|
+
{presets.map((item) => (
|
|
157
|
+
<SelectItem
|
|
158
|
+
key={item.variant}
|
|
159
|
+
value={item.variant}
|
|
160
|
+
className="text-xs"
|
|
161
|
+
>
|
|
162
|
+
{item.variant}
|
|
163
|
+
</SelectItem>
|
|
164
|
+
))}
|
|
165
|
+
</SelectContent>
|
|
166
|
+
</Select>
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
aria-label="Next variant"
|
|
170
|
+
title="Next variant"
|
|
171
|
+
onClick={() => step(1)}
|
|
172
|
+
className={stepClass}
|
|
173
|
+
>
|
|
174
|
+
<ChevronRight className="size-3.5" />
|
|
175
|
+
</button>
|
|
176
|
+
</div>
|
|
116
177
|
);
|
|
117
178
|
}
|
|
118
179
|
|
|
119
180
|
function Content() {
|
|
120
|
-
const { presets, displayName, preset, variant,
|
|
181
|
+
const { presets, displayName, preset, variant, Component } =
|
|
121
182
|
useStory();
|
|
183
|
+
const applyVariant = useApplyVariant();
|
|
122
184
|
const args = useStoryArgs();
|
|
123
|
-
const reset = () =>
|
|
185
|
+
const reset = () => applyVariant(variant);
|
|
124
186
|
return (
|
|
125
187
|
<div className="not-prose overflow-hidden rounded-xl border bg-fd-card text-fd-card-foreground">
|
|
126
188
|
{(displayName || presets.length > 1) && (
|
|
@@ -172,9 +234,25 @@ export function DocsWithControl({ presets, ...rest }: WithControlProps) {
|
|
|
172
234
|
})),
|
|
173
235
|
[presets, rest.displayName],
|
|
174
236
|
);
|
|
237
|
+
const [pinned, setPinned] = useState<ReadonlySet<string>>(new Set());
|
|
238
|
+
const pinApi = useMemo(
|
|
239
|
+
() => ({
|
|
240
|
+
pinned,
|
|
241
|
+
toggle: (name: string) =>
|
|
242
|
+
setPinned((prev) => {
|
|
243
|
+
const next = new Set(prev);
|
|
244
|
+
if (next.has(name)) next.delete(name);
|
|
245
|
+
else next.add(name);
|
|
246
|
+
return next;
|
|
247
|
+
}),
|
|
248
|
+
}),
|
|
249
|
+
[pinned],
|
|
250
|
+
);
|
|
175
251
|
return (
|
|
176
252
|
<StoryProvider {...rest} presets={tweaked}>
|
|
177
|
-
<
|
|
253
|
+
<PinnedContext.Provider value={pinApi}>
|
|
254
|
+
<Content />
|
|
255
|
+
</PinnedContext.Provider>
|
|
178
256
|
</StoryProvider>
|
|
179
257
|
);
|
|
180
258
|
}
|
package/src/lib/story-remark.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type DirectiveMeta = {
|
|
|
13
13
|
props?: string[];
|
|
14
14
|
solo?: boolean;
|
|
15
15
|
paired?: boolean;
|
|
16
|
+
remark?: boolean;
|
|
16
17
|
descriptions?: Record<string, string>;
|
|
17
18
|
raw?: string;
|
|
18
19
|
variants?: Record<string, Partial<DirectiveMeta>>;
|
|
@@ -397,6 +398,27 @@ const DIRECTIVE_META: Record<string, DirectiveMeta> = {
|
|
|
397
398
|
Display: { raw: '$$e = mc^2$$' },
|
|
398
399
|
},
|
|
399
400
|
},
|
|
401
|
+
og: {
|
|
402
|
+
remark: false,
|
|
403
|
+
props: ['url', 'section', 'dataset', 'slug', 'lang', 'version'],
|
|
404
|
+
descriptions: {
|
|
405
|
+
url: 'Page or image.png URL — overrides the fields below',
|
|
406
|
+
section: 'Site section the page belongs to',
|
|
407
|
+
dataset: 'Dataset id within the section',
|
|
408
|
+
slug: 'Page path inside the dataset, e.g. getting-started/install',
|
|
409
|
+
lang: 'Content language of the rendered card',
|
|
410
|
+
version: 'Dataset version pin, e.g. 0.13.18',
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
site: {
|
|
414
|
+
remark: false,
|
|
415
|
+
props: ['view', 'section', 'links'],
|
|
416
|
+
descriptions: {
|
|
417
|
+
view: 'What to show',
|
|
418
|
+
section: 'Limit the listing to one section',
|
|
419
|
+
links: 'URLs to preview in the links view (JSON array)',
|
|
420
|
+
},
|
|
421
|
+
},
|
|
400
422
|
};
|
|
401
423
|
|
|
402
424
|
export function directiveMeta(key: string): DirectiveMeta | undefined {
|