@djangocfg/ui-tools 2.1.289 → 2.1.291

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.
Files changed (98) hide show
  1. package/README.md +14 -3
  2. package/dist/{DocsLayout-YDR7DSMM.cjs → DocsLayout-IKH7BLSU.cjs} +1537 -682
  3. package/dist/DocsLayout-IKH7BLSU.cjs.map +1 -0
  4. package/dist/{DocsLayout-TKJQ5W5E.mjs → DocsLayout-JPXFUKAR.mjs} +1429 -574
  5. package/dist/DocsLayout-JPXFUKAR.mjs.map +1 -0
  6. package/dist/{PrettyCode.client-5GABIN2I.cjs → PrettyCode.client-RPDIE5CH.cjs} +104 -3
  7. package/dist/PrettyCode.client-RPDIE5CH.cjs.map +1 -0
  8. package/dist/{PrettyCode.client-IZTXXYHG.mjs → PrettyCode.client-SPMTQEG4.mjs} +106 -5
  9. package/dist/PrettyCode.client-SPMTQEG4.mjs.map +1 -0
  10. package/dist/{chunk-IULI4XII.cjs → chunk-5Q4UMSWB.cjs} +355 -9
  11. package/dist/chunk-5Q4UMSWB.cjs.map +1 -0
  12. package/dist/{chunk-VZGQC3NG.mjs → chunk-EFWOJPA6.mjs} +349 -9
  13. package/dist/chunk-EFWOJPA6.mjs.map +1 -0
  14. package/dist/index.cjs +18 -10
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +35 -1
  17. package/dist/index.d.ts +35 -1
  18. package/dist/index.mjs +13 -5
  19. package/dist/index.mjs.map +1 -1
  20. package/package.json +20 -15
  21. package/src/components/markdown/MarkdownMessage.tsx +46 -0
  22. package/src/tools/MarkdownEditor/MarkdownEditor.tsx +42 -1
  23. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +87 -178
  24. package/src/tools/OpenapiViewer/README.md +114 -6
  25. package/src/tools/OpenapiViewer/components/DocsLayout/ApiIntroSection.tsx +20 -6
  26. package/src/tools/OpenapiViewer/components/DocsLayout/DocsView.tsx +6 -0
  27. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/LanguageTabs.tsx +36 -0
  28. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/index.tsx +56 -0
  29. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/CodeSamples/useCodeSnippet.ts +77 -0
  30. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MetaActions.tsx +146 -0
  31. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/MethodBadge.tsx +6 -0
  32. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/PathDisplay.tsx +26 -0
  33. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Header/index.tsx +87 -0
  34. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamGroup.tsx +30 -0
  35. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/ParamRow.tsx +36 -0
  36. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Parameters/index.tsx +22 -0
  37. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/RequestBody/index.tsx +33 -0
  38. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +76 -0
  39. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseRow.tsx +80 -0
  40. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/StatusTag.tsx +32 -0
  41. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/index.tsx +21 -0
  42. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/FieldRow.tsx +106 -0
  43. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/buildTree.ts +127 -0
  44. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/index.tsx +31 -0
  45. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/SchemaFields/types.ts +28 -0
  46. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/SectionHeader.tsx +87 -0
  47. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/defaults.ts +27 -0
  48. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/Section/index.tsx +45 -0
  49. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/context.tsx +56 -0
  50. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/hooks/useSectionHash.ts +63 -0
  51. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/index.tsx +96 -0
  52. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/index.ts +133 -0
  53. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/store/selectors.ts +40 -0
  54. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc/types.ts +17 -0
  55. package/src/tools/OpenapiViewer/components/DocsLayout/SchemaCopyMenu.tsx +8 -2
  56. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/BrandHeader.tsx +48 -0
  57. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/CategoryBlock.tsx +33 -0
  58. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/EndpointRow.tsx +73 -0
  59. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/MethodChips.tsx +43 -0
  60. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SchemaSection.tsx +27 -0
  61. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SearchInput.tsx +45 -0
  62. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/SidebarBody.tsx +50 -0
  63. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/Toolbar.tsx +64 -0
  64. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/buildVM.ts +126 -0
  65. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/index.tsx +112 -0
  66. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/types.ts +42 -0
  67. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar/useDebouncedValue.ts +14 -0
  68. package/src/tools/OpenapiViewer/components/DocsLayout/SlideInPlayground.tsx +10 -7
  69. package/src/tools/OpenapiViewer/components/DocsLayout/TryItSheet.tsx +9 -6
  70. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +55 -0
  71. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/PreviewView.tsx +115 -0
  72. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/RawView.tsx +24 -0
  73. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/StatusBar.tsx +63 -0
  74. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/ViewTabs.tsx +45 -0
  75. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/detectContent.ts +97 -0
  76. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/index.tsx +93 -0
  77. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/types.ts +26 -0
  78. package/src/tools/OpenapiViewer/components/shared/ResponsePanel/useResponseView.ts +62 -0
  79. package/src/tools/OpenapiViewer/hooks/useOpenApiSchema.ts +41 -71
  80. package/src/tools/OpenapiViewer/types.ts +10 -0
  81. package/src/tools/OpenapiViewer/utils/codeSamples.ts +287 -0
  82. package/src/tools/OpenapiViewer/utils/index.ts +3 -0
  83. package/src/tools/OpenapiViewer/utils/operationToHar.ts +119 -0
  84. package/src/tools/OpenapiViewer/utils/sampler.ts +72 -0
  85. package/src/tools/PrettyCode/PrettyCode.client.tsx +88 -1
  86. package/src/tools/PrettyCode/PrettyCode.story.tsx +114 -361
  87. package/src/tools/PrettyCode/index.tsx +13 -0
  88. package/src/tools/PrettyCode/lazy.tsx +5 -0
  89. package/src/tools/PrettyCode/registerPrismLanguages.ts +111 -0
  90. package/dist/DocsLayout-TKJQ5W5E.mjs.map +0 -1
  91. package/dist/DocsLayout-YDR7DSMM.cjs.map +0 -1
  92. package/dist/PrettyCode.client-5GABIN2I.cjs.map +0 -1
  93. package/dist/PrettyCode.client-IZTXXYHG.mjs.map +0 -1
  94. package/dist/chunk-IULI4XII.cjs.map +0 -1
  95. package/dist/chunk-VZGQC3NG.mjs.map +0 -1
  96. package/src/tools/OpenapiViewer/components/DocsLayout/EndpointDoc.tsx +0 -273
  97. package/src/tools/OpenapiViewer/components/DocsLayout/Sidebar.tsx +0 -439
  98. package/src/tools/OpenapiViewer/components/shared/ResponsePanel.tsx +0 -127
@@ -7,11 +7,11 @@ export default defineStory({
7
7
  description: 'Syntax highlighted code block with copy button.',
8
8
  });
9
9
 
10
+ // ─── Samples ──────────────────────────────────────────────────────────────────
11
+
10
12
  const LONG_CODE_SAMPLES = {
11
13
  typescript: `import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
12
14
 
13
- // ─── Types ────────────────────────────────────────────────────────────────────
14
-
15
15
  export type Status = 'idle' | 'loading' | 'success' | 'error';
16
16
 
17
17
  export interface PaginationParams {
@@ -21,70 +21,33 @@ export interface PaginationParams {
21
21
  sortOrder?: 'asc' | 'desc';
22
22
  }
23
23
 
24
- export interface ApiResponse<T> {
25
- data: T[];
26
- total: number;
27
- page: number;
28
- pageSize: number;
29
- hasMore: boolean;
30
- }
31
-
32
- export interface Vehicle {
33
- id: string;
34
- make: string;
35
- model: string;
36
- year: number;
37
- price: number;
38
- mileage: number;
39
- fuelType: 'gasoline' | 'diesel' | 'electric' | 'hybrid';
40
- transmission: 'manual' | 'automatic';
41
- color: string;
42
- vin: string;
43
- createdAt: Date;
44
- updatedAt: Date;
45
- }
46
-
47
- // ─── Hook ─────────────────────────────────────────────────────────────────────
48
-
49
24
  export function useVehicles(initialParams?: Partial<PaginationParams>) {
50
25
  const [status, setStatus] = useState<Status>('idle');
51
- const [data, setData] = useState<Vehicle[]>([]);
52
- const [total, setTotal] = useState(0);
26
+ const [data, setData] = useState<unknown[]>([]);
53
27
  const [error, setError] = useState<Error | null>(null);
54
28
  const abortRef = useRef<AbortController | null>(null);
55
29
 
56
30
  const [params, setParams] = useState<PaginationParams>({
57
31
  page: 1,
58
32
  pageSize: 20,
59
- sortBy: 'createdAt',
60
- sortOrder: 'desc',
61
33
  ...initialParams,
62
34
  });
63
35
 
64
36
  const fetch = useCallback(async (p: PaginationParams) => {
65
37
  abortRef.current?.abort();
66
38
  abortRef.current = new AbortController();
67
-
68
39
  setStatus('loading');
69
- setError(null);
70
-
71
40
  try {
72
41
  const query = new URLSearchParams({
73
42
  page: String(p.page),
74
43
  pageSize: String(p.pageSize),
75
- ...(p.sortBy ? { sortBy: p.sortBy } : {}),
76
- ...(p.sortOrder ? { sortOrder: p.sortOrder } : {}),
77
44
  });
78
-
79
45
  const res = await window.fetch(\`/api/vehicles?\${query}\`, {
80
46
  signal: abortRef.current.signal,
81
47
  });
82
-
83
- if (!res.ok) throw new Error(\`HTTP \${res.status}: \${res.statusText}\`);
84
-
85
- const json: ApiResponse<Vehicle> = await res.json();
48
+ if (!res.ok) throw new Error(\`HTTP \${res.status}\`);
49
+ const json = await res.json();
86
50
  setData(json.data);
87
- setTotal(json.total);
88
51
  setStatus('success');
89
52
  } catch (err) {
90
53
  if ((err as Error).name === 'AbortError') return;
@@ -98,21 +61,7 @@ export function useVehicles(initialParams?: Partial<PaginationParams>) {
98
61
  return () => abortRef.current?.abort();
99
62
  }, [params, fetch]);
100
63
 
101
- const hasMore = useMemo(
102
- () => params.page * params.pageSize < total,
103
- [params.page, params.pageSize, total],
104
- );
105
-
106
- const nextPage = useCallback(() =>
107
- setParams(p => ({ ...p, page: p.page + 1 })), []);
108
-
109
- const prevPage = useCallback(() =>
110
- setParams(p => ({ ...p, page: Math.max(1, p.page - 1) })), []);
111
-
112
- const sort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc' = 'asc') =>
113
- setParams(p => ({ ...p, sortBy, sortOrder, page: 1 })), []);
114
-
115
- return { status, data, total, error, params, hasMore, nextPage, prevPage, sort };
64
+ return { status, data, error, params, setParams };
116
65
  }`,
117
66
 
118
67
  python: `from __future__ import annotations
@@ -122,42 +71,27 @@ import logging
122
71
  from dataclasses import dataclass, field
123
72
  from datetime import datetime
124
73
  from enum import StrEnum
125
- from typing import Any, AsyncIterator, Generic, TypeVar
126
- from uuid import UUID, uuid4
127
74
 
128
75
  import httpx
129
76
  from pydantic import BaseModel, ConfigDict, Field, field_validator
130
77
 
131
78
  logger = logging.getLogger(__name__)
132
79
 
133
- T = TypeVar("T")
134
-
135
- # ─── Enums ────────────────────────────────────────────────────────────────────
136
80
 
137
81
  class FuelType(StrEnum):
138
82
  GASOLINE = "gasoline"
139
83
  DIESEL = "diesel"
140
84
  ELECTRIC = "electric"
141
- HYBRID = "hybrid"
142
-
143
- class Transmission(StrEnum):
144
- MANUAL = "manual"
145
- AUTOMATIC = "automatic"
146
85
 
147
- # ─── Models ───────────────────────────────────────────────────────────────────
148
86
 
149
87
  class Vehicle(BaseModel):
150
88
  model_config = ConfigDict(from_attributes=True)
151
89
 
152
- id: UUID = Field(default_factory=uuid4)
90
+ id: str
153
91
  make: str
154
92
  model: str
155
93
  year: int
156
- price: float
157
- mileage: int
158
94
  fuel_type: FuelType
159
- transmission: Transmission
160
- vin: str
161
95
  created_at: datetime = Field(default_factory=datetime.utcnow)
162
96
 
163
97
  @field_validator("year")
@@ -167,93 +101,25 @@ class Vehicle(BaseModel):
167
101
  raise ValueError(f"Invalid year: {v}")
168
102
  return v
169
103
 
170
- @field_validator("vin")
171
- @classmethod
172
- def validate_vin(cls, v: str) -> str:
173
- if len(v) != 17:
174
- raise ValueError("VIN must be exactly 17 characters")
175
- return v.upper()
176
-
177
- class Page(BaseModel, Generic[T]):
178
- data: list[T]
179
- total: int
180
- page: int
181
- page_size: int
182
-
183
- @property
184
- def has_more(self) -> bool:
185
- return self.page * self.page_size < self.total
186
-
187
- # ─── Repository ───────────────────────────────────────────────────────────────
188
104
 
189
105
  @dataclass
190
106
  class VehicleRepository:
191
107
  base_url: str
192
- client: httpx.AsyncClient = field(init=False)
193
-
194
- def __post_init__(self) -> None:
195
- self.client = httpx.AsyncClient(
196
- base_url=self.base_url,
197
- timeout=httpx.Timeout(10.0),
198
- headers={"Accept": "application/json"},
199
- )
200
-
201
- async def list(
202
- self,
203
- page: int = 1,
204
- page_size: int = 20,
205
- sort_by: str = "created_at",
206
- sort_order: str = "desc",
207
- ) -> Page[Vehicle]:
208
- response = await self.client.get(
209
- "/vehicles",
210
- params={
211
- "page": page,
212
- "page_size": page_size,
213
- "sort_by": sort_by,
214
- "sort_order": sort_order,
215
- },
216
- )
217
- response.raise_for_status()
218
- payload = response.json()
219
- return Page[Vehicle](
220
- data=[Vehicle.model_validate(v) for v in payload["data"]],
221
- total=payload["total"],
222
- page=payload["page"],
223
- page_size=payload["page_size"],
224
- )
225
-
226
- async def stream_all(self) -> AsyncIterator[Vehicle]:
227
- page = 1
228
- while True:
229
- result = await self.list(page=page, page_size=100)
230
- for vehicle in result.data:
231
- yield vehicle
232
- if not result.has_more:
233
- break
234
- page += 1
235
-
236
- async def close(self) -> None:
237
- await self.client.aclose()`,
238
-
239
- javascript: `// ─── Event Bus ────────────────────────────────────────────────────────────────
240
-
241
- /**
242
- * Tiny type-safe event bus with wildcard support and once() semantics.
243
- * Zero dependencies, works in browser and Node.js.
244
- */
245
-
246
- /** @template {Record<string, unknown>} Events */
247
- class EventBus {
108
+
109
+ async def list(self, page: int = 1, page_size: int = 20) -> list[Vehicle]:
110
+ async with httpx.AsyncClient(base_url=self.base_url) as client:
111
+ response = await client.get(
112
+ "/vehicles",
113
+ params={"page": page, "page_size": page_size},
114
+ )
115
+ response.raise_for_status()
116
+ payload = response.json()
117
+ return [Vehicle.model_validate(v) for v in payload["data"]]`,
118
+
119
+ javascript: `class EventBus {
248
120
  #listeners = new Map();
249
121
  #wildcards = new Set();
250
122
 
251
- /**
252
- * Subscribe to an event.
253
- * @param {string} event
254
- * @param {Function} handler
255
- * @returns {() => void} unsubscribe
256
- */
257
123
  on(event, handler) {
258
124
  if (event === '*') {
259
125
  this.#wildcards.add(handler);
@@ -266,11 +132,6 @@ class EventBus {
266
132
  return () => this.#listeners.get(event)?.delete(handler);
267
133
  }
268
134
 
269
- /**
270
- * Subscribe once — auto-unsubscribes after first call.
271
- * @param {string} event
272
- * @param {Function} handler
273
- */
274
135
  once(event, handler) {
275
136
  const unsub = this.on(event, (...args) => {
276
137
  unsub();
@@ -279,273 +140,165 @@ class EventBus {
279
140
  return unsub;
280
141
  }
281
142
 
282
- /**
283
- * Emit an event with payload.
284
- * @param {string} event
285
- * @param {unknown} payload
286
- */
287
143
  emit(event, payload) {
288
144
  this.#listeners.get(event)?.forEach(h => h(payload));
289
145
  this.#wildcards.forEach(h => h(event, payload));
290
146
  }
291
-
292
- /** Remove all listeners for an event, or all events if omitted. */
293
- off(event) {
294
- if (event) {
295
- this.#listeners.delete(event);
296
- } else {
297
- this.#listeners.clear();
298
- this.#wildcards.clear();
299
- }
300
- }
301
147
  }
302
148
 
303
- // ─── Usage ────────────────────────────────────────────────────────────────────
304
-
305
149
  const bus = new EventBus();
150
+ bus.on('*', (event, payload) => console.debug(\`[bus] \${event}\`, payload));
151
+ bus.emit('vehicle:created', { id: 'v_001', make: 'BMW' });`,
306
152
 
307
- // Wildcard — log everything
308
- bus.on('*', (event, payload) => {
309
- console.debug(\`[bus] \${event}\`, payload);
310
- });
311
-
312
- // Subscribe
313
- const unsubscribe = bus.on('vehicle:created', ({ id, make, model }) => {
314
- console.log(\`New vehicle: \${make} \${model} (\${id})\`);
315
- });
316
-
317
- // One-time subscription
318
- bus.once('vehicle:deleted', ({ id }) => {
319
- console.warn(\`Vehicle \${id} deleted — cleaning up cache\`);
320
- });
321
-
322
- // Emit
323
- bus.emit('vehicle:created', { id: 'v_001', make: 'BMW', model: 'X5', year: 2023 });
324
- bus.emit('vehicle:updated', { id: 'v_001', price: 62000 });
325
- bus.emit('vehicle:deleted', { id: 'v_001' });
326
-
327
- // Unsubscribe
328
- unsubscribe();`,
329
-
330
- css: `/* ─── Design Tokens ──────────────────────────────────────────────────────── */
331
-
332
- :root {
333
- --radius-sm: 0.25rem;
153
+ css: `:root {
334
154
  --radius-md: 0.5rem;
335
- --radius-lg: 0.75rem;
336
- --radius-xl: 1rem;
337
-
338
155
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
339
- --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
340
- --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
341
-
342
- --transition-fast: 150ms cubic-bezier(0.4, 0, 0.2, 1);
343
156
  --transition-base: 200ms cubic-bezier(0.4, 0, 0.2, 1);
344
- --transition-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1);
345
157
  }
346
158
 
347
- /* ─── Card ───────────────────────────────────────────────────────────────── */
348
-
349
159
  .card {
350
- position: relative;
351
- display: flex;
352
- flex-direction: column;
353
- border-radius: var(--radius-lg);
160
+ border-radius: var(--radius-md);
354
161
  border: 1px solid hsl(var(--border));
355
162
  background: hsl(var(--card));
356
163
  box-shadow: var(--shadow-sm);
357
- overflow: hidden;
358
- transition: box-shadow var(--transition-base), transform var(--transition-base);
164
+ transition: box-shadow var(--transition-base);
359
165
 
360
166
  &:hover {
361
167
  box-shadow: var(--shadow-md);
362
168
  transform: translateY(-1px);
363
169
  }
364
-
365
- &:focus-within {
366
- outline: 2px solid hsl(var(--ring));
367
- outline-offset: 2px;
368
- }
369
- }
370
-
371
- .card-header {
372
- display: flex;
373
- align-items: center;
374
- justify-content: space-between;
375
- padding: 1rem 1.25rem 0.75rem;
376
- border-bottom: 1px solid hsl(var(--border) / 0.5);
377
- }
378
-
379
- .card-title {
380
- font-size: 0.9375rem;
381
- font-weight: 600;
382
- line-height: 1.4;
383
- color: hsl(var(--foreground));
384
- margin: 0;
385
- }
386
-
387
- .card-body {
388
- flex: 1;
389
- padding: 1rem 1.25rem;
390
- overflow-y: auto;
391
- }
392
-
393
- .card-footer {
394
- display: flex;
395
- align-items: center;
396
- gap: 0.5rem;
397
- padding: 0.75rem 1.25rem;
398
- border-top: 1px solid hsl(var(--border) / 0.5);
399
- background: hsl(var(--muted) / 0.3);
400
- }
401
-
402
- /* ─── Variants ───────────────────────────────────────────────────────────── */
403
-
404
- .card--compact {
405
- .card-header { padding: 0.625rem 1rem 0.5rem; }
406
- .card-body { padding: 0.625rem 1rem; }
407
- .card-footer { padding: 0.5rem 1rem; }
408
- .card-title { font-size: 0.875rem; }
409
- }
410
-
411
- .card--ghost {
412
- border-color: transparent;
413
- background: transparent;
414
- box-shadow: none;
415
-
416
- &:hover {
417
- background: hsl(var(--muted) / 0.5);
418
- transform: none;
419
- box-shadow: none;
420
- }
421
170
  }
422
171
 
423
- /* ─── Skeleton loader ────────────────────────────────────────────────────── */
424
-
425
172
  @keyframes shimmer {
426
173
  from { background-position: -200% 0; }
427
174
  to { background-position: 200% 0; }
428
175
  }
429
176
 
430
- .card--skeleton {
431
- pointer-events: none;
432
-
433
- .card-title,
434
- .card-body > * {
435
- border-radius: var(--radius-sm);
436
- background: linear-gradient(
437
- 90deg,
438
- hsl(var(--muted)) 25%,
439
- hsl(var(--muted-foreground) / 0.1) 50%,
440
- hsl(var(--muted)) 75%
441
- );
442
- background-size: 200% 100%;
443
- animation: shimmer 1.5s infinite;
444
- color: transparent;
445
- }
177
+ .card--skeleton .card-title {
178
+ background: linear-gradient(90deg, hsl(var(--muted)) 25%, hsl(var(--muted-foreground) / 0.1) 50%, hsl(var(--muted)) 75%);
179
+ background-size: 200% 100%;
180
+ animation: shimmer 1.5s infinite;
446
181
  }`,
447
- };
448
182
 
449
- const CODE_SAMPLES = {
450
- javascript: LONG_CODE_SAMPLES.javascript,
451
- typescript: LONG_CODE_SAMPLES.typescript,
452
- python: LONG_CODE_SAMPLES.python,
453
- css: LONG_CODE_SAMPLES.css,
183
+ html: `<!DOCTYPE html>
184
+ <html lang="en">
185
+ <head>
186
+ <meta charset="UTF-8" />
187
+ <title>Sample</title>
188
+ </head>
189
+ <body>
190
+ <div id="root"></div>
191
+ <script type="module" src="/entry.tsx"></script>
192
+ </body>
193
+ </html>`,
454
194
  };
455
195
 
196
+ // ─── Stories ──────────────────────────────────────────────────────────────────
197
+
198
+ /** Interactive — language, variant, compact, scrollIsolation all toggleable.
199
+ * Replaces the old per-language / per-mode / per-flag stories which were
200
+ * just this component with different defaults. */
456
201
  export const Interactive = () => {
457
202
  const [language] = useSelect('language', {
458
- options: ['javascript', 'typescript', 'python', 'css'] as const,
203
+ options: ['typescript', 'javascript', 'python', 'css', 'html'] as const,
459
204
  defaultValue: 'typescript',
460
205
  label: 'Language',
461
- description: 'Programming language for syntax highlighting',
206
+ });
207
+
208
+ const [variant] = useSelect('variant', {
209
+ options: ['card', 'plain'] as const,
210
+ defaultValue: 'card',
211
+ label: 'Variant',
212
+ description: 'card = full chrome, plain = bare highlighted code',
462
213
  });
463
214
 
464
215
  const [isCompact] = useBoolean('isCompact', {
465
216
  defaultValue: false,
466
217
  label: 'Compact',
467
- description: 'Use compact styling',
468
218
  });
469
219
 
470
- // No mode prop — component follows playground theme via useResolvedTheme
220
+ const [scrollIsolation] = useBoolean('scrollIsolation', {
221
+ defaultValue: true,
222
+ label: 'Scroll isolation',
223
+ description: 'Require click before the block captures scroll (card only)',
224
+ });
225
+
471
226
  return (
472
227
  <PrettyCode
473
- data={CODE_SAMPLES[language]}
228
+ data={LONG_CODE_SAMPLES[language]}
474
229
  language={language}
230
+ variant={variant}
475
231
  isCompact={isCompact}
232
+ scrollIsolation={scrollIsolation}
476
233
  />
477
234
  );
478
235
  };
479
236
 
480
- export const JavaScript = () => (
481
- <PrettyCode data={CODE_SAMPLES.javascript} language="javascript" />
482
- );
483
-
484
- export const TypeScript = () => (
485
- <PrettyCode data={CODE_SAMPLES.typescript} language="typescript" />
486
- );
487
-
488
- export const Python = () => (
489
- <PrettyCode data={CODE_SAMPLES.python} language="python" />
490
- );
491
-
492
- export const CSS = () => (
493
- <PrettyCode data={CODE_SAMPLES.css} language="css" />
494
- );
495
-
496
- export const LightMode = () => (
497
- <PrettyCode data={CODE_SAMPLES.typescript} language="typescript" mode="light" />
498
- );
499
-
500
- export const Compact = () => (
501
- <PrettyCode data={CODE_SAMPLES.javascript} language="javascript" isCompact />
237
+ /** Variants ``card`` vs ``plain`` side by side. The card has its own
238
+ * border, hover toolbar, and can internally scroll when a ``maxLines``
239
+ * cap is hit. The plain variant is chrome-less for embedding inside
240
+ * an existing scroll container (e.g. a response panel), where the
241
+ * parent surface owns chrome and scroll. */
242
+ export const Variants = () => (
243
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-4 max-w-6xl">
244
+ <section className="space-y-2">
245
+ <h3 className="text-sm font-semibold">variant=&quot;card&quot; (default)</h3>
246
+ <p className="text-xs text-muted-foreground">
247
+ Border, background, hover toolbar with Copy + language tag.
248
+ </p>
249
+ <PrettyCode data={LONG_CODE_SAMPLES.typescript} language="typescript" variant="card" />
250
+ </section>
251
+ <section className="space-y-2">
252
+ <h3 className="text-sm font-semibold">variant=&quot;plain&quot;</h3>
253
+ <p className="text-xs text-muted-foreground">
254
+ Chrome-less. No border, no background, no toolbar. Flows into the
255
+ parent scroll container.
256
+ </p>
257
+ <PrettyCode data={LONG_CODE_SAMPLES.typescript} language="typescript" variant="plain" />
258
+ </section>
259
+ </div>
502
260
  );
503
261
 
262
+ /** LongCode — stress test for multi-column layouts and long snippets. */
504
263
  export const LongCode = () => (
505
264
  <div className="flex flex-col gap-6 max-w-4xl">
506
265
  <div className="h-96">
507
- <PrettyCode data={LONG_CODE_SAMPLES.typescript} language="typescript" />
266
+ <PrettyCode data={LONG_CODE_SAMPLES.typescript} language="typescript" maxLines={30} />
508
267
  </div>
509
268
  <div className="h-96">
510
- <PrettyCode data={LONG_CODE_SAMPLES.python} language="python" />
269
+ <PrettyCode data={LONG_CODE_SAMPLES.python} language="python" maxLines={30} />
511
270
  </div>
512
271
  <div className="h-96">
513
- <PrettyCode data={LONG_CODE_SAMPLES.javascript} language="javascript" />
514
- </div>
515
- <div className="h-96">
516
- <PrettyCode data={LONG_CODE_SAMPLES.css} language="css" />
272
+ <PrettyCode data={LONG_CODE_SAMPLES.javascript} language="javascript" maxLines={30} />
517
273
  </div>
518
274
  </div>
519
275
  );
520
276
 
521
- /** Scroll isolation disabled block scrolls freely without clicking first */
522
- export const ScrollIsolationOff = () => (
523
- <div className="flex flex-col gap-6 max-w-4xl">
524
- <p className="text-sm text-muted-foreground">
525
- <strong>scrollIsolation=false</strong>scroll works immediately without clicking.
526
- </p>
527
- <div className="h-96">
528
- <PrettyCode
529
- data={LONG_CODE_SAMPLES.typescript}
530
- language="typescript"
531
- scrollIsolation={false}
532
- />
533
- </div>
534
- </div>
535
- );
536
-
537
- /** Scroll isolation enabled (default) — must click the block first to scroll inside */
538
- export const ScrollIsolationOn = () => (
539
- <div className="flex flex-col gap-6 max-w-4xl">
540
- <p className="text-sm text-muted-foreground">
541
- <strong>scrollIsolation=true</strong> (default) hover to see the hint, click to unlock scroll.
542
- </p>
543
- <div className="h-96">
544
- <PrettyCode
545
- data={LONG_CODE_SAMPLES.typescript}
546
- language="typescript"
547
- scrollIsolation
548
- />
549
- </div>
277
+ /** ScrollIsolationboth states side-by-side so the difference is
278
+ * obvious without toggling. The isolation flag matters most when the
279
+ * block is embedded in a scrolling page: without it, wheel-scrolling
280
+ * anywhere over the block "captures" the wheel and the page itself
281
+ * stops scrolling click-to-unlock avoids that surprise. */
282
+ export const ScrollIsolation = () => (
283
+ <div className="flex flex-col gap-8 max-w-4xl">
284
+ <section className="space-y-2">
285
+ <h3 className="text-sm font-semibold">scrollIsolation=true (default)</h3>
286
+ <p className="text-xs text-muted-foreground">
287
+ Hover to see the hint; click to unlock wheel-scroll inside the block.
288
+ </p>
289
+ <div className="h-64">
290
+ <PrettyCode data={LONG_CODE_SAMPLES.typescript} language="typescript" maxLines={15} scrollIsolation />
291
+ </div>
292
+ </section>
293
+ <section className="space-y-2">
294
+ <h3 className="text-sm font-semibold">scrollIsolation=false</h3>
295
+ <p className="text-xs text-muted-foreground">
296
+ Wheel-scroll captured immediately — use when the block has no
297
+ surrounding page-level scroll to compete with.
298
+ </p>
299
+ <div className="h-64">
300
+ <PrettyCode data={LONG_CODE_SAMPLES.typescript} language="typescript" maxLines={15} scrollIsolation={false} />
301
+ </div>
302
+ </section>
550
303
  </div>
551
304
  );
@@ -41,6 +41,19 @@ export interface PrettyCodeProps {
41
41
  * to inline short snippets and cap long ones.
42
42
  */
43
43
  maxLines?: number;
44
+ /**
45
+ * Visual variant:
46
+ * - ``"card"`` (default) — full chrome: border, background, hover
47
+ * toolbar (Copy + language tag), optional internal scroll. Suits
48
+ * a standalone code block in documentation prose where the block
49
+ * is its own surface.
50
+ * - ``"plain"`` — chrome-less: no border, no background, no toolbar,
51
+ * no internal scroll. Grows to fit its content. Use when
52
+ * embedding inside another scroll container (e.g. the Response
53
+ * panel) — avoids double-scroll surfaces and lets the parent
54
+ * manage copy/language affordances.
55
+ */
56
+ variant?: 'card' | 'plain';
44
57
  }
45
58
 
46
59
  const PrettyCode: React.FC<PrettyCodeProps> = (props) => {
@@ -25,6 +25,11 @@ export interface PrettyCodeProps {
25
25
  inline?: boolean;
26
26
  customBg?: string;
27
27
  isCompact?: boolean;
28
+ scrollIsolation?: boolean;
29
+ maxLines?: number;
30
+ /** ``'card'`` (default) ships the full chrome. ``'plain'`` is
31
+ * chrome-less — for embedding inside another scroll container. */
32
+ variant?: 'card' | 'plain';
28
33
  }
29
34
 
30
35
  export type { Language };