asasvirtuais 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 (86) hide show
  1. package/README.md +78 -0
  2. package/actions/draw.ts +110 -0
  3. package/components/OAuthCard.tsx +346 -0
  4. package/components/icons.tsx +11 -0
  5. package/components/markdown.tsx +18 -0
  6. package/components/stack/list.tsx +21 -0
  7. package/components/stack/menu.tsx +40 -0
  8. package/components/stack/nav.tsx +39 -0
  9. package/components/table/fixed.tsx +59 -0
  10. package/components/table/key-value.tsx +19 -0
  11. package/components/ui/color-mode.tsx +108 -0
  12. package/components/ui/provider.tsx +15 -0
  13. package/components/ui/toaster.tsx +43 -0
  14. package/components/ui/tooltip.tsx +46 -0
  15. package/hooks/useBoolean.tsx +11 -0
  16. package/hooks/useForwardAs.tsx +29 -0
  17. package/hooks/useHash copy.tsx +27 -0
  18. package/hooks/useHash.tsx +27 -0
  19. package/hooks/useIsMobile.tsx +6 -0
  20. package/hooks/useOAuthTokens.ts +97 -0
  21. package/hooks/useOpenRouterModels.ts +80 -0
  22. package/lib/auth0.ts +11 -0
  23. package/lib/blob.ts +3 -0
  24. package/lib/client-token-storage.ts +216 -0
  25. package/lib/oauth-tokens.ts +85 -0
  26. package/lib/react/context.tsx +20 -0
  27. package/lib/react/index.ts +1 -0
  28. package/lib/tools.ts +375 -0
  29. package/next-env.d.ts +5 -0
  30. package/next.config.ts +23 -0
  31. package/package.json +72 -0
  32. package/packages/blob.ts +97 -0
  33. package/packages/chat/components/chat/feed/index.tsx +76 -0
  34. package/packages/chat/components/chat/feed/story.tsx +18 -0
  35. package/packages/chat/components/chat/index.tsx +16 -0
  36. package/packages/chat/components/chat/story.tsx +74 -0
  37. package/packages/chat/components/debug/index.tsx +54 -0
  38. package/packages/chat/components/header/index.tsx +14 -0
  39. package/packages/chat/components/header/menu/index.tsx +63 -0
  40. package/packages/chat/components/header/story.tsx +33 -0
  41. package/packages/chat/components/header/title/index.tsx +35 -0
  42. package/packages/chat/components/index.ts +13 -0
  43. package/packages/chat/components/input/index.tsx +17 -0
  44. package/packages/chat/components/input/menu/index.tsx +35 -0
  45. package/packages/chat/components/input/send.tsx +21 -0
  46. package/packages/chat/components/input/story.tsx +35 -0
  47. package/packages/chat/components/input/textarea/index.tsx +20 -0
  48. package/packages/chat/components/message/file.tsx +103 -0
  49. package/packages/chat/components/message/menu/index.tsx +26 -0
  50. package/packages/chat/components/message/story.tsx +49 -0
  51. package/packages/chat/components/messages/index.tsx +23 -0
  52. package/packages/chat/components/messages/story.tsx +11 -0
  53. package/packages/chat/components/ui/prose.tsx +263 -0
  54. package/packages/chat/edit-message.tsx +49 -0
  55. package/packages/chat/header.tsx +118 -0
  56. package/packages/chat/index.ts +14 -0
  57. package/packages/chat/input.tsx +89 -0
  58. package/packages/chat/message-menu.tsx +57 -0
  59. package/packages/chat/message.tsx +44 -0
  60. package/packages/chat/messages.tsx +44 -0
  61. package/packages/chat/model-selector.tsx +172 -0
  62. package/packages/chat/scenarios.tsx +68 -0
  63. package/packages/chat/settings.tsx +98 -0
  64. package/packages/chat/temperature-slider.tsx +67 -0
  65. package/packages/chat/tool-results.tsx +32 -0
  66. package/packages/crud/core.ts +75 -0
  67. package/packages/crud/fetcher.ts +64 -0
  68. package/packages/crud/index.ts +2 -0
  69. package/packages/crud/next.ts +128 -0
  70. package/packages/crud/react.tsx +365 -0
  71. package/packages/env.ts +8 -0
  72. package/packages/fields.tsx +157 -0
  73. package/packages/firebase.ts +13 -0
  74. package/packages/firestore.ts +51 -0
  75. package/packages/form.tsx +66 -0
  76. package/packages/next.ts +64 -0
  77. package/packages/openrouter.ts +4 -0
  78. package/packages/react/context.tsx +21 -0
  79. package/packages/react/crud.tsx +372 -0
  80. package/packages/react/hooks.ts +90 -0
  81. package/packages/react/store.tsx +20 -0
  82. package/packages/replit-db.ts +219 -0
  83. package/packages/wretch.ts +22 -0
  84. package/packages/yaml.ts +163 -0
  85. package/pnpm-workspace.yaml +4 -0
  86. package/server/db.ts +15 -0
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+
2
+ Database Schema
3
+ ```tsx
4
+ const schema = {
5
+ users: {
6
+ readable: {
7
+ id: z.string(),
8
+ name: z.string(),
9
+ email: z.string().email(),
10
+ },
11
+ writable: {
12
+ name: z.string(),
13
+ email: z.string().email(),
14
+ }
15
+ }
16
+ }
17
+ ```
18
+
19
+ Fields
20
+ ```tsx
21
+ import { fields as modules } from '@/packages/fields'
22
+ const fields = {
23
+ // Only writable fields
24
+ users: {
25
+ name: modules.string,
26
+ email: modules.email,
27
+ }
28
+ }
29
+ ```
30
+
31
+ TODO: REST API Route
32
+ ```tsx
33
+ import { route } from '@/packages/next'
34
+ export default route<{ id: string }>(
35
+ async function(request, params) {
36
+ const [payload, { id }] = await Promise.all([request.json(), params])
37
+ return Response.json(
38
+ routeLogic(id, payload)
39
+ )
40
+ }
41
+ )
42
+ ```
43
+
44
+ CreateForm, UpdateForm and FiltersForm
45
+ ```tsx
46
+ function MyFormGoesHere() {
47
+ return (
48
+ <FiltersForm table={} onSuccess={handleResult}>
49
+ {/* TODO: handle forms with function as child */}
50
+ {({loading, result, error}) => {
51
+ return (
52
+ <Stack>
53
+ <FiltersFields/>
54
+ {/* TODO: SubmitButton with loading spinner */}
55
+ <SubmitButton disabled={loading} loading={loading}>
56
+ {loading ? <Spinner/> : 'Apply Filters'}
57
+ </SubmitButton>
58
+ {result.map(item => <ViewItem key={item.id} item={item} />)}
59
+ </Stack>
60
+ )
61
+ }}
62
+ </FiltersForm>
63
+ )
64
+ }
65
+ ```
66
+
67
+ Using the DatabaseProvider
68
+ ```tsx
69
+ const index = Object.fromEntries((await server.users.list({})).map(user => [user.id, user]))
70
+ <DatabaseProvider users={index}>
71
+ {...}
72
+ </DatabaseProvider>
73
+ ```
74
+
75
+ Listing Query
76
+ ```tsx
77
+ const array = await server.messages.list({query: { id }})
78
+ ```
@@ -0,0 +1,110 @@
1
+ 'use server'
2
+ import JSZip from 'jszip'
3
+ import { uploadBuffer } from '@/packages/blob'
4
+
5
+ const NOVELAI_API_URL = 'https://image.novelai.net/ai/generate-image'
6
+ const NOVELAI_API_TOKEN = process.env.NOVELAI_API_TOKEN
7
+
8
+ export interface DrawParams {
9
+ prompt?: string
10
+ orientation?: 'portrait' | 'landscape' | 'square'
11
+ }
12
+
13
+ const RESOLUTION_MAP = {
14
+ portrait: { width: 832, height: 1216 },
15
+ landscape: { width: 1216, height: 832 },
16
+ square: { width: 1024, height: 1024 }
17
+ }
18
+
19
+ export async function drawAction({ prompt, orientation = 'square' }: DrawParams) {
20
+ // Following the exact NovelAI API format from the website
21
+
22
+ const enhancedPrompt = `masterpiece, best quality, ${prompt}`
23
+
24
+ const { width = 1024, height = 1024 } = RESOLUTION_MAP[orientation]
25
+
26
+ const payload = {
27
+ input: enhancedPrompt,
28
+ model: 'nai-diffusion-4-5-full',
29
+ action: "generate",
30
+ parameters: {
31
+ params_version: 3,
32
+ width,
33
+ height,
34
+ scale: 5,
35
+ sampler: 'k_euler_ancestral',
36
+ steps: 28,
37
+ n_samples: 1,
38
+ ucPreset: 4,
39
+ qualityToggle: false,
40
+ autoSmea: false,
41
+ dynamic_thresholding: false,
42
+ controlnet_strength: 1,
43
+ legacy: false,
44
+ add_original_image: true,
45
+ cfg_rescale: 0,
46
+ noise_schedule: "karras",
47
+ legacy_v3_extend: false,
48
+ skip_cfg_above_sigma: 58,
49
+ use_coords: true,
50
+ normalize_reference_strength_multiple: true,
51
+ inpaintImg2ImgStrength: 1,
52
+ v4_prompt: {
53
+ caption: {
54
+ base_caption: enhancedPrompt,
55
+ char_captions: []
56
+ },
57
+ use_coords: true,
58
+ use_order: true
59
+ },
60
+ v4_negative_prompt: {
61
+ caption: {
62
+ base_caption: "lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry",
63
+ char_captions: []
64
+ },
65
+ legacy_uc: false
66
+ },
67
+ uc: "",
68
+ seed: Math.floor(Math.random() * 4294967295),
69
+ legacy_uc: false,
70
+ characterPrompts: [],
71
+ deliberate_euler_ancestral_bug: false,
72
+ prefer_brownian: true
73
+ }
74
+ }
75
+
76
+ const response = await fetch(NOVELAI_API_URL, {
77
+ method: 'POST',
78
+ headers: {
79
+ 'Authorization': `Bearer ${NOVELAI_API_TOKEN}`,
80
+ 'Content-Type': 'application/json',
81
+ 'Accept': 'application/x-zip-compressed',
82
+ },
83
+ body: JSON.stringify(payload),
84
+ })
85
+
86
+ if (!response.ok) {
87
+ throw new Error(`NovelAI API error: ${response.statusText}`)
88
+ }
89
+
90
+ const zipBuffer = await response.arrayBuffer()
91
+ const zip = await JSZip.loadAsync(zipBuffer)
92
+
93
+ const imageFile = Object.values(zip.files)[0]
94
+ if (!imageFile) {
95
+ throw new Error('No image found in response')
96
+ }
97
+
98
+ // get binary buffer (node buffer / Uint8Array)
99
+ const bufferData = await imageFile.async('nodebuffer') as Uint8Array
100
+ const buffer = Buffer.from(bufferData)
101
+
102
+ // Use filename from zip entry if available, otherwise generate one
103
+ const filenameBase = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}.png`
104
+ const filename = `novelai/${filenameBase}`
105
+
106
+ // Upload to configured bucket and return public URL
107
+ const publicUrl = await uploadBuffer(buffer, filename, 'image/png')
108
+ return publicUrl
109
+
110
+ }
@@ -0,0 +1,346 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { useSearchParams } from 'next/navigation'
5
+ import {
6
+ Card,
7
+ VStack,
8
+ HStack,
9
+ Heading,
10
+ Text,
11
+ Button,
12
+ Badge,
13
+ Icon,
14
+ Box,
15
+ Code,
16
+ Collapsible,
17
+ IconButton
18
+ } from '@chakra-ui/react'
19
+ import {
20
+ FaGoogle,
21
+ FaFacebook,
22
+ FaInstagram,
23
+ FaChartLine,
24
+ FaLink,
25
+ FaUnlink,
26
+ FaSync,
27
+ FaCopy,
28
+ FaChevronDown,
29
+ FaChevronUp
30
+ } from 'react-icons/fa'
31
+ import { IconType } from 'react-icons'
32
+ import { toaster } from '@/components/ui/toaster'
33
+ import { clientTokenStorage } from '@/lib/client-token-storage'
34
+
35
+ const iconMap: Record<string, IconType> = {
36
+ FaGoogle,
37
+ FaFacebook,
38
+ FaInstagram,
39
+ FaChartLine
40
+ }
41
+
42
+ interface Platform {
43
+ id: string
44
+ name: string
45
+ icon: string
46
+ description: string
47
+ scopes: string[]
48
+ color: string
49
+ }
50
+
51
+ interface OAuthCardProps {
52
+ platform: Platform
53
+ userEmail?: string
54
+ }
55
+
56
+ export default function OAuthCard({ platform, userEmail }: OAuthCardProps) {
57
+ const [isConnected, setIsConnected] = useState(false)
58
+ const [isLoading, setIsLoading] = useState(false)
59
+ const [token, setToken] = useState<string | null>(null)
60
+ const [showToken, setShowToken] = useState(false)
61
+ const searchParams = useSearchParams()
62
+
63
+ const IconComponent = iconMap[platform.icon] || FaLink
64
+
65
+ useEffect(() => {
66
+ const success = searchParams.get('success')
67
+ const error = searchParams.get('error')
68
+ const connectedPlatform = searchParams.get('platform')
69
+ const encodedToken = searchParams.get('token')
70
+
71
+ if (success === 'true' && connectedPlatform === platform.id) {
72
+ setIsConnected(true)
73
+
74
+ // Store token in IndexedDB if provided
75
+ if (encodedToken) {
76
+ try {
77
+ const tokenData = JSON.parse(Buffer.from(encodedToken, 'base64').toString())
78
+ clientTokenStorage.storeToken({
79
+ id: platform.id,
80
+ platform: platform.name,
81
+ accessToken: tokenData.accessToken,
82
+ expiresAt: tokenData.expiresAt,
83
+ lastRefreshed: new Date().toISOString()
84
+ })
85
+ setToken(tokenData.accessToken)
86
+ } catch (err) {
87
+ console.error('Failed to store token:', err)
88
+ }
89
+ }
90
+
91
+ toaster.create({
92
+ title: 'Connected successfully',
93
+ description: `${platform.name} has been connected`,
94
+ type: 'success',
95
+ duration: 5000,
96
+ })
97
+ }
98
+
99
+ if (error && connectedPlatform === platform.id) {
100
+ toaster.create({
101
+ title: 'Connection failed',
102
+ description: `Failed to connect ${platform.name}: ${error}`,
103
+ type: 'error',
104
+ duration: 5000,
105
+ })
106
+ }
107
+ }, [searchParams, platform])
108
+
109
+ // Load token from IndexedDB on mount
110
+ useEffect(() => {
111
+ const loadToken = async () => {
112
+ try {
113
+ const storedToken = await clientTokenStorage.getToken(platform.id)
114
+ if (storedToken) {
115
+ setToken(storedToken.accessToken)
116
+ setIsConnected(true)
117
+ }
118
+ } catch (err) {
119
+ console.error('Failed to load token:', err)
120
+ }
121
+ }
122
+ loadToken()
123
+ }, [platform.id])
124
+
125
+ const handleConnect = async () => {
126
+ setIsLoading(true)
127
+ try {
128
+ const params = new URLSearchParams({
129
+ platform: platform.id,
130
+ scopes: platform.scopes.join(','),
131
+ userEmail: userEmail || ''
132
+ })
133
+
134
+ window.location.href = `/api/oauth/connect?${params.toString()}`
135
+ } catch (error) {
136
+ toaster.create({
137
+ title: 'Connection failed',
138
+ description: 'Unable to initiate OAuth connection',
139
+ type: 'error',
140
+ duration: 5000,
141
+ })
142
+ setIsLoading(false)
143
+ }
144
+ }
145
+
146
+ const handleDisconnect = async () => {
147
+ setIsLoading(true)
148
+ try {
149
+ const response = await fetch(`/api/oauth/disconnect`, {
150
+ method: 'POST',
151
+ headers: { 'Content-Type': 'application/json' },
152
+ body: JSON.stringify({ platform: platform.id })
153
+ })
154
+
155
+ if (!response.ok) throw new Error('Disconnection failed')
156
+
157
+ setIsConnected(false)
158
+ setToken(null)
159
+
160
+ // Remove token from IndexedDB
161
+ await clientTokenStorage.removeToken(platform.id)
162
+
163
+ toaster.create({
164
+ title: 'Disconnected',
165
+ description: `${platform.name} has been disconnected`,
166
+ type: 'success',
167
+ duration: 3000,
168
+ })
169
+ } catch (error) {
170
+ toaster.create({
171
+ title: 'Disconnection failed',
172
+ description: 'Unable to disconnect platform',
173
+ type: 'error',
174
+ duration: 5000,
175
+ })
176
+ } finally {
177
+ setIsLoading(false)
178
+ }
179
+ }
180
+
181
+ const handleRefreshToken = async () => {
182
+ setIsLoading(true)
183
+ try {
184
+ const response = await fetch(`/api/oauth/refresh`, {
185
+ method: 'POST',
186
+ headers: { 'Content-Type': 'application/json' },
187
+ body: JSON.stringify({ platform: platform.id })
188
+ })
189
+
190
+ if (!response.ok) throw new Error('Token refresh failed')
191
+
192
+ const data = await response.json()
193
+ setToken(data.accessToken)
194
+
195
+ // Update token in IndexedDB
196
+ await clientTokenStorage.storeToken({
197
+ id: platform.id,
198
+ platform: platform.name,
199
+ accessToken: data.accessToken,
200
+ expiresAt: data.expiresIn ?
201
+ new Date(Date.now() + data.expiresIn * 1000).toISOString() :
202
+ undefined,
203
+ lastRefreshed: new Date().toISOString()
204
+ })
205
+
206
+ toaster.create({
207
+ title: 'Token refreshed',
208
+ description: 'Your access token has been refreshed',
209
+ type: 'success',
210
+ duration: 3000,
211
+ })
212
+ } catch (error) {
213
+ toaster.create({
214
+ title: 'Refresh failed',
215
+ description: 'Unable to refresh token',
216
+ type: 'error',
217
+ duration: 5000,
218
+ })
219
+ } finally {
220
+ setIsLoading(false)
221
+ }
222
+ }
223
+
224
+ const copyToken = () => {
225
+ if (token) {
226
+ navigator.clipboard.writeText(token)
227
+ toaster.create({
228
+ title: 'Copied!',
229
+ description: 'Token copied to clipboard',
230
+ type: 'success',
231
+ duration: 2000,
232
+ })
233
+ }
234
+ }
235
+
236
+ return (
237
+ <Card.Root variant="outline" size="lg">
238
+ <Card.Header>
239
+ <HStack justify="space-between">
240
+ <HStack gap={3}>
241
+ <Icon as={IconComponent} boxSize={6} color={platform.color} />
242
+ <Heading size="md">{platform.name}</Heading>
243
+ </HStack>
244
+ <Badge colorScheme={isConnected ? 'green' : 'gray'}>
245
+ {isConnected ? 'Connected' : 'Not Connected'}
246
+ </Badge>
247
+ </HStack>
248
+ </Card.Header>
249
+
250
+ <Card.Body>
251
+ <VStack align="stretch" gap={4}>
252
+ <Text color="fg.muted" fontSize="sm">
253
+ {platform.description}
254
+ </Text>
255
+
256
+ <Box>
257
+ <Text fontSize="sm" fontWeight="medium" mb={1}>Scopes:</Text>
258
+ <Text fontSize="xs" color="fg.muted" lineClamp={2}>
259
+ {platform.scopes.join(', ')}
260
+ </Text>
261
+ </Box>
262
+
263
+ <HStack gap={2}>
264
+ {!isConnected ? (
265
+ <Button
266
+ colorScheme="blue"
267
+ onClick={handleConnect}
268
+ loading={isLoading}
269
+ loadingText="Connecting..."
270
+ size="sm"
271
+ width="full"
272
+ >
273
+ <FaLink />
274
+ Connect
275
+ </Button>
276
+ ) : (
277
+ <>
278
+ <Button
279
+ variant="outline"
280
+ onClick={handleRefreshToken}
281
+ loading={isLoading}
282
+ size="sm"
283
+ flex={1}
284
+ >
285
+ <FaSync />
286
+ Refresh
287
+ </Button>
288
+ <Button
289
+ variant="outline"
290
+ colorScheme="red"
291
+ onClick={handleDisconnect}
292
+ loading={isLoading}
293
+ size="sm"
294
+ flex={1}
295
+ >
296
+ <FaUnlink />
297
+ Disconnect
298
+ </Button>
299
+ </>
300
+ )}
301
+ </HStack>
302
+
303
+ {isConnected && token && (
304
+ <Box>
305
+ <HStack justify="space-between" mb={2}>
306
+ <Text fontSize="sm" fontWeight="medium">Access Token:</Text>
307
+ <HStack gap={1}>
308
+ <IconButton
309
+ aria-label="Copy token"
310
+ size="xs"
311
+ variant="ghost"
312
+ onClick={copyToken}
313
+ >
314
+ <FaCopy />
315
+ </IconButton>
316
+ <IconButton
317
+ aria-label="Toggle token visibility"
318
+ size="xs"
319
+ variant="ghost"
320
+ onClick={() => setShowToken(!showToken)}
321
+ >
322
+ {showToken ? <FaChevronUp /> : <FaChevronDown />}
323
+ </IconButton>
324
+ </HStack>
325
+ </HStack>
326
+ <Collapsible.Root open={showToken}>
327
+ <Collapsible.Content>
328
+ <Code
329
+ p={3}
330
+ borderRadius="md"
331
+ fontSize="xs"
332
+ display="block"
333
+ overflowX="auto"
334
+ whiteSpace="pre"
335
+ >
336
+ {token}
337
+ </Code>
338
+ </Collapsible.Content>
339
+ </Collapsible.Root>
340
+ </Box>
341
+ )}
342
+ </VStack>
343
+ </Card.Body>
344
+ </Card.Root>
345
+ )
346
+ }
@@ -0,0 +1,11 @@
1
+ 'use client'
2
+
3
+ import { forwardRef } from 'react'
4
+
5
+ export const AirtableIcon = forwardRef<SVGSVGElement>((props, ref) => (
6
+ <svg ref={ref} viewBox='0 0 20 16' xmlns='http://www.w3.org/2000/svg' {...props}><path d='m8.50297526.20100591-7.09941044 2.93764625c-.39480037.16338794-.39071029.72415946.00656565.88162754l7.12911731 2.82710645c.62642805.24841855 1.32400201.24841855 1.95032242 0l7.129225-2.82710645c.3971683-.15746808.401366-.7182396.006458-.88162754l-7.0993028-2.93764625c-.6477395-.26800788-1.37534322-.26800788-2.02297514 0' fill='#ffbf00'/><path d='m10.1469076 8.39120841v7.06249209c0 .3359247.3387232.565938.6509686.4421592l7.9440119-3.0834899c.1813628-.0718993.300298-.2471269.300298-.4421592v-7.06249207c0-.33592474-.3387232-.56593793-.6509685-.44215918l-7.944012 3.08348988c-.1812551.07189931-.300298.24712699-.300298.44215918' fill='#26b5f8'/><path d='m8.29192714 8.75561302-2.35760833 1.13833392-.23937732.11570616-4.97676647 2.3846244c-.31547434.152194-.71813197-.0777115-.71813197-.4281668v-6.62872832c0-.12679249.06501075-.23625595.15219404-.31870335.03638019-.03648782.07760389-.06651762.1204421-.09030467.11893522-.07136113.28856591-.09041229.43279506-.03336644l7.54684367 2.99017147c.38360647.15219408.41374392.68971668.03960922.87043363' fill='#ed3049'/><path d='m8.29192714 8.75561302-2.35760833 1.13833392-5.78208172-4.87526791c.03638019-.03648782.07760389-.06651762.1204421-.09030467.11893522-.07136113.28856591-.09041229.43279506-.03336644l7.54684367 2.99017147c.38360647.15219408.41374392.68971668.03960922.87043363' fillOpacity='.25'/></svg>
7
+ ))
8
+
9
+ export const LinkedInIcon = forwardRef<SVGSVGElement>((props, ref) => (
10
+ <svg ref={ref} height='72' viewBox='0 0 72 72' width='72' xmlns='http://www.w3.org/2000/svg' {...props}><g fill='none' fillRule='evenodd'><path d='M8,72 L64,72 C68.418278,72 72,68.418278 72,64 L72,8 C72,3.581722 68.418278,-8.11624501e-16 64,0 L8,0 C3.581722,8.11624501e-16 -5.41083001e-16,3.581722 0,8 L0,64 C5.41083001e-16,68.418278 3.581722,72 8,72 Z' fill='#007EBB'/><path d='M62,62 L51.315625,62 L51.315625,43.8021149 C51.315625,38.8127542 49.4197917,36.0245323 45.4707031,36.0245323 C41.1746094,36.0245323 38.9300781,38.9261103 38.9300781,43.8021149 L38.9300781,62 L28.6333333,62 L28.6333333,27.3333333 L38.9300781,27.3333333 L38.9300781,32.0029283 C38.9300781,32.0029283 42.0260417,26.2742151 49.3825521,26.2742151 C56.7356771,26.2742151 62,30.7644705 62,40.051212 L62,62 Z M16.349349,22.7940133 C12.8420573,22.7940133 10,19.9296567 10,16.3970067 C10,12.8643566 12.8420573,10 16.349349,10 C19.8566406,10 22.6970052,12.8643566 22.6970052,16.3970067 C22.6970052,19.9296567 19.8566406,22.7940133 16.349349,22.7940133 Z M11.0325521,62 L21.769401,62 L21.769401,27.3333333 L11.0325521,27.3333333 L11.0325521,62 Z' fill='#FFF'/></g></svg>
11
+ ))
@@ -0,0 +1,18 @@
1
+ 'use client'
2
+ import ReactMarkdown from 'react-markdown'
3
+ import gfm from 'remark-gfm'
4
+ import breaks from 'remark-breaks'
5
+ import { Link, Text } from '@chakra-ui/react'
6
+
7
+ export default function Markdown({ children } : {children: string}) {
8
+ return (
9
+ <ReactMarkdown
10
+ components={{
11
+ a: props => <Link target='_blank' {...props}/>,
12
+ // @ts-expect-error
13
+ em: props => <Text as='i' fontStyle='italic' {...props} />,
14
+ }}
15
+ remarkPlugins={[breaks, gfm]}
16
+ >{children}</ReactMarkdown>
17
+ )
18
+ }
@@ -0,0 +1,21 @@
1
+ 'use client'
2
+ import { forwardRef } from 'react'
3
+ import { List, Stack, HStack, VStack } from '@chakra-ui/react'
4
+ import useForwardAs from '@/hooks/useForwardAs'
5
+
6
+ export type StackListProps = List.RootProps
7
+
8
+ export const StackList = forwardRef<typeof Stack, StackListProps>(({ as, ...props }, ref) => (
9
+ <List.Root
10
+ // @ts-expect-error
11
+ ref={ref}
12
+ as={useForwardAs(Stack, as)} {...props} />
13
+ ))
14
+
15
+ export const HList = forwardRef<typeof HStack, StackListProps>(({ as, ...props }, ref) => (
16
+ <StackList ref={ref} as={useForwardAs(HStack, as)} {...props} />
17
+ ))
18
+
19
+ export const VList = forwardRef<typeof HStack, StackListProps>(({ as, ...props }, ref) => (
20
+ <StackList ref={ref} as={useForwardAs(VStack, as)} {...props} />
21
+ ))
@@ -0,0 +1,40 @@
1
+ 'use client'
2
+ import { ElementType, forwardRef } from 'react'
3
+ import { StackList, VList, HList, StackListProps } from '@/components/stack/list'
4
+ import useForwardAs from '@/hooks/useForwardAs'
5
+
6
+ export type StackMenuProps = StackListProps
7
+
8
+ export const StackMenu = forwardRef<typeof StackList, StackMenuProps>(({ children, as, ...props }, ref) => (
9
+ <StackList
10
+ // @ts-expect-error
11
+ ref={ref}
12
+ as={as ? useForwardAs(as, 'menu') : 'menu'}
13
+ // List Defaults
14
+ p={0} m={0} listStyleType='none'
15
+ // Alignment
16
+ justifyContent='space-between'
17
+ {...props}>{children}</StackList>
18
+ ))
19
+
20
+ export const HMenu = forwardRef<typeof HList, StackMenuProps>(({ children, ...props }, ref) => (
21
+ <StackMenu ref={ref} as={HList} {...props}>{children}</StackMenu>
22
+ ))
23
+
24
+ export const VMenu = forwardRef<typeof VList, StackMenuProps>(({ children, ...props }, ref) => (
25
+ <StackMenu ref={ref} as={VList} {...props}>{children}</StackMenu>
26
+ ))
27
+
28
+ // Menu Item, keep it here in this file
29
+ import { Button, ButtonProps, ListItem } from '@chakra-ui/react'
30
+
31
+ export type MenuItemProps = ButtonProps
32
+ export const MenuItem = forwardRef<ElementType, MenuItemProps>(({ children, ...props }, ref) => (
33
+ <ListItem
34
+ // @ts-expect-error
35
+ ref={ref}>
36
+ <Button size='sm' variant='ghost' {...props}>
37
+ {children}
38
+ </Button>
39
+ </ListItem>
40
+ ))
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+ import { forwardRef } from 'react'
3
+ import { StackList, VList, HList, StackListProps } from '@/components/stack/list'
4
+ import useForwardAs from '@/hooks/useForwardAs'
5
+
6
+ export type StackNavProps = StackListProps
7
+
8
+ export const StackNav = forwardRef<typeof StackList, StackListProps>(({ children, as, ...props }, ref) => (
9
+ <StackList
10
+ // @ts-expect-error
11
+ ref={ref}
12
+ as={as ? useForwardAs(as, 'nav') : 'nav'}
13
+ // List Defaults
14
+ p={0} m={0} listStyleType='none'
15
+ // Alignment
16
+ justifyContent='space-between'
17
+ {...props}>{children}</StackList>
18
+ ))
19
+
20
+ export const HNav = forwardRef<typeof HList, StackListProps>(({ children, ...props }, ref) => (
21
+ <StackNav ref={ref} as={HList} {...props}>{children}</StackNav>
22
+ ))
23
+
24
+ export const VNav = forwardRef<typeof VList, StackListProps>(({ children, ...props }, ref) => (
25
+ <StackNav ref={ref} as={VList} {...props}>{children}</StackNav>
26
+ ))
27
+
28
+ import { ListItem, ListItemProps, Link, LinkProps } from '@chakra-ui/react'
29
+
30
+ export type NavItemProps = LinkProps
31
+
32
+ export const NavItem = forwardRef<typeof Link, NavItemProps>(({ children, as, ...props }, ref) => (
33
+ <ListItem
34
+ // @ts-expect-error
35
+ ref={ref}
36
+ {...props}>
37
+ <Link {...props}>{children}</Link>
38
+ </ListItem>
39
+ ))