@terrafirmagreg-team/mdx 0.0.0-stage → 1.2.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/package.json CHANGED
@@ -1,6 +1,64 @@
1
1
  {
2
2
  "name": "@terrafirmagreg-team/mdx",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.2.0",
4
+ "type": "module",
5
+ "description": "MDX/markdown pipeline",
6
+ "license": "LGPL-3.0-only",
7
+ "exports": {
8
+ "./lib": "./src/lib/index.ts",
9
+ "./lib/*": [
10
+ "./src/lib/*.ts",
11
+ "./src/lib/*/index.ts"
12
+ ],
13
+ "./components/*": [
14
+ "./src/components/*.tsx",
15
+ "./src/components/*/index.tsx",
16
+ "./src/components/*/index.ts"
17
+ ],
18
+ "./types/*": "./src/types/*.ts",
19
+ "./types": "./src/types/index.ts"
20
+ },
21
+ "dependencies": {
22
+ "@fumadocs/story": "^1.5.3",
23
+ "@fumari/stf": "^1.1.2",
24
+ "@terrafirmagreg-team/translate": "1.2.0",
25
+ "@terrafirmagreg-team/ui": "1.2.0",
26
+ "@types/turndown": "^5.0.6",
27
+ "fumadocs-core": "16.17.8",
28
+ "fumadocs-docgen": "^3.2.0",
29
+ "fumadocs-twoslash": "^4.0.4",
30
+ "fumadocs-typescript": "5.6.0",
31
+ "lucide-react": "^0.577.0",
32
+ "rehype-katex": "^7.0.1",
33
+ "rehype-raw": "^7.0.0",
34
+ "remark-directive": "^4.0.0",
35
+ "remark-gfm": "^4.0.1",
36
+ "remark-math": "^6.0.0",
37
+ "remark-parse": "^11.0.0",
38
+ "turndown": "^7.2.5",
39
+ "turndown-plugin-gfm": "^1.0.2",
40
+ "unified": "^11.0.5",
41
+ "unist-util-visit": "^5.2.0"
42
+ },
43
+ "peerDependencies": {
44
+ "next": ">=15",
45
+ "react": ">=19"
46
+ },
47
+ "devDependencies": {
48
+ "@types/mdx": "^2.0.14",
49
+ "@types/node": "^26.6.5",
50
+ "@types/react": "^19.3.0",
51
+ "@types/react-dom": "^19.3.0"
52
+ },
53
+ "repository": {
54
+ "type": "git",
55
+ "url": "https://github.com/TerraFirmaGreg-Team/Web-Foundation.git",
56
+ "directory": "packages/mdx"
57
+ },
58
+ "publishConfig": {
59
+ "access": "public"
60
+ },
61
+ "files": [
62
+ "src"
63
+ ]
6
64
  }
@@ -0,0 +1,62 @@
1
+ import { ImageZoom } from '@terrafirmagreg-team/ui/components/mdx/image-zoom';
2
+ import { resolveSrc } from '../lib/pack-assets';
3
+ import type { DocsImageProps } from '../types/mdx';
4
+
5
+ export function DocsImage({
6
+ src,
7
+ pack,
8
+ mod,
9
+ path,
10
+ alt = '',
11
+ width,
12
+ height,
13
+ className,
14
+ caption,
15
+ defaultPack: pagePack,
16
+ ...rest
17
+ }: DocsImageProps) {
18
+ const packHint = pack ?? pagePack;
19
+ const resolved = resolveSrc({ src, pack, mod, path }, packHint);
20
+ if (!resolved) {
21
+ return (
22
+ <p className="not-prose text-sm text-muted-foreground">
23
+ Missing image source (set <code>src</code> or <code>mod</code>+
24
+ <code>path</code>; pack defaults from the docs slug).
25
+ </p>
26
+ );
27
+ }
28
+
29
+ const w = Number(width) || undefined;
30
+ const h = Number(height) || undefined;
31
+
32
+ return (
33
+ <figure className="not-prose my-4 space-y-2">
34
+ <ImageZoom
35
+ src={resolved}
36
+ alt={alt}
37
+ width={w ?? 960}
38
+ height={h ?? 540}
39
+ className={className}
40
+ {...rest}
41
+ >
42
+ {w === undefined || h === undefined ? (
43
+ // eslint-disable-next-line @next/next/no-img-element
44
+ <img
45
+ src={resolved}
46
+ alt={alt}
47
+ width={w}
48
+ height={h}
49
+ className={className}
50
+ style={{ width: w ?? 'auto', height: h ?? 'auto' }}
51
+ />
52
+ ) : undefined}
53
+ </ImageZoom>
54
+ {caption ? (
55
+ <figcaption className="text-center text-sm text-muted-foreground">
56
+ {caption}
57
+ </figcaption>
58
+ ) : null}
59
+ </figure>
60
+ );
61
+ }
62
+
@@ -0,0 +1,158 @@
1
+ import {
2
+ Tab,
3
+ Tabs,
4
+ TabsContent,
5
+ TabsList,
6
+ TabsTrigger,
7
+ } from '@terrafirmagreg-team/ui/components/mdx/tabs';
8
+ import {
9
+ Accordion,
10
+ Accordions,
11
+ } from '@terrafirmagreg-team/ui/components/mdx/accordion';
12
+ import { Step, Steps } from '@terrafirmagreg-team/ui/components/mdx/steps';
13
+ import {
14
+ File,
15
+ Folder,
16
+ Files,
17
+ } from '@terrafirmagreg-team/ui/components/mdx/files';
18
+ import { ImageZoom } from '@terrafirmagreg-team/ui/components/mdx/image-zoom';
19
+ import { Banner } from '@terrafirmagreg-team/ui/components/mdx/banner';
20
+ import { TypeTable } from '@terrafirmagreg-team/ui/components/mdx/type-table';
21
+ import { Card, Cards } from '@terrafirmagreg-team/ui/components/mdx/card';
22
+ import { Table } from '@terrafirmagreg-team/ui/components/mdx/table';
23
+ import {
24
+ CalloutDescription,
25
+ CalloutTitle,
26
+ } from '@terrafirmagreg-team/ui/components/mdx/callout';
27
+ import {
28
+ CodeBlock,
29
+ CodeBlockTab,
30
+ CodeBlockTabs,
31
+ CodeBlockTabsList,
32
+ CodeBlockTabsTrigger,
33
+ Pre,
34
+ } from '@terrafirmagreg-team/ui/components/mdx/codeblock';
35
+ import { Heading } from '@terrafirmagreg-team/ui/components/blocks/page/heading';
36
+ import * as Twoslash from 'fumadocs-twoslash/ui';
37
+ import { Mermaid } from '@terrafirmagreg-team/ui/components/mdx/mermaid';
38
+ import { Callout, CalloutContainer } from '@terrafirmagreg-team/ui/components/mdx/callout';
39
+ import { Input, Output, Preview } from '@terrafirmagreg-team/ui/components/mdx/preview';
40
+ import { DocsImage } from './docs-image';
41
+ import { Multiblock } from '@terrafirmagreg-team/ui/components/mdx/multiblock';
42
+ import { GradientText } from '@terrafirmagreg-team/ui/components/mdx/gradient-text';
43
+ import { GlowHeader } from '@terrafirmagreg-team/ui/components/mdx/glow-header';
44
+ import { Icon } from '@terrafirmagreg-team/ui/components/mdx/icon';
45
+ import { LinkPreview } from '@terrafirmagreg-team/ui/components/blocks/link/link-preview';
46
+ import type { MDXComponents } from 'mdx/types';
47
+ import type { ComponentProps, HTMLAttributes } from 'react';
48
+
49
+ const extras: MDXComponents = {};
50
+
51
+ export function registerMdxComponents(map: MDXComponents): void {
52
+ Object.assign(extras, map);
53
+ }
54
+
55
+ export function getMDXComponents(components?: MDXComponents) {
56
+ return {
57
+ CodeBlockTab,
58
+ CodeBlockTabs,
59
+ CodeBlockTabsList,
60
+ CodeBlockTabsTrigger,
61
+ Tab,
62
+ Tabs,
63
+ TabsContent,
64
+ TabsList,
65
+ TabsTrigger,
66
+ ...Twoslash,
67
+ Accordion,
68
+ Accordions,
69
+ Step,
70
+ Steps,
71
+ File,
72
+ Folder,
73
+ Files,
74
+ Mermaid,
75
+ Banner,
76
+ TypeTable,
77
+ Card,
78
+ Cards,
79
+ Callout,
80
+ CalloutContainer,
81
+ CalloutTitle,
82
+ CalloutDescription,
83
+ Preview,
84
+ Input,
85
+ Output,
86
+ DocsImage,
87
+ Multiblock,
88
+ GradientText,
89
+ GlowHeader,
90
+ Icon,
91
+ a: LinkPreview,
92
+ pre: (props: HTMLAttributes<HTMLPreElement>) => (
93
+ <CodeBlock {...props}>
94
+ <Pre>{props.children}</Pre>
95
+ </CodeBlock>
96
+ ),
97
+ h1: (props: HTMLAttributes<HTMLHeadingElement>) => (
98
+ <Heading as="h1" {...props} />
99
+ ),
100
+ h2: (props: HTMLAttributes<HTMLHeadingElement>) => (
101
+ <Heading as="h2" {...props} />
102
+ ),
103
+ h3: (props: HTMLAttributes<HTMLHeadingElement>) => (
104
+ <Heading as="h3" {...props} />
105
+ ),
106
+ h4: (props: HTMLAttributes<HTMLHeadingElement>) => (
107
+ <Heading as="h4" {...props} />
108
+ ),
109
+ h5: (props: HTMLAttributes<HTMLHeadingElement>) => (
110
+ <Heading as="h5" {...props} />
111
+ ),
112
+ h6: (props: HTMLAttributes<HTMLHeadingElement>) => (
113
+ <Heading as="h6" {...props} />
114
+ ),
115
+ table: Table,
116
+ img: (props: ComponentProps<'img'>) => {
117
+ const src =
118
+ typeof props.src === 'object' && props.src !== null
119
+ ? ((props.src as { src?: string }).src ?? '')
120
+ : props.src;
121
+ if (typeof src !== 'string' || /^https?:\/\//i.test(src)) {
122
+ // eslint-disable-next-line @next/next/no-img-element
123
+ return <img {...props} src={src} alt={props.alt ?? ''} />;
124
+ }
125
+ const width = Number(props.width) || undefined;
126
+ const height = Number(props.height) || undefined;
127
+ return (
128
+ <ImageZoom
129
+ src={src}
130
+ alt={props.alt ?? ''}
131
+ width={width ?? 960}
132
+ height={height ?? 540}
133
+ sizes={props.sizes as string | undefined}
134
+ className={props.className}
135
+ >
136
+ {width === undefined || height === undefined ? (
137
+ // eslint-disable-next-line @next/next/no-img-element
138
+ <img
139
+ src={src}
140
+ alt={props.alt ?? ''}
141
+ width={width}
142
+ height={height}
143
+ className={props.className}
144
+ style={{ width: width ?? 'auto', height: height ?? 'auto' }}
145
+ />
146
+ ) : undefined}
147
+ </ImageZoom>
148
+ );
149
+ },
150
+ ...extras,
151
+ ...components,
152
+ } as MDXComponents;
153
+ }
154
+
155
+ declare global {
156
+ type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
157
+ }
158
+
@@ -0,0 +1,318 @@
1
+ 'use client';
2
+
3
+ import { useDataEngine, useFieldValue } from '@fumari/stf';
4
+ import {
5
+ typeToString,
6
+ validate,
7
+ type TypeNode,
8
+ } from '@fumadocs/story/type-tree';
9
+ import { directiveMeta } from '../lib/story-remark';
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from '@terrafirmagreg-team/ui/components/ui/select';
17
+ import { cn } from '@terrafirmagreg-team/ui/lib/utils/cn';
18
+ import { X } from 'lucide-react';
19
+ import { useState } from 'react';
20
+
21
+ function resolveNode(node: TypeNode, value: unknown): TypeNode {
22
+ if (node.type === 'intersection') return resolveNode(node.intersection, value);
23
+ if (node.type !== 'union') return node;
24
+ return resolveNode(
25
+ node.types.find((item) => validate(item, value)) ?? node.types[0],
26
+ value,
27
+ );
28
+ }
29
+
30
+ function selectMembers(
31
+ node: TypeNode,
32
+ ): { label: string; value: unknown }[] | undefined {
33
+ if (node.type === 'enum') return node.members;
34
+ if (
35
+ node.type === 'union' &&
36
+ node.types.every((t) => t.type === 'literal')
37
+ ) {
38
+ return node.types.map((t) => ({
39
+ label: String(t.type === 'literal' ? t.value : ''),
40
+ value: t.type === 'literal' ? t.value : undefined,
41
+ }));
42
+ }
43
+ return undefined;
44
+ }
45
+
46
+ const inputClass =
47
+ 'min-w-0 flex-1 bg-transparent px-3 text-fd-foreground outline-none placeholder:text-fd-muted-foreground/70 focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-fd-primary';
48
+
49
+ function StringEditor({
50
+ name,
51
+ value,
52
+ setValue,
53
+ multiline,
54
+ }: {
55
+ name: string;
56
+ value: unknown;
57
+ setValue: (v: unknown) => void;
58
+ multiline?: boolean;
59
+ }) {
60
+ if (multiline) {
61
+ const lineCount =
62
+ typeof value === 'string' ? value.split('\n').length : 0;
63
+ return (
64
+ <textarea
65
+ aria-label={name}
66
+ rows={Math.min(12, Math.max(4, lineCount + 1))}
67
+ value={value == null ? '' : String(value)}
68
+ placeholder="string"
69
+ onChange={(e) => setValue(e.target.value)}
70
+ className={cn(inputClass, 'min-h-12 resize-y py-2 font-mono text-xs')}
71
+ />
72
+ );
73
+ }
74
+ return (
75
+ <input
76
+ aria-label={name}
77
+ value={value == null ? '' : String(value)}
78
+ placeholder="string"
79
+ onChange={(e) => setValue(e.target.value)}
80
+ className={inputClass}
81
+ />
82
+ );
83
+ }
84
+
85
+ function NumberEditor({
86
+ name,
87
+ value,
88
+ setValue,
89
+ }: {
90
+ name: string;
91
+ value: unknown;
92
+ setValue: (v: unknown) => void;
93
+ }) {
94
+ return (
95
+ <input
96
+ aria-label={name}
97
+ type="number"
98
+ value={value == null ? '' : String(value)}
99
+ placeholder="number"
100
+ onChange={(e) =>
101
+ setValue(e.target.value === '' ? undefined : Number(e.target.value))
102
+ }
103
+ className={inputClass}
104
+ />
105
+ );
106
+ }
107
+
108
+ function BooleanEditor({
109
+ name,
110
+ value,
111
+ setValue,
112
+ }: {
113
+ name: string;
114
+ value: unknown;
115
+ setValue: (v: unknown) => void;
116
+ }) {
117
+ return (
118
+ <input
119
+ aria-label={name}
120
+ type="checkbox"
121
+ checked={value === true}
122
+ onChange={(e) => setValue(e.target.checked)}
123
+ className="mx-3 size-3.5 shrink-0 accent-fd-primary"
124
+ />
125
+ );
126
+ }
127
+
128
+ function SelectEditor({
129
+ name,
130
+ value,
131
+ setValue,
132
+ members,
133
+ }: {
134
+ name: string;
135
+ value: unknown;
136
+ setValue: (v: unknown) => void;
137
+ members: { label: string; value: unknown }[];
138
+ }) {
139
+ const index = members.findIndex((m) => Object.is(m.value, value));
140
+ return (
141
+ <Select
142
+ value={index < 0 ? '' : String(index)}
143
+ onValueChange={(v) => {
144
+ const next = members[Number(v)];
145
+ if (next) setValue(next.value);
146
+ }}
147
+ >
148
+ <SelectTrigger
149
+ aria-label={name}
150
+ className="h-8 flex-1 gap-1 border-0 bg-transparent px-3 text-xs shadow-none focus:ring-0 focus-visible:ring-1"
151
+ >
152
+ <SelectValue placeholder="select">
153
+ {(v: unknown) =>
154
+ typeof v === 'string' && v !== ''
155
+ ? members[Number(v)]?.label
156
+ : null
157
+ }
158
+ </SelectValue>
159
+ </SelectTrigger>
160
+ <SelectContent>
161
+ {members.map((m, i) => (
162
+ <SelectItem key={i} value={String(i)} className="text-xs">
163
+ {m.label}
164
+ </SelectItem>
165
+ ))}
166
+ </SelectContent>
167
+ </Select>
168
+ );
169
+ }
170
+
171
+ function JsonEditor({
172
+ name,
173
+ value,
174
+ setValue,
175
+ }: {
176
+ name: string;
177
+ value: unknown;
178
+ setValue: (v: unknown) => void;
179
+ }) {
180
+ const [text, setText] = useState(() =>
181
+ value === undefined ? '' : JSON.stringify(value),
182
+ );
183
+ const [invalid, setInvalid] = useState(false);
184
+ const [prevValue, setPrevValue] = useState(value);
185
+ if (prevValue !== value) {
186
+ setPrevValue(value);
187
+ setText(value === undefined ? '' : JSON.stringify(value));
188
+ setInvalid(false);
189
+ }
190
+ return (
191
+ <textarea
192
+ aria-label={name}
193
+ rows={2}
194
+ value={text}
195
+ placeholder="JSON"
196
+ onChange={(e) => {
197
+ const next = e.target.value;
198
+ setText(next);
199
+ if (next.trim() === '') {
200
+ setValue(undefined);
201
+ setInvalid(false);
202
+ return;
203
+ }
204
+ try {
205
+ setValue(JSON.parse(next));
206
+ setInvalid(false);
207
+ } catch {
208
+ setInvalid(true);
209
+ }
210
+ }}
211
+ className={cn(
212
+ inputClass,
213
+ 'min-h-10 resize-y py-2 font-mono text-xs',
214
+ invalid && 'text-fd-error',
215
+ )}
216
+ />
217
+ );
218
+ }
219
+
220
+ function Field({
221
+ name,
222
+ node,
223
+ required,
224
+ multiline,
225
+ description,
226
+ }: {
227
+ name: string;
228
+ node: TypeNode;
229
+ required?: boolean;
230
+ multiline?: boolean;
231
+ description?: string;
232
+ }) {
233
+ const engine = useDataEngine();
234
+ const [value, setValue] = useFieldValue([name]);
235
+ const resolved = resolveNode(node, value);
236
+ const members = selectMembers(resolved);
237
+
238
+ let editor: React.ReactNode;
239
+ if (members) {
240
+ editor = (
241
+ <SelectEditor
242
+ name={name}
243
+ value={value}
244
+ setValue={setValue}
245
+ members={members}
246
+ />
247
+ );
248
+ } else if (resolved.type === 'boolean') {
249
+ editor = <BooleanEditor name={name} value={value} setValue={setValue} />;
250
+ } else if (resolved.type === 'number' || resolved.type === 'bigint') {
251
+ editor = <NumberEditor name={name} value={value} setValue={setValue} />;
252
+ } else if (resolved.type === 'string' || resolved.type === 'literal') {
253
+ editor = (
254
+ <StringEditor
255
+ name={name}
256
+ value={value}
257
+ setValue={setValue}
258
+ multiline={multiline}
259
+ />
260
+ );
261
+ } else {
262
+ editor = <JsonEditor name={name} value={value} setValue={setValue} />;
263
+ }
264
+
265
+ return (
266
+ <div className="flex min-h-10 items-center border-b last:border-0">
267
+ <span
268
+ className="flex w-28 shrink-0 items-center gap-1 px-3 font-mono text-[0.8125rem] text-fd-muted-foreground"
269
+ title={description}
270
+ >
271
+ <span className="truncate">{name}</span>
272
+ {!required && <span className="text-fd-muted-foreground/60">?</span>}
273
+ </span>
274
+ {editor}
275
+ <span className="hidden w-24 shrink-0 truncate px-2 font-mono text-xs text-fd-muted-foreground/70 @3xl:block">
276
+ {typeToString(resolved)}
277
+ </span>
278
+ {!required && (
279
+ <button
280
+ type="button"
281
+ aria-label={`Unset ${name}`}
282
+ onClick={() => engine.delete([name])}
283
+ className="me-2 flex size-6 shrink-0 items-center justify-center rounded-md text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-foreground"
284
+ >
285
+ <X className="size-3" />
286
+ </button>
287
+ )}
288
+ </div>
289
+ );
290
+ }
291
+
292
+ export function StoryFields({
293
+ node,
294
+ storyKey,
295
+ }: {
296
+ node: TypeNode;
297
+ storyKey: string;
298
+ }) {
299
+ const meta = directiveMeta(storyKey);
300
+ if (node.type !== 'object') return null;
301
+ const multiline = new Set(meta?.multiline ?? []);
302
+ const descriptions = meta?.descriptions ?? {};
303
+ return (
304
+ <div className="text-[0.8125rem]">
305
+ {node.properties.map((prop) => (
306
+ <Field
307
+ key={prop.name}
308
+ name={prop.name}
309
+ node={prop.type}
310
+ required={prop.required}
311
+ multiline={multiline.has(prop.name)}
312
+ description={descriptions[prop.name]}
313
+ />
314
+ ))}
315
+ </div>
316
+ );
317
+ }
318
+