@clidey/dory 0.1.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.
Files changed (104) hide show
  1. package/.dockerignore +4 -0
  2. package/.github/ISSUE_TEMPLATE/anything-else.md +10 -0
  3. package/.github/ISSUE_TEMPLATE/bug_report.md +39 -0
  4. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. package/.github/dependabot.yml +11 -0
  6. package/.github/workflows/claude.yml +37 -0
  7. package/CODE_OF_CONDUCT.md +128 -0
  8. package/CONTRIBUTING.md +91 -0
  9. package/LICENSE +201 -0
  10. package/README.md +185 -0
  11. package/SECURITY.md +11 -0
  12. package/assets/dory_logo.png +0 -0
  13. package/bin/dory.js +131 -0
  14. package/docs/README.md +2 -0
  15. package/docs/configuration/app-settings/feature-flags.mdx +157 -0
  16. package/docs/configuration/app-settings/user-settings.mdx +201 -0
  17. package/docs/configuration/installation-setup/env-vars.mdx +198 -0
  18. package/docs/configuration/installation-setup/installation.mdx +127 -0
  19. package/docs/dory.json +162 -0
  20. package/docs/integrations/analytics-monitoring/error-tracking.mdx +199 -0
  21. package/docs/integrations/analytics-monitoring/notifications.mdx +189 -0
  22. package/docs/integrations/analytics-monitoring/user-analytics.mdx +182 -0
  23. package/docs/integrations/shopify-integration/shopify-setup.mdx +226 -0
  24. package/docs/integrations/shopify-integration/shopify-webhooks.mdx +207 -0
  25. package/docs/overview/developer-overview/dev-architecture.mdx +292 -0
  26. package/docs/overview/product-intro/app-overview.mdx +167 -0
  27. package/docs/overview/product-intro/architecture.mdx +203 -0
  28. package/docs/overview/product-intro/core-concepts.mdx +267 -0
  29. package/docs/platform-admin/billing-subscriptions/billing.mdx +176 -0
  30. package/docs/platform-admin/system-maintenance/db-migrations.mdx +184 -0
  31. package/docs/platform-admin/system-maintenance/health-monitoring.mdx +222 -0
  32. package/docs/platform-admin/system-maintenance/queue-management.mdx +234 -0
  33. package/docs/platform-admin/user-team-management/teams.mdx +170 -0
  34. package/docs/platform-admin/user-team-management/users.mdx +185 -0
  35. package/docs/support/legal/dependency-analysis.mdx +132 -0
  36. package/docs/support/legal/privacy-terms.mdx +73 -0
  37. package/docs/support/troubleshooting/common-issues.mdx +159 -0
  38. package/docs/usage/api-usage/api-inventory.mdx +213 -0
  39. package/docs/usage/api-usage/api-products.mdx +249 -0
  40. package/docs/usage/api-usage/api-purchase-orders.mdx +289 -0
  41. package/docs/usage/api-usage/api-reports.mdx +227 -0
  42. package/docs/usage/api-usage/api-suppliers.mdx +171 -0
  43. package/docs/usage/api-usage/api-user-management.mdx +171 -0
  44. package/docs/usage/ui-workflows/bundles.mdx +157 -0
  45. package/docs/usage/ui-workflows/demand-planning.mdx +210 -0
  46. package/docs/usage/ui-workflows/favorites.mdx +167 -0
  47. package/docs/usage/ui-workflows/inventory-management.mdx +179 -0
  48. package/docs/usage/ui-workflows/onboarding.mdx +215 -0
  49. package/docs/usage/ui-workflows/purchase-orders.mdx +200 -0
  50. package/docs/usage/ui-workflows/reports.mdx +257 -0
  51. package/index.html +54 -0
  52. package/k8s/Dockerfile +15 -0
  53. package/k8s/config/nginx-default.conf +22 -0
  54. package/k8s/dev.Dockerfile +16 -0
  55. package/package.json +71 -0
  56. package/src/app.css +3 -0
  57. package/src/app.tsx +13 -0
  58. package/src/components/dark-mode-toggle.tsx +25 -0
  59. package/src/components/dropdown.tsx +49 -0
  60. package/src/components/hooks.tsx +28 -0
  61. package/src/components/loading.tsx +23 -0
  62. package/src/components/mdx-provider.tsx +13 -0
  63. package/src/components/navigation.tsx +53 -0
  64. package/src/components/notification.tsx +97 -0
  65. package/src/components/routes.tsx +89 -0
  66. package/src/components/search-index.ts +43 -0
  67. package/src/components/search.tsx +508 -0
  68. package/src/components/store.ts +144 -0
  69. package/src/index.css +24 -0
  70. package/src/main.tsx +18 -0
  71. package/src/mdx/accordion.tsx +48 -0
  72. package/src/mdx/api-playground.tsx +674 -0
  73. package/src/mdx/api.tsx +354 -0
  74. package/src/mdx/callout.tsx +80 -0
  75. package/src/mdx/card.tsx +48 -0
  76. package/src/mdx/code.tsx +289 -0
  77. package/src/mdx/expandable.tsx +40 -0
  78. package/src/mdx/fence.tsx +73 -0
  79. package/src/mdx/icon.tsx +73 -0
  80. package/src/mdx/latex.tsx +26 -0
  81. package/src/mdx/list.tsx +7 -0
  82. package/src/mdx/mdx.tsx +43 -0
  83. package/src/mdx/mermaid.tsx +102 -0
  84. package/src/mdx/open-api.tsx +325 -0
  85. package/src/mdx/source.tsx +47 -0
  86. package/src/mdx/step.tsx +49 -0
  87. package/src/mdx/table.tsx +15 -0
  88. package/src/mdx/tag.tsx +66 -0
  89. package/src/mdx/text.tsx +98 -0
  90. package/src/plugins/sanitize.ts +15 -0
  91. package/src/theme.scss +124 -0
  92. package/src/ui/header.tsx +88 -0
  93. package/src/ui/layout.tsx +167 -0
  94. package/src/ui/mobile-navigation.tsx +91 -0
  95. package/src/ui/prev-next-link.tsx +74 -0
  96. package/src/ui/table-of-content.tsx +166 -0
  97. package/src/utils/fonts.ts +20 -0
  98. package/src/utils/functions.ts +12 -0
  99. package/src/utils/hooks.ts +46 -0
  100. package/src/vite-env.d.ts +1 -0
  101. package/tsconfig.app.json +32 -0
  102. package/tsconfig.json +7 -0
  103. package/tsconfig.node.json +25 -0
  104. package/vite.config.ts +27 -0
@@ -0,0 +1,354 @@
1
+ import { Tab, TabGroup, TabList, TabPanel, TabPanels } from '@headlessui/react'
2
+ import classNames from 'classnames'
3
+ import {
4
+ Children,
5
+ createContext,
6
+ isValidElement,
7
+ useContext,
8
+ useEffect,
9
+ useRef,
10
+ useState,
11
+ } from 'react'
12
+ import { Tag } from './tag'
13
+
14
+ const languageNames: Record<string, string> = {
15
+ js: 'JavaScript',
16
+ ts: 'TypeScript',
17
+ javascript: 'JavaScript',
18
+ typescript: 'TypeScript',
19
+ php: 'PHP',
20
+ python: 'Python',
21
+ ruby: 'Ruby',
22
+ go: 'Go',
23
+ }
24
+
25
+ function getPanelTitle({
26
+ title,
27
+ language,
28
+ }: {
29
+ title?: string
30
+ language?: string
31
+ }) {
32
+ if (title) {
33
+ return title
34
+ }
35
+ if (language && language in languageNames) {
36
+ return languageNames[language]
37
+ }
38
+ return 'API'
39
+ }
40
+
41
+ function ClipboardIcon(props: React.ComponentPropsWithoutRef<'svg'>) {
42
+ return (
43
+ <svg viewBox="0 0 20 20" aria-hidden="true" {...props}>
44
+ <path
45
+ strokeWidth="0"
46
+ d="M5.5 13.5v-5a2 2 0 0 1 2-2l.447-.894A2 2 0 0 1 9.737 4.5h.527a2 2 0 0 1 1.789 1.106l.447.894a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2Z"
47
+ />
48
+ <path
49
+ fill="none"
50
+ strokeLinejoin="round"
51
+ d="M12.5 6.5a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-5a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2m5 0-.447-.894a2 2 0 0 0-1.79-1.106h-.527a2 2 0 0 0-1.789 1.106L7.5 6.5m5 0-1 1h-3l-1-1"
52
+ />
53
+ </svg>
54
+ )
55
+ }
56
+
57
+ function CopyButton({ code }: { code: string }) {
58
+ const [copyCount, setCopyCount] = useState(0)
59
+ const copied = copyCount > 0
60
+
61
+ useEffect(() => {
62
+ if (copyCount > 0) {
63
+ const timeout = setTimeout(() => setCopyCount(0), 1000)
64
+ return () => {
65
+ clearTimeout(timeout)
66
+ }
67
+ }
68
+ }, [copyCount])
69
+
70
+ return (
71
+ <button
72
+ type="button"
73
+ className={classNames(
74
+ 'group/button absolute top-3.5 right-4 overflow-hidden rounded-full py-1 pr-3 pl-2 text-2xs font-medium opacity-0 backdrop-blur-sm transition group-hover:opacity-100 focus:opacity-100',
75
+ copied
76
+ ? 'bg-sky-400/10 ring-1 ring-sky-400/20 ring-inset'
77
+ : 'bg-white/5 hover:bg-white/7.5 dark:bg-white/2.5 dark:hover:bg-white/5',
78
+ )}
79
+ onClick={() => {
80
+ window.navigator.clipboard.writeText(code).then(() => {
81
+ setCopyCount((count) => count + 1)
82
+ })
83
+ }}
84
+ >
85
+ <span
86
+ aria-hidden={copied}
87
+ className={classNames(
88
+ 'pointer-events-none flex items-center gap-0.5 text-zinc-400 transition duration-300',
89
+ copied && '-translate-y-1.5 opacity-0',
90
+ )}
91
+ >
92
+ <ClipboardIcon className="h-5 w-5 fill-zinc-500/20 stroke-zinc-500 transition-colors group-hover/button:stroke-zinc-400" />
93
+ Copy
94
+ </span>
95
+ <span
96
+ aria-hidden={!copied}
97
+ className={classNames(
98
+ 'pointer-events-none absolute inset-0 flex items-center justify-center text-sky-400 transition duration-300',
99
+ !copied && 'translate-y-1.5 opacity-0',
100
+ )}
101
+ >
102
+ Copied!
103
+ </span>
104
+ </button>
105
+ )
106
+ }
107
+
108
+ function APIPanelHeader({ tag, label }: { tag?: string; label?: string }) {
109
+ if (!tag && !label) {
110
+ return null
111
+ }
112
+
113
+ return (
114
+ <div className="flex h-9 items-center gap-2 border-y border-t-transparent border-b-white/7.5 bg-zinc-900 px-4 dark:border-b-white/5 dark:bg-white/1">
115
+ {tag && (
116
+ <div className="dark flex">
117
+ <Tag variant="small">{tag}</Tag>
118
+ </div>
119
+ )}
120
+ {tag && label && (
121
+ <span className="h-0.5 w-0.5 rounded-full bg-zinc-500" />
122
+ )}
123
+ {label && (
124
+ <span className="font-mono text-xs text-zinc-400">{label}</span>
125
+ )}
126
+ </div>
127
+ )
128
+ }
129
+
130
+ function APIPanel({
131
+ children,
132
+ tag,
133
+ label,
134
+ code,
135
+ }: {
136
+ children: React.ReactNode
137
+ tag?: string
138
+ label?: string
139
+ code?: string
140
+ }) {
141
+ const child = Children.only(children)
142
+
143
+ if (isValidElement(child)) {
144
+ // @ts-expect-error child.props is not typed
145
+ tag = child.props.tag ?? tag
146
+ // @ts-expect-error child.props is not typed
147
+ label = child.props.label ?? label
148
+ // @ts-expect-error child.props is not typed
149
+ code = child.props.code ?? code
150
+ }
151
+
152
+ return (
153
+ <div className="group dark:bg-white/2.5">
154
+ <APIPanelHeader tag={tag} label={label} />
155
+ <div className="relative">
156
+ <pre className="overflow-x-auto p-4 text-xs text-white">{children}</pre>
157
+ {/* @ts-expect-error code is not typed */}
158
+ <CopyButton code={code} />
159
+ </div>
160
+ </div>
161
+ )
162
+ }
163
+
164
+ function APIGroupHeader({
165
+ title,
166
+ children,
167
+ selectedIndex,
168
+ }: {
169
+ title: string
170
+ children: React.ReactNode
171
+ selectedIndex: number
172
+ }) {
173
+ const hasTabs = Children.count(children) > 1
174
+
175
+ if (!title && !hasTabs) {
176
+ return null
177
+ }
178
+
179
+ return (
180
+ <div className="flex min-h-[calc(--spacing(12)+1px)] flex-wrap items-start gap-x-4 border-b border-zinc-700 bg-zinc-800 px-4 dark:border-zinc-800 dark:bg-transparent">
181
+ {title && (
182
+ <h3 className="mr-auto pt-3 text-xs font-semibold text-white">
183
+ {title}
184
+ </h3>
185
+ )}
186
+ {hasTabs && (
187
+ <TabList className="-mb-px flex gap-4 text-xs font-medium">
188
+ {Children.map(children, (child, childIndex) => (
189
+ <Tab
190
+ className={classNames(
191
+ 'border-b py-3 transition data-selected:not-data-focus:outline-hidden',
192
+ childIndex === selectedIndex
193
+ ? 'border-sky-500 text-sky-400'
194
+ : 'border-transparent text-zinc-400 hover:text-zinc-300',
195
+ )}
196
+ >
197
+ {/* @ts-expect-error child.props is not typed */}
198
+ {getPanelTitle(isValidElement(child) ? child.props : {})}
199
+ </Tab>
200
+ ))}
201
+ </TabList>
202
+ )}
203
+ </div>
204
+ )
205
+ }
206
+
207
+ function APIGroupPanels({
208
+ children,
209
+ ...props
210
+ }: React.ComponentPropsWithoutRef<typeof APIPanel>) {
211
+ const hasTabs = Children.count(children) > 1
212
+
213
+ if (hasTabs) {
214
+ return (
215
+ <TabPanels>
216
+ {Children.map(children, (child) => (
217
+ <TabPanel>
218
+ <APIPanel {...props}>{child}</APIPanel>
219
+ </TabPanel>
220
+ ))}
221
+ </TabPanels>
222
+ )
223
+ }
224
+
225
+ return <APIPanel {...props}>{children}</APIPanel>
226
+ }
227
+ function usePreventLayoutShift() {
228
+ const positionRef = useRef<HTMLElement>(null)
229
+ const rafRef = useRef<number>()
230
+
231
+ useEffect(() => {
232
+ return () => {
233
+ if (typeof rafRef.current !== 'undefined') {
234
+ window.cancelAnimationFrame(rafRef.current)
235
+ }
236
+ }
237
+ }, [])
238
+
239
+ return {
240
+ positionRef,
241
+ preventLayoutShift(callback: () => void) {
242
+ if (!positionRef.current) {
243
+ return
244
+ }
245
+
246
+ const initialTop = positionRef.current.getBoundingClientRect().top
247
+
248
+ callback()
249
+
250
+ rafRef.current = window.requestAnimationFrame(() => {
251
+ const newTop =
252
+ positionRef.current?.getBoundingClientRect().top ?? initialTop
253
+ window.scrollBy(0, newTop - initialTop)
254
+ })
255
+ },
256
+ }
257
+ }
258
+
259
+ function useTabGroupProps(availableLanguages: Array<string>) {
260
+ const [selectedIndex, setSelectedIndex] = useState(0)
261
+ const [preferredLanguages, setPreferredLanguages] = useState<string[]>([])
262
+
263
+ const activeLanguage = [...availableLanguages].sort(
264
+ (a, z) => preferredLanguages.indexOf(z) - preferredLanguages.indexOf(a),
265
+ )[0]
266
+ const languageIndex = availableLanguages.indexOf(activeLanguage)
267
+ const newSelectedIndex = languageIndex === -1 ? selectedIndex : languageIndex
268
+
269
+ if (newSelectedIndex !== selectedIndex) {
270
+ setSelectedIndex(newSelectedIndex)
271
+ }
272
+
273
+ const { positionRef, preventLayoutShift } = usePreventLayoutShift()
274
+
275
+ return {
276
+ as: 'div' as const,
277
+ ref: positionRef,
278
+ selectedIndex,
279
+ onChange: (newSelectedIndex: number) => {
280
+ preventLayoutShift(() => {
281
+ const newLanguage = availableLanguages[newSelectedIndex]
282
+ setPreferredLanguages(prev => [
283
+ ...prev.filter(lang => lang !== newLanguage),
284
+ newLanguage
285
+ ])
286
+ })
287
+ },
288
+ }
289
+ }
290
+
291
+ const APIGroupContext = createContext(false)
292
+
293
+ export function APIGroup({
294
+ children,
295
+ title,
296
+ ...props
297
+ }: React.ComponentPropsWithoutRef<typeof APIGroupPanels> & { title: string }) {
298
+ const languages =
299
+ Children.map(children, (child) =>
300
+ // @ts-expect-error child.props is not typed
301
+ getPanelTitle(isValidElement(child) ? child.props : {}),
302
+ ) ?? []
303
+ const tabGroupProps = useTabGroupProps(languages)
304
+ const hasTabs = Children.count(children) > 1
305
+
306
+ const containerClassName =
307
+ 'my-6 overflow-hidden rounded-2xl bg-zinc-900 shadow-md dark:ring-1 dark:ring-white/10 w-full'
308
+ const header = (
309
+ <APIGroupHeader title={title} selectedIndex={tabGroupProps.selectedIndex}>
310
+ {children}
311
+ </APIGroupHeader>
312
+ )
313
+ const panels = <APIGroupPanels {...props}>{children}</APIGroupPanels>
314
+
315
+ return (
316
+ <APIGroupContext.Provider value={true}>
317
+ {hasTabs ? (
318
+ <TabGroup {...tabGroupProps} className={containerClassName}>
319
+ <div className="not-prose">
320
+ {header}
321
+ {panels}
322
+ </div>
323
+ </TabGroup>
324
+ ) : (
325
+ <div className={containerClassName}>
326
+ <div className="not-prose">
327
+ {header}
328
+ {panels}
329
+ </div>
330
+ </div>
331
+ )}
332
+ </APIGroupContext.Provider>
333
+ )
334
+ }
335
+
336
+ export function API({
337
+ children,
338
+ ...props
339
+ }: React.ComponentPropsWithoutRef<'code'>) {
340
+ const isGrouped = useContext(APIGroupContext)
341
+
342
+ if (isGrouped) {
343
+ if (typeof children !== 'string') {
344
+ throw new Error(
345
+ '`API` children must be a string when nested inside a `APIGroup`.',
346
+ )
347
+ }
348
+ return <code {...props}>
349
+ {children}
350
+ </code>
351
+ }
352
+
353
+ return <code {...props}>{children}</code>
354
+ }
@@ -0,0 +1,80 @@
1
+ import classNames from "classnames"
2
+ import { Icon } from "./icon"
3
+
4
+ const styles = {
5
+ note: {
6
+ container: 'my-6 flex gap-2.5 rounded-2xl border border-emerald-500/20 bg-emerald-50/50 p-4 text-sm/6 text-emerald-900 dark:border-emerald-500/30 dark:bg-emerald-500/5 dark:text-emerald-200 dark:[--tw-prose-links-hover:var(--color-emerald-300)] dark:[--tw-prose-links:var(--color-white)]',
7
+ title: 'text-emerald-900 dark:text-emerald-200',
8
+ body: 'text-emerald-900 dark:text-emerald-200',
9
+ },
10
+ warning: {
11
+ container: 'my-6 flex gap-2.5 rounded-2xl border border-amber-500/20 bg-amber-50/50 p-4 text-sm/6 text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/5 dark:text-amber-200 dark:[--tw-prose-links-hover:var(--color-amber-300)] dark:[--tw-prose-links:var(--color-white)]',
12
+ title: 'text-amber-900 dark:text-amber-200',
13
+ body: 'text-amber-900 dark:text-amber-200',
14
+ },
15
+ info: {
16
+ container: 'my-6 flex gap-2.5 rounded-2xl border border-blue-500/20 bg-blue-50/50 p-4 text-sm/6 text-blue-900 dark:border-blue-500/30 dark:bg-blue-500/5 dark:text-blue-200 dark:[--tw-prose-links-hover:var(--color-blue-300)] dark:[--tw-prose-links:var(--color-white)]',
17
+ title: 'text-blue-900 dark:text-blue-200',
18
+ body: 'text-blue-900 dark:text-blue-200',
19
+ },
20
+ tip: {
21
+ container: 'my-6 flex gap-2.5 rounded-2xl border border-green-500/20 bg-green-50/50 p-4 text-sm/6 text-green-900 dark:border-green-500/30 dark:bg-green-500/5 dark:text-green-200 dark:[--tw-prose-links-hover:var(--color-green-300)] dark:[--tw-prose-links:var(--color-white)]',
22
+ title: 'text-green-900 dark:text-green-200',
23
+ body: 'text-green-900 dark:text-green-200',
24
+ },
25
+ check: {
26
+ container: 'my-6 flex gap-2.5 rounded-2xl border border-purple-500/20 bg-purple-50/50 p-4 text-sm/6 text-purple-900 dark:border-purple-500/30 dark:bg-purple-500/5 dark:text-purple-200 dark:[--tw-prose-links-hover:var(--color-purple-300)] dark:[--tw-prose-links:var(--color-white)]',
27
+ title: 'text-purple-900 dark:text-purple-200',
28
+ body: 'text-purple-900 dark:text-purple-200',
29
+ },
30
+ }
31
+
32
+ const icons = {
33
+ note: (props: { className?: string }) => <Icon icon="InfoCircle" {...props} />,
34
+ warning: (props: { className?: string }) => <Icon icon="AlertTriangle" {...props} />,
35
+ info: (props: { className?: string }) => <Icon icon="InfoCircle" {...props} />,
36
+ tip: (props: { className?: string }) => <Icon icon="Lightbulb" {...props} />,
37
+ check: (props: { className?: string }) => <Icon icon="CheckCircle" {...props} />,
38
+ }
39
+
40
+ export function Callout({
41
+ title,
42
+ children,
43
+ type = 'note',
44
+ }: {
45
+ title: string
46
+ children: React.ReactNode
47
+ type?: keyof typeof styles
48
+ }) {
49
+ const IconComponent = icons[type]
50
+
51
+ return (
52
+ <div className={classNames(styles[type].container)}>
53
+ <IconComponent className="mt-1 h-4 w-4 flex-none" />
54
+ <div className="[&>:first-child]:mt-0 [&>:last-child]:mb-0">
55
+ <div className={styles[type].title}>{title}</div>
56
+ <div className={styles[type].body}>{children}</div>
57
+ </div>
58
+ </div>
59
+ )
60
+ }
61
+
62
+ export function Note({ children }: { children: React.ReactNode }) {
63
+ return <Callout type="note" title="Note">{children}</Callout>
64
+ }
65
+
66
+ export function Warning({ children }: { children: React.ReactNode }) {
67
+ return <Callout type="warning" title="Warning">{children}</Callout>
68
+ }
69
+
70
+ export function Info({ children }: { children: React.ReactNode }) {
71
+ return <Callout type="info" title="Info">{children}</Callout>
72
+ }
73
+
74
+ export function Tip({ children }: { children: React.ReactNode }) {
75
+ return <Callout type="tip" title="Tip">{children}</Callout>
76
+ }
77
+
78
+ export function Check({ children }: { children: React.ReactNode }) {
79
+ return <Callout type="check" title="Check">{children}</Callout>
80
+ }
@@ -0,0 +1,48 @@
1
+ import classNames from "classnames";
2
+ import { Icon } from "./icon";
3
+ import type { ComponentChildren } from "preact";
4
+
5
+ export function CardGroup({ children, cols = 2 }: { children: React.ReactNode; cols?: number }) {
6
+ return (
7
+ <div className={classNames(
8
+ 'not-prose my-12 grid grid-cols-1 gap-6',
9
+ cols === 2 && 'sm:grid-cols-2',
10
+ cols === 3 && 'sm:grid-cols-3',
11
+ cols === 4 && 'sm:grid-cols-4'
12
+ )}>
13
+ {children}
14
+ </div>
15
+ )
16
+ }
17
+
18
+ export function Card({
19
+ title,
20
+ description,
21
+ href,
22
+ icon,
23
+ children,
24
+ }: {
25
+ title: string
26
+ description: string
27
+ href: string
28
+ icon: string
29
+ children?: ComponentChildren
30
+ }) {
31
+ return (
32
+ <div className="group relative rounded-xl border border-slate-200 dark:border-slate-700">
33
+ <div className="absolute -inset-px rounded-xl border-2 border-transparent opacity-0 [background:linear-gradient(var(--quick-links-hover-bg,var(--color-sky-50)),var(--quick-links-hover-bg,var(--color-sky-50)))_padding-box,linear-gradient(to_top,var(--color-indigo-400),var(--color-cyan-400),var(--color-sky-500))_border-box] group-hover:opacity-100 dark:[--quick-links-hover-bg:var(--color-slate-800)]" />
34
+ <div className="relative overflow-hidden rounded-xl p-6 flex gap-2 items-center">
35
+ <Icon icon={icon} className="h-8 w-8" />
36
+ <h2 className="mt-4 font-display text-base text-slate-900 dark:text-white">
37
+ <a href={href}>
38
+ <span className="absolute -inset-px rounded-xl" />
39
+ {title}
40
+ </a>
41
+ </h2>
42
+ <div className="text-sm text-slate-700 dark:text-slate-400 leading-tight [&>p]:mb-0">
43
+ {description ?? children}
44
+ </div>
45
+ </div>
46
+ </div>
47
+ )
48
+ }