@clidey/dory 1.0.3 → 1.0.6

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 (66) hide show
  1. package/bin/dist/dory.js +36 -2
  2. package/bin/dist/dory.js.map +1 -1
  3. package/index.html +0 -1
  4. package/package.json +27 -31
  5. package/scripts/check-trigger-children.mjs +60 -0
  6. package/scripts/trigger-children-lib.mjs +149 -0
  7. package/scripts/trigger-children.json +47 -0
  8. package/src/components/hooks.tsx +2 -1
  9. package/src/components/store.ts +5 -4
  10. package/src/config/mdx.js +8 -3
  11. package/src/config/mdx.ts +8 -3
  12. package/src/embed/widget-iframe.ts +10 -3
  13. package/src/entry-server.tsx +11 -7
  14. package/src/main.tsx +5 -1
  15. package/src/mdx/accordion.tsx +11 -8
  16. package/src/mdx/api.tsx +2 -352
  17. package/src/mdx/async-api.tsx +124 -295
  18. package/src/mdx/callout.tsx +22 -24
  19. package/src/mdx/card.tsx +20 -22
  20. package/src/mdx/chip.tsx +14 -0
  21. package/src/mdx/code.tsx +133 -110
  22. package/src/mdx/expandable.tsx +8 -8
  23. package/src/mdx/fence-meta.test.ts +39 -0
  24. package/src/mdx/fence-meta.ts +61 -0
  25. package/src/mdx/fence.tsx +155 -44
  26. package/src/mdx/icon-grid.tsx +29 -0
  27. package/src/mdx/icon.tsx +2 -2
  28. package/src/mdx/latex.tsx +14 -7
  29. package/src/mdx/list.tsx +3 -3
  30. package/src/mdx/mdx.tsx +7 -1
  31. package/src/mdx/mermaid.tsx +81 -11
  32. package/src/mdx/open-api.tsx +396 -239
  33. package/src/mdx/source.tsx +148 -40
  34. package/src/mdx/specimen.tsx +69 -0
  35. package/src/mdx/step.tsx +22 -30
  36. package/src/mdx/table.tsx +4 -9
  37. package/src/mdx/tabs.tsx +39 -0
  38. package/src/mdx/tag.tsx +13 -21
  39. package/src/mdx/text.tsx +17 -9
  40. package/src/mdx/type-scale.tsx +28 -0
  41. package/src/mdx/websocket-playground.tsx +221 -704
  42. package/src/plugins/code-meta.ts +23 -0
  43. package/src/plugins/frontmatter-dev-server.ts +6 -2
  44. package/src/plugins/html-files-middleware.ts +9 -2
  45. package/src/plugins/html-metadata-injector.ts +4 -3
  46. package/src/plugins/llm-txt-dev-server.ts +5 -2
  47. package/src/plugins/llm-txt-generator.ts +2 -1
  48. package/src/plugins/prerender.ts +7 -4
  49. package/src/plugins/relative-assets.test.ts +26 -0
  50. package/src/plugins/relative-assets.ts +80 -0
  51. package/src/plugins/robots-generator.ts +6 -4
  52. package/src/plugins/sanitize.test.ts +5 -0
  53. package/src/plugins/sanitize.ts +2 -0
  54. package/src/plugins/shared.ts +17 -0
  55. package/src/plugins/sitemap-generator.ts +2 -1
  56. package/src/theme.scss +282 -3
  57. package/src/types/config.ts +3 -0
  58. package/src/ui/header.tsx +4 -3
  59. package/src/ui/layout.tsx +22 -4
  60. package/src/ui/page-meta.tsx +37 -0
  61. package/src/ui/prev-next-link.tsx +4 -3
  62. package/src/ui/table-of-content.tsx +11 -3
  63. package/src/utils/base-path.test.ts +46 -0
  64. package/src/utils/base-path.ts +41 -0
  65. package/vite.config.ssr.ts +3 -0
  66. package/vite.config.ts +3 -0
@@ -1,48 +1,37 @@
1
- import { useState } from "preact/hooks"
2
- import { APIGroup } from "./api"
3
- import { PlayButton } from "./api-playground"
4
- import { Fence } from "./fence"
5
1
  import { Properties, Property } from "./open-api"
6
- import { Tag } from "./tag"
7
- import { Col, Row } from "./text"
8
- import { WebSocketPlayground, type WebSocketServer } from "./websocket-playground"
9
- import { Button, Sheet, SheetContent, SheetHeader, SheetTitle } from "@clidey/ux"
2
+ import { Table, Td, Th } from "./table"
3
+ import { WebSocketPanel, type WebSocketServer } from "./websocket-playground"
4
+
5
+ interface Schema {
6
+ $ref?: string
7
+ type?: string
8
+ format?: string
9
+ enum?: unknown[]
10
+ items?: Schema
11
+ properties?: Record<string, Schema>
12
+ required?: string[]
13
+ description?: string
14
+ default?: unknown
15
+ example?: unknown
16
+ }
10
17
 
11
18
  interface AsyncAPIMessage {
19
+ $ref?: string
12
20
  name?: string
13
21
  title?: string
14
22
  summary?: string
15
23
  description?: string
16
24
  payload?: Schema
17
25
  headers?: Schema
18
- correlationId?: {
19
- description?: string
20
- location?: string
21
- }
22
- schemaFormat?: string
23
26
  contentType?: string
24
- examples?: any[]
27
+ examples?: unknown[]
25
28
  }
26
29
 
27
30
  interface AsyncAPIOperation {
28
31
  operationId?: string
29
32
  summary?: string
30
33
  description?: string
31
- tags?: Array<{
32
- name: string
33
- description?: string
34
- }>
35
- message?: AsyncAPIMessage | {
36
- oneOf?: AsyncAPIMessage[]
37
- }
38
- bindings?: {
39
- ws?: {
40
- method?: string
41
- query?: Schema
42
- headers?: Schema
43
- bindingVersion?: string
44
- }
45
- }
34
+ message?: AsyncAPIMessage | { oneOf?: AsyncAPIMessage[] }
46
35
  }
47
36
 
48
37
  interface AsyncAPIChannel {
@@ -52,302 +41,142 @@ interface AsyncAPIChannel {
52
41
  description?: string
53
42
  subscribe?: AsyncAPIOperation
54
43
  publish?: AsyncAPIOperation
55
- parameters?: Record<string, Parameter>
56
- messages?: Record<string, AsyncAPIMessage>
57
- bindings?: {
58
- ws?: {
59
- method?: string
60
- query?: Schema
61
- headers?: Schema
62
- bindingVersion?: string
63
- }
64
- }
65
- }
66
-
67
- interface Schema {
68
- $ref?: string
69
- type?: string
70
- items?: Schema
71
- properties?: Record<string, Schema>
72
- required?: string[]
73
- description?: string
74
- default?: any
75
- example?: any
76
- format?: string
77
- enum?: any[]
78
- oneOf?: Schema[]
79
- anyOf?: Schema[]
80
- allOf?: Schema[]
81
- }
82
-
83
- interface Parameter {
84
- description?: string
85
- schema?: Schema
86
- location?: string
87
- default?: any
88
44
  }
89
45
 
90
46
  interface Components {
91
47
  schemas?: Record<string, Schema>
92
48
  messages?: Record<string, AsyncAPIMessage>
93
- parameters?: Record<string, Parameter>
94
- }
95
-
96
- interface Info {
97
- title: string
98
- version: string
99
- description?: string
100
- termsOfService?: string
101
- contact?: {
102
- name?: string
103
- url?: string
104
- email?: string
105
- }
106
- license?: {
107
- name: string
108
- url?: string
109
- }
110
49
  }
111
50
 
112
51
  export interface AsyncAPIJson {
113
52
  asyncapi: string
114
- info: Info
115
- servers?: Record<string, WebSocketServer & {
116
- host?: string
117
- pathname?: string
118
- protocol?: string
119
- protocolVersion?: string
120
- description?: string
121
- title?: string
122
- summary?: string
123
- variables?: Record<string, any>
124
- security?: any[]
125
- tags?: Array<{
126
- name: string
127
- description?: string
128
- }>
129
- bindings?: any
130
- }>
53
+ info: { title: string; version: string; description?: string }
54
+ servers?: Record<string, WebSocketServer & { host?: string; pathname?: string; protocol?: string; description?: string; title?: string }>
131
55
  channels: Record<string, AsyncAPIChannel>
132
56
  components?: Components
133
- defaultContentType?: string
134
57
  }
135
58
 
59
+ type Direction = 'SEND' | 'RECV'
60
+
136
61
  function resolveSchema(schema: Schema | undefined, components: Components): Schema {
137
62
  if (!schema) return {}
138
-
139
63
  if (schema.$ref) {
140
- const refPath = schema.$ref.replace(/^#\/components\/schemas\//, '')
141
- return resolveSchema(components.schemas?.[refPath], components)
142
- }
143
-
144
- if (schema.type === 'array' && schema.items) {
145
- return {
146
- ...schema,
147
- items: resolveSchema(schema.items, components)
148
- }
64
+ const name = schema.$ref.replace(/^#\/components\/schemas\//, '')
65
+ return resolveSchema(components.schemas?.[name], components)
149
66
  }
150
-
67
+ if (schema.type === 'array' && schema.items) return { ...schema, items: resolveSchema(schema.items, components) }
151
68
  if (schema.properties) {
152
- const resolvedProps: Record<string, Schema> = {}
153
- for (const [key, value] of Object.entries(schema.properties)) {
154
- resolvedProps[key] = resolveSchema(value, components)
155
- }
156
- return {
157
- ...schema,
158
- properties: resolvedProps
159
- }
69
+ const properties: Record<string, Schema> = {}
70
+ for (const [key, value] of Object.entries(schema.properties)) properties[key] = resolveSchema(value, components)
71
+ return { ...schema, properties }
160
72
  }
161
-
162
73
  return schema
163
74
  }
164
75
 
165
- function isDirectMessage(message: AsyncAPIMessage | { oneOf?: AsyncAPIMessage[] } | undefined): message is AsyncAPIMessage {
166
- return message != null && !('oneOf' in message);
76
+ /** Messages an operation carries, with `$ref` and `oneOf` resolved against components. */
77
+ function messagesOf(operation: AsyncAPIOperation | undefined, components: Components): AsyncAPIMessage[] {
78
+ if (!operation?.message) return []
79
+ const list = 'oneOf' in operation.message && operation.message.oneOf ? operation.message.oneOf : [operation.message as AsyncAPIMessage]
80
+ return list.map((message) => {
81
+ if (!message.$ref) return message
82
+ const name = message.$ref.replace(/^#\/components\/messages\//, '')
83
+ return { name, ...(components.messages?.[name] ?? {}) }
84
+ })
167
85
  }
168
86
 
169
- export function AsyncAPI({
170
- asyncAPIJson,
171
- channel: selectedChannel,
172
- operation: selectedOperation
173
- }: {
87
+ function typeLabel(schema: Schema): string {
88
+ if (schema.enum) return `enum<${schema.type ?? 'string'}>`
89
+ if (schema.type === 'array') return `array<${schema.items ? typeLabel(schema.items) : 'any'}>`
90
+ if (schema.format) return `${schema.type ?? 'string'} · ${schema.format}`
91
+ return schema.type ?? 'object'
92
+ }
93
+
94
+ /**
95
+ * WebSocket operation page: a numbered "Connect" section with the live panel,
96
+ * a "Message types" table (SEND for publish, RECV for subscribe) and the payload fields.
97
+ */
98
+ export function AsyncAPI({
99
+ asyncAPIJson,
100
+ channel: selectedChannel,
101
+ operation: selectedOperation,
102
+ }: {
174
103
  asyncAPIJson: AsyncAPIJson
175
104
  channel: string
176
105
  operation: 'subscribe' | 'publish'
177
106
  }) {
178
- const { channels, components, servers } = asyncAPIJson;
179
- const [showPlayground, setShowPlayground] = useState(false)
180
-
181
- const handleShowPlayground = () => {
182
- setShowPlayground(true)
107
+ const { channels, components = {}, servers } = asyncAPIJson
108
+ const channel = channels[selectedChannel]
109
+ const operation = channel?.[selectedOperation]
110
+ if (!channel || !operation) return null
111
+
112
+ const wsServers: WebSocketServer[] = servers
113
+ ? Object.entries(servers).map(([key, server]) => ({
114
+ url: `${server.protocol}://${server.host}${server.pathname || ''}`,
115
+ description: server.description || server.title || key,
116
+ protocol: server.protocol,
117
+ }))
118
+ : []
119
+
120
+ const rows: Array<{ name: string; direction: Direction; description?: string }> = [
121
+ ...messagesOf(channel.publish, components).map((m) => ({ name: m.name ?? m.title ?? 'message', direction: 'SEND' as Direction, description: m.summary ?? m.description })),
122
+ ...messagesOf(channel.subscribe, components).map((m) => ({ name: m.name ?? m.title ?? 'message', direction: 'RECV' as Direction, description: m.summary ?? m.description })),
123
+ ]
124
+ const primary = messagesOf(operation, components)[0]
125
+ const payload = primary?.payload ? resolveSchema(primary.payload, components) : undefined
126
+ const headerExample = primary?.headers ? resolveSchema(primary.headers, components) : undefined
127
+ const defaultHeaders: Record<string, string> = {}
128
+ for (const [name, schema] of Object.entries(headerExample?.properties ?? {})) {
129
+ if (schema.example !== undefined) defaultHeaders[name] = String(schema.example)
183
130
  }
184
131
 
185
- // Convert AsyncAPI servers to WebSocket servers
186
- const wsServers: WebSocketServer[] = servers ? Object.entries(servers).map(([key, server]) => ({
187
- url: `${server.protocol}://${server.host}${server.pathname || ''}`,
188
- description: server.description || server.title || key,
189
- protocol: server.protocol
190
- })) : [];
191
-
192
132
  return (
193
- <div className="max-w-none w-full">
194
- <Sheet open={showPlayground} onOpenChange={setShowPlayground} modal={true}>
195
- <SheetContent side="bottom" className="h-[90vh] w-full overflow-y-auto p-8">
196
- <SheetHeader className="p-0">
197
- <SheetTitle>WebSocket Playground</SheetTitle>
198
- </SheetHeader>
199
- <WebSocketPlayground
200
- url={selectedChannel}
201
- servers={wsServers}
202
- />
203
- </SheetContent>
204
- </Sheet>
205
-
206
- {Object.entries(channels).map(([channelPath, channelDetails]) => {
207
- if (channelPath !== selectedChannel) {
208
- return null
209
- }
210
-
211
- const operation = channelDetails[selectedOperation]
212
- if (!operation) {
213
- return null
214
- }
215
-
216
- const { summary, description, message } = operation
217
- const resolvedMessage = isDirectMessage(message) ? message :
218
- (message?.oneOf && message.oneOf.length > 0 ? message.oneOf[0] : undefined)
219
-
220
- return (
221
- <div key={channelPath} className="my-8">
222
- <Row cols={2}>
223
- <Col>
224
- <h2 className="flex items-center mt-4">
225
- <Tag variant="medium" className="mr-2 py-1 px-4 rounded-lg">
226
- {selectedOperation.toUpperCase()}
227
- </Tag>
228
- <span className="ml-2 font-mono">{channelPath}</span>
229
- </h2>
230
-
231
- {(summary || description) && (
232
- <p>{summary || description}</p>
233
- )}
234
-
235
- {resolvedMessage && (
236
- <div className="mt-4">
237
- <h3>Message</h3>
238
- {resolvedMessage.title && <h4>{resolvedMessage.title}</h4>}
239
- {resolvedMessage.description && <p>{resolvedMessage.description}</p>}
240
-
241
- {resolvedMessage.payload && (
242
- <div className="mt-4">
243
- <h4>Payload</h4>
244
- {(() => {
245
- const resolvedPayload = resolveSchema(resolvedMessage.payload, components || {})
246
- return (
247
- <Properties>
248
- {Object.entries(resolvedPayload.properties || {}).map(([propName, prop]) => (
249
- <Property
250
- key={propName}
251
- name={propName}
252
- type={prop.type || 'string'}
253
- required={resolvedPayload.required?.includes(propName)}
254
- >
255
- {prop.description}
256
- {prop.default !== undefined && (
257
- <span className="text-muted-foreground ml-1">(default: {JSON.stringify(prop.default)})</span>
258
- )}
259
- </Property>
260
- ))}
261
- </Properties>
262
- )
263
- })()}
264
- </div>
265
- )}
266
-
267
- {resolvedMessage.headers && (
268
- <div className="mt-4">
269
- <h4>Headers</h4>
270
- {(() => {
271
- const resolvedHeaders = resolveSchema(resolvedMessage.headers, components || {})
272
- return (
273
- <Properties>
274
- {Object.entries(resolvedHeaders.properties || {}).map(([headerName, header]) => (
275
- <Property
276
- key={headerName}
277
- name={headerName}
278
- type={header.type || 'string'}
279
- required={resolvedHeaders.required?.includes(headerName)}
280
- >
281
- {header.description}
282
- </Property>
283
- ))}
284
- </Properties>
285
- )
286
- })()}
287
- </div>
288
- )}
289
-
290
- {resolvedMessage?.examples && resolvedMessage.examples.length > 0 && (
291
- <div className="mt-4">
292
- <h4>Examples</h4>
293
- {resolvedMessage.examples.map((example: any, idx: number) => (
294
- <Fence key={idx} language="json">
295
- {JSON.stringify(example, null, 2)}
296
- </Fence>
297
- ))}
298
- </div>
299
- )}
300
- </div>
301
- )}
302
-
303
- {components?.schemas && (
304
- <div className="mt-4">
305
- <h3>Schemas</h3>
306
- {Object.entries(components.schemas).filter(([, schema]) => schema.properties).map(([name, schema]) => (
307
- <div key={name} className="mt-4">
308
- <h4>{name}</h4>
309
- <Properties>
310
- {Object.entries(schema.properties || {}).map(([propName, prop]) => (
311
- <Property
312
- key={propName}
313
- name={propName}
314
- type={prop.type || 'string'}
315
- required={schema.required?.includes(propName)}
316
- >
317
- {prop.description}
318
- </Property>
319
- ))}
320
- </Properties>
321
- </div>
322
- ))}
323
- </div>
324
- )}
325
- </Col>
326
- <Col sticky>
327
- <Button
328
- onClick={handleShowPlayground}
329
- className='ml-auto mt-3 flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium'
330
- >
331
- Try
332
- <PlayButton className="h-4 w-4" />
333
- </Button>
334
- <APIGroup title="Message" tag={selectedOperation.toUpperCase()} label={channelPath}>
335
- <>
336
- {resolvedMessage?.payload && (() => {
337
- const resolvedPayload = resolveSchema(resolvedMessage.payload, components || {})
338
- return (
339
- <Fence language="json">
340
- {JSON.stringify(resolvedPayload, null, 2)}
341
- </Fence>
342
- )
343
- })()}
344
- </>
345
- </APIGroup>
346
- </Col>
347
- </Row>
348
- </div>
349
- )
350
- })}
133
+ <div className="dory-asyncapi max-w-none w-full">
134
+ {(operation.description || operation.summary) && <p className="dory-lead -mt-4 mb-6">{operation.description || operation.summary}</p>}
135
+
136
+ <h2 id="connect" className="dory-h2 font-heading mb-3 mt-10"><span>Connect</span></h2>
137
+ <WebSocketPanel url={channel.address ?? selectedChannel} servers={wsServers} headers={defaultHeaders} />
138
+
139
+ {rows.length > 0 && (
140
+ <>
141
+ <h2 id="message-types" className="dory-h2 font-heading mb-3 mt-10"><span>Message types</span></h2>
142
+ <Table>
143
+ <thead>
144
+ <tr>
145
+ <Th>Type</Th>
146
+ <Th>Dir</Th>
147
+ <Th>Description</Th>
148
+ </tr>
149
+ </thead>
150
+ <tbody>
151
+ {rows.map((row) => (
152
+ <tr key={`${row.direction}-${row.name}`}>
153
+ <Td><span className="font-mono text-xs">{row.name}</span></Td>
154
+ <Td>
155
+ <span className={row.direction === 'SEND' ? 'font-mono text-xs font-semibold text-[var(--brand-readable,#b65401)]' : 'font-mono text-xs font-semibold text-[var(--primary)]'}>
156
+ {row.direction}
157
+ </span>
158
+ </Td>
159
+ <Td>{row.description}</Td>
160
+ </tr>
161
+ ))}
162
+ </tbody>
163
+ </Table>
164
+ </>
165
+ )}
166
+
167
+ {payload?.properties && (
168
+ <>
169
+ <h2 id="payload" className="dory-h2 font-heading mb-3 mt-10"><span>Payload</span></h2>
170
+ {primary?.contentType && <p className="-mt-2 mb-2 font-mono text-xs text-muted-foreground">{primary.contentType}</p>}
171
+ <Properties>
172
+ {Object.entries(payload.properties).map(([name, schema]) => (
173
+ <Property key={name} name={name} type={typeLabel(schema)} required={payload.required?.includes(name)} defaultValue={schema.default}>
174
+ {schema.description}
175
+ </Property>
176
+ ))}
177
+ </Properties>
178
+ </>
179
+ )}
351
180
  </div>
352
181
  )
353
- }
182
+ }
@@ -1,28 +1,21 @@
1
- import { Alert, AlertTitle, AlertDescription } from '@clidey/ux'
1
+ import classNames from 'classnames'
2
2
  import { Icon } from "./icon"
3
3
 
4
- const typeToVariant = {
5
- note: 'default',
6
- warning: 'destructive',
7
- info: 'default',
8
- tip: 'default',
9
- check: 'default',
10
- } as const
11
-
12
4
  const icons = {
13
- note: 'InfoCircle',
5
+ note: 'Info',
14
6
  warning: 'AlertTriangle',
15
- info: 'InfoCircle',
16
- tip: 'Lightbulb',
17
- check: 'CheckCircle',
7
+ info: 'Info',
8
+ tip: 'Star',
9
+ check: 'Check',
18
10
  } as const
19
11
 
20
- const typeColors = {
21
- note: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-900 dark:text-emerald-200',
22
- warning: 'border-amber-500/30 bg-amber-500/10 text-amber-900 dark:text-amber-200 *:data-[slot=alert-description]:text-inherit dark:data-[slot=alert-description]:text-inherit',
23
- info: 'border-blue-500/30 bg-blue-500/10 text-blue-900 dark:text-blue-200',
24
- tip: 'border-green-500/30 bg-green-500/10 text-green-900 dark:text-green-200',
25
- check: 'border-purple-500/30 bg-purple-500/10 text-purple-900 dark:text-purple-200',
12
+ // Each type gets a distinct surface and icon badge shape so they stay distinguishable without color.
13
+ const typeStyles = {
14
+ note: { box: 'bg-muted border-[var(--border)]', badge: 'bg-card border border-[var(--border)] text-foreground rounded-md' },
15
+ warning: { box: 'bg-background border-[var(--highlight)]', badge: 'bg-[var(--highlight)] text-[var(--highlight-foreground)] rounded-[5px]' },
16
+ info: { box: 'bg-accent border-transparent', badge: 'bg-[var(--primary)] text-white rounded-[5px]' },
17
+ tip: { box: 'bg-[var(--success-muted)] border-transparent', badge: 'border border-foreground/70 text-foreground rounded-md' },
18
+ check: { box: 'bg-background border-[var(--border)]', badge: 'bg-[var(--success-muted)] text-foreground rounded-[5px]' },
26
19
  }
27
20
 
28
21
  export function Callout({
@@ -34,12 +27,17 @@ export function Callout({
34
27
  children: React.ReactNode
35
28
  type?: keyof typeof icons
36
29
  }) {
30
+ const styles = typeStyles[type]
37
31
  return (
38
- <Alert variant={typeToVariant[type]} className={`my-6 ${typeColors[type]}`}>
39
- <Icon icon={icons[type]} className="h-4 w-4" />
40
- <AlertTitle>{title}</AlertTitle>
41
- <AlertDescription>{children}</AlertDescription>
42
- </Alert>
32
+ <div role="note" className={classNames('dory-callout not-prose my-5 flex gap-3 rounded-lg border px-4 py-3 text-foreground', styles.box)}>
33
+ <span className={classNames('mt-px flex size-6 shrink-0 items-center justify-center', styles.badge)}>
34
+ <Icon icon={icons[type]} className="h-3 w-3 !stroke-current" />
35
+ </span>
36
+ <div className="min-w-0 flex-1">
37
+ <p className="font-semibold leading-snug">{title}</p>
38
+ <div className="leading-relaxed [&_p]:mb-0">{children}</div>
39
+ </div>
40
+ </div>
43
41
  )
44
42
  }
45
43
 
package/src/mdx/card.tsx CHANGED
@@ -1,18 +1,13 @@
1
1
  import classNames from "classnames";
2
- import {
3
- Card as UXCard,
4
- CardHeader,
5
- CardTitle,
6
- CardDescription,
7
- CardContent
8
- } from '@clidey/ux'
2
+ import { withBase } from '../utils/base-path';
9
3
  import { Icon } from "./icon";
10
4
  import type { ComponentChildren } from "preact";
11
5
 
12
- export function CardGroup({ children, cols = 2 }: { children: React.ReactNode; cols?: number }) {
6
+ export function CardGroup({ children, cols = 2, numbered = false }: { children: React.ReactNode; cols?: number; numbered?: boolean }) {
13
7
  return (
14
8
  <div className={classNames(
15
- 'not-prose my-12 grid grid-cols-1 gap-6',
9
+ 'not-prose my-6 grid grid-cols-1 gap-3',
10
+ numbered && 'dory-cards-numbered',
16
11
  cols === 2 && 'sm:grid-cols-2',
17
12
  cols === 3 && 'sm:grid-cols-3',
18
13
  cols === 4 && 'sm:grid-cols-4'
@@ -36,19 +31,22 @@ export function Card({
36
31
  children?: ComponentChildren
37
32
  }) {
38
33
  return (
39
- <UXCard className="group relative border-ring border-1 border-black/10 dark:border-white/10">
40
- {href && <a href={href} className="absolute inset-0 z-10" aria-label={title} />}
41
- <CardHeader>
42
- <div className="flex items-center gap-3">
43
- {icon && <Icon icon={icon} className="h-8 w-8" />}
44
- <CardTitle className="text-base">{title}</CardTitle>
34
+ <div className="dory-card group relative flex flex-col rounded-lg border border-[var(--border)] bg-card p-4 transition-shadow hover:shadow-[0_2px_10px_rgb(14_34_64/0.07)]">
35
+ {href && <a href={withBase(href)} className="absolute inset-0 z-10 rounded-lg" aria-label={title} />}
36
+ {(icon || href) && (
37
+ <div className="mb-3 flex items-start justify-between">
38
+ {icon ? (
39
+ <span className="flex size-8 items-center justify-center rounded-md bg-accent text-[var(--primary)]">
40
+ <Icon icon={icon} className="h-4 w-4 !stroke-current" />
41
+ </span>
42
+ ) : <span />}
43
+ {href && <Icon icon="ExternalLink" className="h-3.5 w-3.5 !stroke-[var(--muted-foreground)]" />}
45
44
  </div>
46
- </CardHeader>
47
- <CardContent>
48
- <CardDescription className="text-sm leading-tight">
49
- {description ?? children}
50
- </CardDescription>
51
- </CardContent>
52
- </UXCard>
45
+ )}
46
+ <p className="font-heading text-[15px] font-semibold tracking-tight text-foreground">{title}</p>
47
+ <div className="mt-1 text-[13px] leading-normal text-muted-foreground [&_p]:mb-0">
48
+ {description ?? children}
49
+ </div>
50
+ </div>
53
51
  )
54
52
  }
@@ -0,0 +1,14 @@
1
+ /** Wrapping row of Chips with even gaps. */
2
+ export function ChipGroup({ children }: { children: preact.ComponentChildren }) {
3
+ return <div className="not-prose my-4 flex flex-wrap gap-2">{children}</div>
4
+ }
5
+
6
+ /** Pill label with a brand-colored dot, for short lists such as supported databases. Links when `href` is set. */
7
+ export function Chip({ children, href }: { children: preact.ComponentChildren; href?: string }) {
8
+ const className = 'inline-flex items-center gap-2 rounded-full border border-[var(--border)] bg-card px-3.5 py-1.5 text-[13px] font-medium text-foreground'
9
+ const dot = <span aria-hidden="true" className="size-1.5 shrink-0 rounded-full bg-[var(--primary)]" />
10
+ if (href) {
11
+ return <a href={href} className={`${className} hover:border-[var(--primary)]`}>{dot}{children}</a>
12
+ }
13
+ return <span className={className}>{dot}{children}</span>
14
+ }