@djangocfg/widget-code 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/package.json +119 -0
- package/src/diff-viewer/DiffViewer.tsx +168 -0
- package/src/diff-viewer/README.md +33 -0
- package/src/diff-viewer/components/CopyButton.tsx +49 -0
- package/src/diff-viewer/components/DiffLineContent.tsx +48 -0
- package/src/diff-viewer/components/SplitView.tsx +270 -0
- package/src/diff-viewer/components/UnifiedView.tsx +189 -0
- package/src/diff-viewer/diff-viewer.stories.tsx +168 -0
- package/src/diff-viewer/hooks/useDiff.ts +47 -0
- package/src/diff-viewer/hooks/useHighlighter.ts +54 -0
- package/src/diff-viewer/index.ts +22 -0
- package/src/diff-viewer/lazy.tsx +25 -0
- package/src/diff-viewer/types.ts +125 -0
- package/src/diff-viewer/utils/computeDiff.ts +171 -0
- package/src/editor/README.md +187 -0
- package/src/editor/components/DiffEditor.tsx +158 -0
- package/src/editor/components/Editor.tsx +293 -0
- package/src/editor/components/index.ts +2 -0
- package/src/editor/context/EditorProvider.tsx +212 -0
- package/src/editor/context/index.ts +1 -0
- package/src/editor/editor.stories.tsx +397 -0
- package/src/editor/hooks/index.ts +4 -0
- package/src/editor/hooks/useEditor.ts +36 -0
- package/src/editor/hooks/useEditorTheme.ts +91 -0
- package/src/editor/hooks/useLanguage.ts +29 -0
- package/src/editor/hooks/useMonaco.ts +79 -0
- package/src/editor/index.ts +16 -0
- package/src/editor/lazy.tsx +76 -0
- package/src/editor/lib/index.ts +2 -0
- package/src/editor/lib/languages.ts +227 -0
- package/src/editor/lib/themes.ts +42 -0
- package/src/editor/types/index.ts +137 -0
- package/src/editor/workers/index.ts +1 -0
- package/src/editor/workers/setup.ts +58 -0
- package/src/index.ts +10 -0
- package/src/json-editor/JsonEditor.tsx +115 -0
- package/src/json-editor/index.ts +1 -0
- package/src/json-editor/lazy.tsx +24 -0
- package/src/markdown-message/ActionRow.tsx +53 -0
- package/src/markdown-message/ChatMessageRow.tsx +103 -0
- package/src/markdown-message/CodeBlock.tsx +137 -0
- package/src/markdown-message/CollapseToggle.tsx +69 -0
- package/src/markdown-message/MarkdownMessage.tsx +376 -0
- package/src/markdown-message/README.md +221 -0
- package/src/markdown-message/builtinRules.tsx +92 -0
- package/src/markdown-message/components.tsx +224 -0
- package/src/markdown-message/index.ts +22 -0
- package/src/markdown-message/linkRules.ts +83 -0
- package/src/markdown-message/plainText.ts +83 -0
- package/src/markdown-message/sanitize.ts +88 -0
- package/src/markdown-message/streamingBlocks.ts +168 -0
- package/src/markdown-message/types.ts +168 -0
- package/src/panel-header/CodePanelHeader.tsx +121 -0
- package/src/panel-header/index.ts +1 -0
- package/src/pretty-code/PrettyCode.client.tsx +402 -0
- package/src/pretty-code/README.md +81 -0
- package/src/pretty-code/index.tsx +72 -0
- package/src/pretty-code/lazy.tsx +69 -0
- package/src/pretty-code/pretty-code.stories.tsx +353 -0
- package/src/pretty-code/registerPrismLanguages.ts +116 -0
- package/src/role-tokens.ts +39 -0
- package/src/use-collapsible-content.ts +236 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# PrettyCode
|
|
2
|
+
|
|
3
|
+
Syntax-highlighted code block. Prism-powered, dark-by-default surface.
|
|
4
|
+
|
|
5
|
+
## Why dark by default
|
|
6
|
+
|
|
7
|
+
PrettyCode renders on a fixed dark surface (`#0d1117` + `vsDark` palette)
|
|
8
|
+
regardless of the host theme — the same convention as GitHub, VSCode,
|
|
9
|
+
ChatGPT, Slack. Syntax highlighting ships its own contrast model
|
|
10
|
+
(keyword / string / number / comment) that flattens to low-contrast
|
|
11
|
+
pastels when forced onto a light UI surface.
|
|
12
|
+
|
|
13
|
+
The surface is intentionally **not** a semantic token. Semantic tokens
|
|
14
|
+
(`--primary`, `--accent`, `--card`) describe UI affordances; code
|
|
15
|
+
display is data, not UI.
|
|
16
|
+
|
|
17
|
+
## Structure
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
PrettyCode/
|
|
21
|
+
├── PrettyCode.client.tsx # Main component (Highlight + dark surface)
|
|
22
|
+
├── registerPrismLanguages.ts # Lazy-load extra grammars (bash, ruby, java, php)
|
|
23
|
+
├── index.tsx # Public re-export
|
|
24
|
+
└── lazy.tsx # Lazy-loaded export
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { PrettyCode } from '@djangocfg/widget-code/pretty-code';
|
|
31
|
+
|
|
32
|
+
<PrettyCode data={sourceCode} language="typescript" />
|
|
33
|
+
|
|
34
|
+
// Inline snippet (used in markdown rendering)
|
|
35
|
+
<PrettyCode data={`const x = 1`} language="ts" inline />
|
|
36
|
+
|
|
37
|
+
// Capped at 50 visible lines, scrolls if longer
|
|
38
|
+
<PrettyCode data={longBlob} language="json" maxLines={50} />
|
|
39
|
+
|
|
40
|
+
// Chrome-less variant — embed inside another scroll container
|
|
41
|
+
<PrettyCode data={src} language="bash" variant="plain" />
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Props
|
|
45
|
+
|
|
46
|
+
| Prop | Type | Default | Description |
|
|
47
|
+
|------|------|---------|-------------|
|
|
48
|
+
| `data` | `string \| object` | — | Code string. Objects are `JSON.stringify`d. |
|
|
49
|
+
| `language` | `Language` | — | Prism language id. Unknown → plain text. |
|
|
50
|
+
| `mode` | `'dark' \| 'light'` | `'dark'` | Palette override. Default is always dark (see "Why dark"). |
|
|
51
|
+
| `inline` | `boolean` | `false` | Render as inline `<code>` (no card / toolbar). |
|
|
52
|
+
| `variant` | `'card' \| 'plain'` | `'card'` | `plain` strips border, background, and toolbar. |
|
|
53
|
+
| `isCompact` | `boolean` | `false` | 12px font instead of 14px. |
|
|
54
|
+
| `maxLines` | `number` | `undefined` | When set, block scrolls past this many lines. |
|
|
55
|
+
| `customBg` | `string` | — | Tailwind class to override the dark surface. |
|
|
56
|
+
| `scrollIsolation` | `boolean` | `true` | Block scroll capture until clicked (long snippets). |
|
|
57
|
+
| `className` | `string` | — | Extra classes on the wrapper. |
|
|
58
|
+
|
|
59
|
+
## When to override `mode`
|
|
60
|
+
|
|
61
|
+
Almost never. The dark default is correct for ~all cases. Pass
|
|
62
|
+
`mode="light"` only for deliberate light-on-light renders (e.g. PDF
|
|
63
|
+
export targeting a printed page). The surface falls back to
|
|
64
|
+
`bg-card` semantic token in that mode.
|
|
65
|
+
|
|
66
|
+
## Supported languages
|
|
67
|
+
|
|
68
|
+
Out of the box: `javascript`, `typescript`, `python`, `json`, `css`,
|
|
69
|
+
`markup` (html/xml), `sql`, `yaml`, `markdown`, `go`.
|
|
70
|
+
|
|
71
|
+
Lazy-loaded on first use: `bash`/`shell`, `ruby`, `java`, `php`.
|
|
72
|
+
Lazy load is tracked through `useEnsurePrismLanguages()` — the
|
|
73
|
+
component re-renders once the grammar resolves.
|
|
74
|
+
|
|
75
|
+
Unknown languages fall back to plain text (no highlighting, no error).
|
|
76
|
+
|
|
77
|
+
## Storybook
|
|
78
|
+
|
|
79
|
+
`UI Tools / Code / PrettyCode` — `Playground`, `TypeScript`, `Json`,
|
|
80
|
+
`Python`, `Bash`, `Sql`, `Yaml`, `LongFileWithScroll`, `Inline`,
|
|
81
|
+
`Compact`, `LightMode`, `PlainVariant`.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PrettyCode Component - Dynamic Import Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Lazy loads the heavy Prism library (~500KB) only when needed.
|
|
5
|
+
* This significantly reduces the initial bundle size.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import React, { lazy, Suspense } from 'react';
|
|
11
|
+
import type { Language } from 'prism-react-renderer';
|
|
12
|
+
|
|
13
|
+
import { LoadingFallback } from '@djangocfg/widget-kit/lazy';
|
|
14
|
+
|
|
15
|
+
// Lazy load the client component
|
|
16
|
+
const PrettyCodeClient = lazy(() => import('./PrettyCode.client'));
|
|
17
|
+
|
|
18
|
+
// Shared compact placeholder (16px muted spinner), inside the code-surface
|
|
19
|
+
// border so nothing shifts when PrettyCode mounts. Same look as LazyPrettyCode
|
|
20
|
+
// (lazy.tsx) and the JSON viewer.
|
|
21
|
+
const CodeFallback = () => (
|
|
22
|
+
<LoadingFallback
|
|
23
|
+
minHeight={64}
|
|
24
|
+
text="Loading code…"
|
|
25
|
+
className="rounded-sm border border-border bg-muted dark:bg-code"
|
|
26
|
+
/>
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export interface PrettyCodeProps {
|
|
30
|
+
data: string | object;
|
|
31
|
+
language: Language;
|
|
32
|
+
className?: string;
|
|
33
|
+
mode?: 'dark' | 'light';
|
|
34
|
+
inline?: boolean;
|
|
35
|
+
customBg?: string;
|
|
36
|
+
isCompact?: boolean;
|
|
37
|
+
/** Block scroll capture until user clicks (default: true) */
|
|
38
|
+
scrollIsolation?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Line count above which the block starts scrolling instead of growing.
|
|
41
|
+
* ``undefined`` (default) = no cap, block grows to fit. Set e.g. ``50``
|
|
42
|
+
* to inline short snippets and cap long ones.
|
|
43
|
+
*/
|
|
44
|
+
maxLines?: number;
|
|
45
|
+
/** When set, a header expand (⤢) button appears while the code is
|
|
46
|
+
* height-capped (exceeds `maxLines`). Lets a host open it full-screen. */
|
|
47
|
+
onExpand?: () => void;
|
|
48
|
+
/**
|
|
49
|
+
* Visual variant:
|
|
50
|
+
* - ``"card"`` (default) — full chrome: border, background, hover
|
|
51
|
+
* toolbar (Copy + language tag), optional internal scroll. Suits
|
|
52
|
+
* a standalone code block in documentation prose where the block
|
|
53
|
+
* is its own surface.
|
|
54
|
+
* - ``"plain"`` — chrome-less: no border, no background, no toolbar,
|
|
55
|
+
* no internal scroll. Grows to fit its content. Use when
|
|
56
|
+
* embedding inside another scroll container (e.g. the Response
|
|
57
|
+
* panel) — avoids double-scroll surfaces and lets the parent
|
|
58
|
+
* manage copy/language affordances.
|
|
59
|
+
*/
|
|
60
|
+
variant?: 'card' | 'plain';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const PrettyCode: React.FC<PrettyCodeProps> = (props) => {
|
|
64
|
+
return (
|
|
65
|
+
<Suspense fallback={<CodeFallback />}>
|
|
66
|
+
<PrettyCodeClient {...props} />
|
|
67
|
+
</Suspense>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default PrettyCode;
|
|
72
|
+
export type { Language };
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lazy-loaded PrettyCode Component
|
|
5
|
+
*
|
|
6
|
+
* Heavy Prism library (~500KB) is loaded only when component is rendered.
|
|
7
|
+
* Use this for automatic code-splitting with Suspense fallback.
|
|
8
|
+
*
|
|
9
|
+
* For direct imports without lazy loading, use:
|
|
10
|
+
* import PrettyCode from '@djangocfg/widget-code/pretty-code'
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { createLazyComponent, LoadingFallback } from '@djangocfg/widget-kit/lazy';
|
|
14
|
+
import type { Language } from 'prism-react-renderer';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export interface PrettyCodeProps {
|
|
21
|
+
data: string | object;
|
|
22
|
+
language: Language;
|
|
23
|
+
className?: string;
|
|
24
|
+
mode?: 'dark' | 'light';
|
|
25
|
+
inline?: boolean;
|
|
26
|
+
customBg?: string;
|
|
27
|
+
isCompact?: boolean;
|
|
28
|
+
scrollIsolation?: boolean;
|
|
29
|
+
maxLines?: number;
|
|
30
|
+
/** Header expand (⤢) callback, shown while the code is height-capped. */
|
|
31
|
+
onExpand?: () => void;
|
|
32
|
+
/** ``'card'`` (default) ships the full chrome. ``'plain'`` is
|
|
33
|
+
* chrome-less — for embedding inside another scroll container. */
|
|
34
|
+
variant?: 'card' | 'plain';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type { Language };
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Lazy Component
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* LazyPrettyCode - Lazy-loaded code syntax highlighter
|
|
45
|
+
*
|
|
46
|
+
* Automatically shows loading state while Prism loads (~500KB). Uses the shared
|
|
47
|
+
* compact `LoadingFallback` (16px muted spinner) so the code viewer and the JSON
|
|
48
|
+
* viewer show the SAME quiet placeholder while their chunks load — kept inside a
|
|
49
|
+
* subtle code-surface border so nothing shifts when PrettyCode mounts.
|
|
50
|
+
*/
|
|
51
|
+
export const LazyPrettyCode = createLazyComponent<PrettyCodeProps>(
|
|
52
|
+
() => import('./PrettyCode.client'),
|
|
53
|
+
{
|
|
54
|
+
displayName: 'LazyPrettyCode',
|
|
55
|
+
fallback: (
|
|
56
|
+
<LoadingFallback
|
|
57
|
+
minHeight={64}
|
|
58
|
+
text="Loading code…"
|
|
59
|
+
className="rounded-sm border border-border bg-muted dark:bg-code"
|
|
60
|
+
/>
|
|
61
|
+
),
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// `PrettyCode` is the historical named export — same component as
|
|
66
|
+
// `LazyPrettyCode`, kept for backwards compatibility with callers that
|
|
67
|
+
// imported it from the old root barrel.
|
|
68
|
+
export { LazyPrettyCode as PrettyCode };
|
|
69
|
+
export { default } from './index';
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react-vite';
|
|
2
|
+
import { LazyPrettyCode as PrettyCode } from './lazy';
|
|
3
|
+
|
|
4
|
+
// ─── Samples ──────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const TS_SAMPLE = `import { useCallback, useEffect, useRef, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
export type Status = 'idle' | 'loading' | 'success' | 'error';
|
|
9
|
+
|
|
10
|
+
export interface PaginationParams {
|
|
11
|
+
page: number;
|
|
12
|
+
pageSize: number;
|
|
13
|
+
sortBy?: string;
|
|
14
|
+
sortOrder?: 'asc' | 'desc';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useVehicles(initialParams?: Partial<PaginationParams>) {
|
|
18
|
+
const [status, setStatus] = useState<Status>('idle');
|
|
19
|
+
const [data, setData] = useState<unknown[]>([]);
|
|
20
|
+
const abortRef = useRef<AbortController | null>(null);
|
|
21
|
+
|
|
22
|
+
const fetch = useCallback(async (p: PaginationParams) => {
|
|
23
|
+
abortRef.current?.abort();
|
|
24
|
+
abortRef.current = new AbortController();
|
|
25
|
+
setStatus('loading');
|
|
26
|
+
const res = await window.fetch(\`/api/vehicles?page=\${p.page}\`, {
|
|
27
|
+
signal: abortRef.current.signal,
|
|
28
|
+
});
|
|
29
|
+
const json = await res.json();
|
|
30
|
+
setData(json.data);
|
|
31
|
+
setStatus('success');
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
fetch(initialParams as PaginationParams);
|
|
36
|
+
return () => abortRef.current?.abort();
|
|
37
|
+
}, []);
|
|
38
|
+
|
|
39
|
+
return { status, data };
|
|
40
|
+
}`;
|
|
41
|
+
|
|
42
|
+
const JSON_SAMPLE = {
|
|
43
|
+
framework: 'next',
|
|
44
|
+
version: '16.2.2',
|
|
45
|
+
features: ['app-router', 'rsc', 'turbopack'],
|
|
46
|
+
flags: { strictMode: true, experimentalRSC: true },
|
|
47
|
+
routes: [
|
|
48
|
+
{ path: '/', component: 'HomePage' },
|
|
49
|
+
{ path: '/about', component: 'AboutPage' },
|
|
50
|
+
],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const PYTHON_SAMPLE = `from __future__ import annotations
|
|
54
|
+
|
|
55
|
+
import asyncio
|
|
56
|
+
from dataclasses import dataclass
|
|
57
|
+
from datetime import datetime
|
|
58
|
+
from enum import StrEnum
|
|
59
|
+
|
|
60
|
+
import httpx
|
|
61
|
+
from pydantic import BaseModel, Field, field_validator
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class FuelType(StrEnum):
|
|
65
|
+
GASOLINE = "gasoline"
|
|
66
|
+
DIESEL = "diesel"
|
|
67
|
+
ELECTRIC = "electric"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class Vehicle(BaseModel):
|
|
71
|
+
id: str
|
|
72
|
+
make: str
|
|
73
|
+
model: str
|
|
74
|
+
year: int
|
|
75
|
+
fuel_type: FuelType
|
|
76
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
77
|
+
|
|
78
|
+
@field_validator("year")
|
|
79
|
+
@classmethod
|
|
80
|
+
def validate_year(cls, v: int) -> int:
|
|
81
|
+
if not (1900 <= v <= datetime.utcnow().year + 1):
|
|
82
|
+
raise ValueError(f"Invalid year: {v}")
|
|
83
|
+
return v
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class VehicleRepository:
|
|
88
|
+
base_url: str
|
|
89
|
+
|
|
90
|
+
async def list(self, page: int = 1) -> list[Vehicle]:
|
|
91
|
+
async with httpx.AsyncClient(base_url=self.base_url) as client:
|
|
92
|
+
response = await client.get("/vehicles", params={"page": page})
|
|
93
|
+
response.raise_for_status()
|
|
94
|
+
return [Vehicle.model_validate(v) for v in response.json()["data"]]`;
|
|
95
|
+
|
|
96
|
+
const BASH_SAMPLE = `#!/usr/bin/env bash
|
|
97
|
+
set -euo pipefail
|
|
98
|
+
|
|
99
|
+
# Deploy script for production
|
|
100
|
+
ENV="\${1:-staging}"
|
|
101
|
+
TAG="\${2:-latest}"
|
|
102
|
+
|
|
103
|
+
echo "Deploying to \$ENV with tag \$TAG"
|
|
104
|
+
|
|
105
|
+
docker build -t myapp:\$TAG .
|
|
106
|
+
docker push registry.example.com/myapp:\$TAG
|
|
107
|
+
|
|
108
|
+
kubectl set image deployment/myapp app=registry.example.com/myapp:\$TAG
|
|
109
|
+
kubectl rollout status deployment/myapp --timeout=2m
|
|
110
|
+
|
|
111
|
+
echo "Done."`;
|
|
112
|
+
|
|
113
|
+
const SQL_SAMPLE = `-- Top selling vehicles in the last 30 days
|
|
114
|
+
WITH recent_sales AS (
|
|
115
|
+
SELECT
|
|
116
|
+
v.id,
|
|
117
|
+
v.make,
|
|
118
|
+
v.model,
|
|
119
|
+
COUNT(s.id) AS sales_count,
|
|
120
|
+
SUM(s.amount) AS revenue
|
|
121
|
+
FROM vehicles v
|
|
122
|
+
INNER JOIN sales s ON s.vehicle_id = v.id
|
|
123
|
+
WHERE s.created_at >= NOW() - INTERVAL '30 days'
|
|
124
|
+
GROUP BY v.id, v.make, v.model
|
|
125
|
+
)
|
|
126
|
+
SELECT *
|
|
127
|
+
FROM recent_sales
|
|
128
|
+
WHERE sales_count > 5
|
|
129
|
+
ORDER BY revenue DESC
|
|
130
|
+
LIMIT 20;`;
|
|
131
|
+
|
|
132
|
+
const YAML_SAMPLE = `version: '3.9'
|
|
133
|
+
|
|
134
|
+
services:
|
|
135
|
+
api:
|
|
136
|
+
image: registry.example.com/myapp:latest
|
|
137
|
+
environment:
|
|
138
|
+
DATABASE_URL: postgres://db/app
|
|
139
|
+
REDIS_URL: redis://cache:6379
|
|
140
|
+
depends_on:
|
|
141
|
+
- db
|
|
142
|
+
- cache
|
|
143
|
+
ports:
|
|
144
|
+
- "8000:8000"
|
|
145
|
+
|
|
146
|
+
db:
|
|
147
|
+
image: postgres:16-alpine
|
|
148
|
+
volumes:
|
|
149
|
+
- db_data:/var/lib/postgresql/data
|
|
150
|
+
environment:
|
|
151
|
+
POSTGRES_DB: app
|
|
152
|
+
POSTGRES_PASSWORD: secret
|
|
153
|
+
|
|
154
|
+
cache:
|
|
155
|
+
image: redis:7-alpine
|
|
156
|
+
|
|
157
|
+
volumes:
|
|
158
|
+
db_data:`;
|
|
159
|
+
|
|
160
|
+
const LONG_SAMPLE = Array.from(
|
|
161
|
+
{ length: 60 },
|
|
162
|
+
(_, i) => `const value_${i} = ${i * 3}; // computed entry ${i + 1}`,
|
|
163
|
+
).join('\n');
|
|
164
|
+
|
|
165
|
+
// ─── Meta ─────────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
const meta = {
|
|
168
|
+
title: 'Widgets/Code/PrettyCode',
|
|
169
|
+
component: PrettyCode,
|
|
170
|
+
tags: ['autodocs'],
|
|
171
|
+
argTypes: {
|
|
172
|
+
data: { control: 'text', table: { category: 'Content' } },
|
|
173
|
+
language: {
|
|
174
|
+
control: 'select',
|
|
175
|
+
options: [
|
|
176
|
+
'tsx',
|
|
177
|
+
'ts',
|
|
178
|
+
'js',
|
|
179
|
+
'jsx',
|
|
180
|
+
'json',
|
|
181
|
+
'python',
|
|
182
|
+
'bash',
|
|
183
|
+
'sql',
|
|
184
|
+
'yaml',
|
|
185
|
+
'css',
|
|
186
|
+
'html',
|
|
187
|
+
'go',
|
|
188
|
+
'rust',
|
|
189
|
+
'markdown',
|
|
190
|
+
],
|
|
191
|
+
table: { category: 'Content' },
|
|
192
|
+
},
|
|
193
|
+
mode: {
|
|
194
|
+
control: 'inline-radio',
|
|
195
|
+
options: ['dark', 'light'],
|
|
196
|
+
table: { category: 'Appearance' },
|
|
197
|
+
},
|
|
198
|
+
variant: {
|
|
199
|
+
control: 'inline-radio',
|
|
200
|
+
options: ['card', 'plain'],
|
|
201
|
+
table: { category: 'Appearance' },
|
|
202
|
+
},
|
|
203
|
+
inline: { control: 'boolean', table: { category: 'Appearance' } },
|
|
204
|
+
isCompact: { control: 'boolean', table: { category: 'Appearance' } },
|
|
205
|
+
customBg: { control: 'text', table: { category: 'Appearance' } },
|
|
206
|
+
scrollIsolation: { control: 'boolean', table: { category: 'Behaviour' } },
|
|
207
|
+
maxLines: { control: { type: 'number', min: 0, step: 1 }, table: { category: 'Behaviour' } },
|
|
208
|
+
},
|
|
209
|
+
} satisfies Meta<typeof PrettyCode>;
|
|
210
|
+
|
|
211
|
+
export default meta;
|
|
212
|
+
|
|
213
|
+
type Story = StoryObj<typeof meta>;
|
|
214
|
+
|
|
215
|
+
const Frame = ({ children }: { children: React.ReactNode }) => (
|
|
216
|
+
<div className="w-[720px]">{children}</div>
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// ─── Playground ───────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
export const Playground: Story = {
|
|
222
|
+
args: {
|
|
223
|
+
data: TS_SAMPLE,
|
|
224
|
+
language: 'tsx',
|
|
225
|
+
mode: 'dark',
|
|
226
|
+
variant: 'card',
|
|
227
|
+
inline: false,
|
|
228
|
+
isCompact: false,
|
|
229
|
+
scrollIsolation: true,
|
|
230
|
+
},
|
|
231
|
+
render: (args) => (
|
|
232
|
+
<Frame>
|
|
233
|
+
<PrettyCode {...args} />
|
|
234
|
+
</Frame>
|
|
235
|
+
),
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// ─── Curated presets ──────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
export const TypeScript: Story = {
|
|
241
|
+
args: { data: TS_SAMPLE, language: 'tsx' },
|
|
242
|
+
render: (args) => (
|
|
243
|
+
<Frame>
|
|
244
|
+
<PrettyCode {...args} />
|
|
245
|
+
</Frame>
|
|
246
|
+
),
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
export const Json: Story = {
|
|
250
|
+
args: { data: JSON_SAMPLE, language: 'json' },
|
|
251
|
+
render: (args) => (
|
|
252
|
+
<Frame>
|
|
253
|
+
<PrettyCode {...args} />
|
|
254
|
+
</Frame>
|
|
255
|
+
),
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
export const Python: Story = {
|
|
259
|
+
args: { data: PYTHON_SAMPLE, language: 'python' },
|
|
260
|
+
render: (args) => (
|
|
261
|
+
<Frame>
|
|
262
|
+
<PrettyCode {...args} />
|
|
263
|
+
</Frame>
|
|
264
|
+
),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export const Bash: Story = {
|
|
268
|
+
args: { data: BASH_SAMPLE, language: 'bash' },
|
|
269
|
+
render: (args) => (
|
|
270
|
+
<Frame>
|
|
271
|
+
<PrettyCode {...args} />
|
|
272
|
+
</Frame>
|
|
273
|
+
),
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
export const Sql: Story = {
|
|
277
|
+
args: { data: SQL_SAMPLE, language: 'sql' },
|
|
278
|
+
render: (args) => (
|
|
279
|
+
<Frame>
|
|
280
|
+
<PrettyCode {...args} />
|
|
281
|
+
</Frame>
|
|
282
|
+
),
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export const Yaml: Story = {
|
|
286
|
+
args: { data: YAML_SAMPLE, language: 'yaml' },
|
|
287
|
+
render: (args) => (
|
|
288
|
+
<Frame>
|
|
289
|
+
<PrettyCode {...args} />
|
|
290
|
+
</Frame>
|
|
291
|
+
),
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
export const LongFileWithScroll: Story = {
|
|
295
|
+
args: {
|
|
296
|
+
data: LONG_SAMPLE,
|
|
297
|
+
language: 'ts',
|
|
298
|
+
maxLines: 20,
|
|
299
|
+
},
|
|
300
|
+
render: (args) => (
|
|
301
|
+
<Frame>
|
|
302
|
+
<PrettyCode {...args} />
|
|
303
|
+
</Frame>
|
|
304
|
+
),
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
export const Inline: Story = {
|
|
308
|
+
args: {
|
|
309
|
+
data: `const status: 'ok' | 'err' = 'ok';`,
|
|
310
|
+
language: 'ts',
|
|
311
|
+
inline: true,
|
|
312
|
+
},
|
|
313
|
+
render: (args) => (
|
|
314
|
+
<div className="text-sm">
|
|
315
|
+
The variable <PrettyCode {...args} /> represents the result state.
|
|
316
|
+
</div>
|
|
317
|
+
),
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const Compact: Story = {
|
|
321
|
+
args: {
|
|
322
|
+
data: `const port = 3000;`,
|
|
323
|
+
language: 'ts',
|
|
324
|
+
isCompact: true,
|
|
325
|
+
},
|
|
326
|
+
render: (args) => <PrettyCode {...args} />,
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export const LightMode: Story = {
|
|
330
|
+
args: {
|
|
331
|
+
data: TS_SAMPLE,
|
|
332
|
+
language: 'tsx',
|
|
333
|
+
mode: 'light',
|
|
334
|
+
},
|
|
335
|
+
render: (args) => (
|
|
336
|
+
<Frame>
|
|
337
|
+
<PrettyCode {...args} />
|
|
338
|
+
</Frame>
|
|
339
|
+
),
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
export const PlainVariant: Story = {
|
|
343
|
+
args: {
|
|
344
|
+
data: TS_SAMPLE,
|
|
345
|
+
language: 'tsx',
|
|
346
|
+
variant: 'plain',
|
|
347
|
+
},
|
|
348
|
+
render: (args) => (
|
|
349
|
+
<Frame>
|
|
350
|
+
<PrettyCode {...args} />
|
|
351
|
+
</Frame>
|
|
352
|
+
),
|
|
353
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register extra Prism language grammars onto the vendored Prism
|
|
3
|
+
* instance that ``prism-react-renderer`` ships.
|
|
4
|
+
*
|
|
5
|
+
* Why this file exists:
|
|
6
|
+
* ``prism-react-renderer@2.x`` bundles only a small set of core
|
|
7
|
+
* languages (javascript, typescript, python, go, json, css,
|
|
8
|
+
* markup, clike, …). Anything else — ``bash``, ``ruby``, ``java``,
|
|
9
|
+
* ``php`` — isn't in the bundle, so passing ``language="bash"`` to
|
|
10
|
+
* ``<Highlight>`` silently renders as plain text.
|
|
11
|
+
*
|
|
12
|
+
* Canonical solution (per the official README):
|
|
13
|
+
* - Grab the exported ``Prism`` instance.
|
|
14
|
+
* - Hang it off ``globalThis`` so that ``prismjs`` language
|
|
15
|
+
* components can find it when they self-register (they read
|
|
16
|
+
* ``window.Prism`` / ``global.Prism`` at module-evaluation time).
|
|
17
|
+
* - Load each grammar via dynamic ``import()`` — bare ``require``
|
|
18
|
+
* doesn't work in ESM/Vite and static ``import 'prismjs/…'``
|
|
19
|
+
* runs before we can hang Prism on the global.
|
|
20
|
+
*
|
|
21
|
+
* Runs once on module load, with a guard so repeated imports in
|
|
22
|
+
* different bundles are a no-op. Safe for SSR: the grammars don't
|
|
23
|
+
* touch the DOM and the dynamic import is awaited asynchronously —
|
|
24
|
+
* server renders just miss the extra grammars, client renders pick
|
|
25
|
+
* them up after hydration which is when the <Highlight> components
|
|
26
|
+
* actually render anyway.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
30
|
+
|
|
31
|
+
import { useSyncExternalStore } from 'react';
|
|
32
|
+
import { Prism } from 'prism-react-renderer';
|
|
33
|
+
|
|
34
|
+
// Hang the vendored Prism on the global so ``prismjs`` language
|
|
35
|
+
// components can find it when their self-registration runs.
|
|
36
|
+
const globalScope: any = typeof globalThis !== 'undefined' ? globalThis : {};
|
|
37
|
+
if (!globalScope.Prism) {
|
|
38
|
+
globalScope.Prism = Prism;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Ready-state machinery: one-shot boolean + subscribers. When loading
|
|
42
|
+
// finishes we flip ``ready`` and notify listeners so components that
|
|
43
|
+
// rendered before the grammars loaded can re-render and pick them up.
|
|
44
|
+
let ready = false;
|
|
45
|
+
let loading: Promise<void> | null = null;
|
|
46
|
+
const listeners = new Set<() => void>();
|
|
47
|
+
|
|
48
|
+
function subscribe(cb: () => void): () => void {
|
|
49
|
+
listeners.add(cb);
|
|
50
|
+
return () => listeners.delete(cb);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getSnapshot(): boolean {
|
|
54
|
+
return ready;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Server snapshot — always ``true`` so SSR doesn't trip the
|
|
58
|
+
// ``server ≠ client`` warning. The actual registration only runs on
|
|
59
|
+
// the client; server renders fall back to the bundled Prism set,
|
|
60
|
+
// which for the OpenAPI viewer means JS / Python / Go highlight
|
|
61
|
+
// server-side and bash / java / ruby / php render plain until hydration.
|
|
62
|
+
function getServerSnapshot(): boolean {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function ensurePrismLanguages(): Promise<void> {
|
|
67
|
+
if (ready) return;
|
|
68
|
+
if (loading) return loading;
|
|
69
|
+
|
|
70
|
+
loading = (async () => {
|
|
71
|
+
// Dynamic imports so these modules evaluate AFTER we've
|
|
72
|
+
// attached Prism to the global scope above.
|
|
73
|
+
//
|
|
74
|
+
// `as string` because the grammar files are untyped side-effect
|
|
75
|
+
// modules — `prismjs` ships no declarations for `components/*` and
|
|
76
|
+
// there is no `@types/prismjs` here. It widens the specifier, not a
|
|
77
|
+
// value, so nothing about the loaded module is being asserted.
|
|
78
|
+
//
|
|
79
|
+
// Load order matters: ``prism-php`` calls
|
|
80
|
+
// ``Prism.languages.markup.tokenizePlaceholders`` at module
|
|
81
|
+
// evaluation time, which is only defined after
|
|
82
|
+
// ``markup-templating`` loads — so that must come first.
|
|
83
|
+
// ``bash`` / ``java`` / ``ruby`` only depend on ``clike``
|
|
84
|
+
// which is already bundled in ``prism-react-renderer``.
|
|
85
|
+
await import('prismjs/components/prism-markup-templating' as string);
|
|
86
|
+
await Promise.all([
|
|
87
|
+
import('prismjs/components/prism-bash' as string),
|
|
88
|
+
import('prismjs/components/prism-java' as string),
|
|
89
|
+
import('prismjs/components/prism-ruby' as string),
|
|
90
|
+
import('prismjs/components/prism-php' as string),
|
|
91
|
+
]);
|
|
92
|
+
ready = true;
|
|
93
|
+
listeners.forEach((cb) => cb());
|
|
94
|
+
})();
|
|
95
|
+
|
|
96
|
+
return loading;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Hook that triggers a re-render when the extra grammars finish
|
|
100
|
+
* loading. Used by ``PrettyCode`` so the first render (which might
|
|
101
|
+
* happen before loading completes) gets re-highlighted once the
|
|
102
|
+
* grammars are available. */
|
|
103
|
+
export function useEnsurePrismLanguages(): boolean {
|
|
104
|
+
const loaded = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
105
|
+
if (!loaded) {
|
|
106
|
+
// Kick off loading (idempotent — re-entry is cheap) so the
|
|
107
|
+
// next store update flips ``ready`` and we re-render.
|
|
108
|
+
void ensurePrismLanguages();
|
|
109
|
+
}
|
|
110
|
+
return loaded;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Also kick off on module load so the grammars start loading as
|
|
114
|
+
// early as possible — by the time the user clicks a code-sample tab
|
|
115
|
+
// they've usually finished.
|
|
116
|
+
void ensurePrismLanguages();
|