@rwoll/canvas-kit 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +506 -2
- package/cli/canvas-kit.mjs +148 -0
- package/cli/lib.mjs +697 -0
- package/cli/release.json +4 -0
- package/dist/client.d.ts +72 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +156 -0
- package/dist/client.js.map +1 -0
- package/dist/define.d.ts +249 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.js +197 -0
- package/dist/define.js.map +1 -0
- package/dist/errors.d.ts +12 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +39 -0
- package/dist/errors.js.map +1 -0
- package/dist/extension.d.ts +25 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +70 -0
- package/dist/extension.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs.d.ts +56 -0
- package/dist/jobs.d.ts.map +1 -0
- package/dist/jobs.js +33 -0
- package/dist/jobs.js.map +1 -0
- package/dist/playwright.d.ts +79 -0
- package/dist/playwright.d.ts.map +1 -0
- package/dist/playwright.js +124 -0
- package/dist/playwright.js.map +1 -0
- package/dist/react.d.ts +55 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +72 -0
- package/dist/react.js.map +1 -0
- package/dist/server/headless.d.ts +20 -0
- package/dist/server/headless.d.ts.map +1 -0
- package/dist/server/headless.js +35 -0
- package/dist/server/headless.js.map +1 -0
- package/dist/server/host.d.ts +146 -0
- package/dist/server/host.d.ts.map +1 -0
- package/dist/server/host.js +1078 -0
- package/dist/server/host.js.map +1 -0
- package/dist/server/index.d.ts +9 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +5 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/release.d.ts +30 -0
- package/dist/server/release.d.ts.map +1 -0
- package/dist/server/release.js +71 -0
- package/dist/server/release.js.map +1 -0
- package/dist/server/storage.d.ts +63 -0
- package/dist/server/storage.d.ts.map +1 -0
- package/dist/server/storage.js +253 -0
- package/dist/server/storage.js.map +1 -0
- package/dist/testing.d.ts +49 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +42 -0
- package/dist/testing.js.map +1 -0
- package/dist/vite.d.ts +15 -0
- package/dist/vite.d.ts.map +1 -0
- package/dist/vite.js +213 -0
- package/dist/vite.js.map +1 -0
- package/package.json +115 -3
- package/styles.css +122 -0
- package/templates/canvas/README.md +19 -0
- package/templates/canvas/e2e/canvas.spec.ts +15 -0
- package/templates/canvas/index.html +13 -0
- package/templates/canvas/package.json +29 -0
- package/templates/canvas/playwright.config.ts +5 -0
- package/templates/canvas/server/canvas.test.ts +22 -0
- package/templates/canvas/server/canvas.ts +27 -0
- package/templates/canvas/src/App.css +21 -0
- package/templates/canvas/src/App.tsx +31 -0
- package/templates/canvas/src/index.css +3 -0
- package/templates/canvas/src/main.tsx +11 -0
- package/templates/canvas/tsconfig.app.json +1 -0
- package/templates/canvas/tsconfig.json +7 -0
- package/templates/canvas/tsconfig.node.json +1 -0
- package/templates/canvas/vite.config.ts +7 -0
- package/templates/ui/README.md +18 -0
- package/templates/ui/package.json +22 -0
- package/templates/ui/src/Checklist.tsx +42 -0
- package/templates/ui/src/Header.tsx +17 -0
- package/templates/ui/src/Markdown.tsx +140 -0
- package/templates/ui/src/TextButton.tsx +6 -0
- package/templates/ui/src/index.ts +4 -0
- package/templates/ui/theme.css +143 -0
- package/templates/ui/tsconfig.json +18 -0
- package/tsconfig/canvas.json +27 -0
- package/tsconfig/node.json +24 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "__DESCRIPTION__",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"dev": "vite",
|
|
9
|
+
"build": "tsc -b && vite build",
|
|
10
|
+
"typecheck": "tsc -b",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"e2e": "playwright test",
|
|
13
|
+
"playwright:install": "playwright install chromium"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@rwoll/canvas-kit": "",
|
|
17
|
+
"react": "",
|
|
18
|
+
"react-dom": ""
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@playwright/test": "",
|
|
22
|
+
"@types/node": "",
|
|
23
|
+
"@types/react": "",
|
|
24
|
+
"@types/react-dom": "",
|
|
25
|
+
"typescript": "",
|
|
26
|
+
"vite": "",
|
|
27
|
+
"vitest": ""
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createTestCanvas } from '@rwoll/canvas-kit/testing'
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
3
|
+
import canvas from './canvas.ts'
|
|
4
|
+
|
|
5
|
+
describe('__TITLE__ canvas', () => {
|
|
6
|
+
let close: (() => Promise<void>) | undefined
|
|
7
|
+
afterEach(() => close?.())
|
|
8
|
+
|
|
9
|
+
it('counts and resets', async () => {
|
|
10
|
+
const harness = await createTestCanvas(canvas)
|
|
11
|
+
close = harness.close
|
|
12
|
+
await harness.open('test')
|
|
13
|
+
expect(harness.state('test')).toEqual({ count: 0 })
|
|
14
|
+
|
|
15
|
+
await harness.invoke('test', 'increment', { by: 2 })
|
|
16
|
+
await harness.invoke('test', 'increment', {}, 'ui')
|
|
17
|
+
expect(harness.state('test')).toEqual({ count: 3 })
|
|
18
|
+
|
|
19
|
+
await harness.invoke('test', 'reset')
|
|
20
|
+
expect(await harness.invoke('test', 'get_state')).toEqual({ count: 0 })
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { defineCanvas, z } from '@rwoll/canvas-kit'
|
|
2
|
+
|
|
3
|
+
export default defineCanvas({
|
|
4
|
+
id: '__CANVAS_ID__',
|
|
5
|
+
title: '__TITLE__',
|
|
6
|
+
description: '__DESCRIPTION__',
|
|
7
|
+
// State shared with the UI. It is validated on every change.
|
|
8
|
+
state: z.object({
|
|
9
|
+
count: z.number().int().min(0),
|
|
10
|
+
}),
|
|
11
|
+
// Optional open input, validated before init():
|
|
12
|
+
// input: z.object({ path: z.string() }),
|
|
13
|
+
init: () => ({ count: 0 }),
|
|
14
|
+
// Optional private persistence keyed by domain identity:
|
|
15
|
+
// persist: { key: ({ input }) => input.path },
|
|
16
|
+
actions: ({ action }) => ({
|
|
17
|
+
increment: action({
|
|
18
|
+
description: 'Increase the counter shown in the canvas.',
|
|
19
|
+
input: z.object({ by: z.number().int().min(1).max(100).default(1) }),
|
|
20
|
+
run: (ctx, { by }) => ctx.setState((state) => ({ count: state.count + by })),
|
|
21
|
+
}),
|
|
22
|
+
reset: action({
|
|
23
|
+
description: 'Reset the counter to zero.',
|
|
24
|
+
run: (ctx) => ctx.setState({ count: 0 }),
|
|
25
|
+
}),
|
|
26
|
+
}),
|
|
27
|
+
})
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
main {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
align-items: flex-start;
|
|
5
|
+
gap: 16px;
|
|
6
|
+
max-width: 40rem;
|
|
7
|
+
margin: 0 auto;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
h1 {
|
|
11
|
+
margin: 0;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
p {
|
|
15
|
+
margin: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.connection {
|
|
19
|
+
color: var(--muted);
|
|
20
|
+
font-size: 0.85rem;
|
|
21
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { createCanvasHooks } from '@rwoll/canvas-kit/react'
|
|
2
|
+
import type canvas from '../server/canvas.ts'
|
|
3
|
+
import './App.css'
|
|
4
|
+
|
|
5
|
+
const { useCanvas, useAction } = createCanvasHooks<typeof canvas>()
|
|
6
|
+
|
|
7
|
+
const TITLE = '__TITLE__'
|
|
8
|
+
|
|
9
|
+
function App() {
|
|
10
|
+
const { state, status } = useCanvas()
|
|
11
|
+
const [increment, { pending, error }] = useAction('increment')
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<main>
|
|
15
|
+
<h1>{TITLE}</h1>
|
|
16
|
+
<p>
|
|
17
|
+
Edit <code>src/App.tsx</code> for the UI and <code>server/canvas.ts</code> for state and
|
|
18
|
+
actions.
|
|
19
|
+
</p>
|
|
20
|
+
<button type="button" disabled={pending} onClick={() => increment()}>
|
|
21
|
+
Count is {state.count}
|
|
22
|
+
</button>
|
|
23
|
+
{error && <p role="alert">{error.message}</p>}
|
|
24
|
+
<p className="connection" role="status">
|
|
25
|
+
{status === 'live' ? 'Live' : status === 'connecting' ? 'Connecting…' : 'Offline'}
|
|
26
|
+
</p>
|
|
27
|
+
</main>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default App
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import '@rwoll/canvas-kit/styles.css'
|
|
4
|
+
import './index.css'
|
|
5
|
+
import App from './App.tsx'
|
|
6
|
+
|
|
7
|
+
createRoot(document.getElementById('root')!).render(
|
|
8
|
+
<StrictMode>
|
|
9
|
+
<App />
|
|
10
|
+
</StrictMode>,
|
|
11
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "extends": "@rwoll/canvas-kit/tsconfig/canvas.json" }
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "extends": "@rwoll/canvas-kit/tsconfig/node.json" }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# __UI_NAME__
|
|
2
|
+
|
|
3
|
+
React components and the theme shared by every canvas in this marketplace. Change
|
|
4
|
+
the brand once in `theme.css`, and add components here when two canvases need the
|
|
5
|
+
same UI.
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
// canvases/<name>/src/main.tsx
|
|
9
|
+
import '@rwoll/canvas-kit/styles.css'
|
|
10
|
+
import '__UI_NAME__/theme.css'
|
|
11
|
+
|
|
12
|
+
// canvases/<name>/src/App.tsx
|
|
13
|
+
import { Checklist, Header, Markdown, TextButton } from '__UI_NAME__'
|
|
14
|
+
|
|
15
|
+
<Markdown label="Notes">{state.notes}</Markdown> // Markdown the agent writes, safely
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`canvas-kit new` adds this package to every new canvas.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__UI_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "React components and the theme shared by every canvas in this marketplace.",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./theme.css": "./theme.css"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"typecheck": "tsc -p ."
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"react": "^19.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/react": "",
|
|
19
|
+
"react": "",
|
|
20
|
+
"typescript": ""
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface ChecklistItem {
|
|
4
|
+
id: string
|
|
5
|
+
label: string
|
|
6
|
+
done: boolean
|
|
7
|
+
/** Shown at the end of the row, such as a status or a count. */
|
|
8
|
+
note?: ReactNode
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ChecklistProps {
|
|
12
|
+
/** Accessible name of the list, such as "Blockers". */
|
|
13
|
+
label: string
|
|
14
|
+
items: readonly ChecklistItem[]
|
|
15
|
+
/** Called when a row is checked or unchecked. Without it, the boxes are read-only. */
|
|
16
|
+
onToggle?: (id: string, done: boolean) => void
|
|
17
|
+
/** Shown instead of the list when there are no items. */
|
|
18
|
+
empty?: ReactNode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A list of items to check off, shared by every canvas that tracks work. */
|
|
22
|
+
export function Checklist({ label, items, onToggle, empty = 'Nothing here yet.' }: ChecklistProps) {
|
|
23
|
+
if (items.length === 0) return <p className="ui-empty">{empty}</p>
|
|
24
|
+
return (
|
|
25
|
+
<ul className="ui-checklist" aria-label={label}>
|
|
26
|
+
{items.map((item) => (
|
|
27
|
+
<li key={item.id} className={item.done ? 'done' : undefined}>
|
|
28
|
+
<label>
|
|
29
|
+
<input
|
|
30
|
+
type="checkbox"
|
|
31
|
+
checked={item.done}
|
|
32
|
+
disabled={!onToggle}
|
|
33
|
+
onChange={(event) => onToggle?.(item.id, event.target.checked)}
|
|
34
|
+
/>
|
|
35
|
+
<span className="text">{item.label}</span>
|
|
36
|
+
{item.note && <span className="note">{item.note}</span>}
|
|
37
|
+
</label>
|
|
38
|
+
</li>
|
|
39
|
+
))}
|
|
40
|
+
</ul>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface HeaderProps {
|
|
4
|
+
title: string
|
|
5
|
+
/** A short line under the title, such as the repository and a count. */
|
|
6
|
+
meta?: ReactNode
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The title block every canvas starts with. */
|
|
10
|
+
export function Header({ title, meta }: HeaderProps) {
|
|
11
|
+
return (
|
|
12
|
+
<header className="ui-header">
|
|
13
|
+
<h1>{title}</h1>
|
|
14
|
+
{meta && <p>{meta}</p>}
|
|
15
|
+
</header>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
export interface MarkdownProps {
|
|
4
|
+
/** Markdown, such as notes the agent writes. Raw HTML is shown as text, never run. */
|
|
5
|
+
children: string
|
|
6
|
+
/** Accessible name. With it, the notes are a labeled region. */
|
|
7
|
+
label?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Markdown the agent writes, in the marketplace's theme: headings, paragraphs,
|
|
12
|
+
* lists, task lists, quotes, code, rules, bold, italic, strikethrough, inline code,
|
|
13
|
+
* and http(s) or mailto links. It builds React elements directly, so nothing in
|
|
14
|
+
* the text can inject HTML.
|
|
15
|
+
*/
|
|
16
|
+
export function Markdown({ children, label }: MarkdownProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="ui-markdown" role={label ? 'region' : undefined} aria-label={label}>
|
|
19
|
+
{blocks(children)}
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const FENCE = /^\s*(```|~~~)/
|
|
25
|
+
const HEADING = /^(#{1,6})\s+(.*?)\s*#*\s*$/
|
|
26
|
+
const RULE = /^\s*([-*_])(\s*\1){2,}\s*$/
|
|
27
|
+
const QUOTE = /^\s*>\s?(.*)$/
|
|
28
|
+
const ITEM = /^\s*([-*+]|\d{1,9}[.)])\s+(.*)$/
|
|
29
|
+
const TASK = /^\[([ xX])\]\s+(.*)$/
|
|
30
|
+
|
|
31
|
+
function blocks(source: string): ReactNode[] {
|
|
32
|
+
const lines = source.replace(/\r\n?/g, '\n').split('\n')
|
|
33
|
+
const out: ReactNode[] = []
|
|
34
|
+
let i = 0
|
|
35
|
+
const startsBlock = (line: string) => FENCE.test(line) || HEADING.test(line) || RULE.test(line) || QUOTE.test(line) || ITEM.test(line)
|
|
36
|
+
|
|
37
|
+
while (i < lines.length) {
|
|
38
|
+
const line = lines[i]!
|
|
39
|
+
const key = out.length
|
|
40
|
+
if (!line.trim()) {
|
|
41
|
+
i += 1
|
|
42
|
+
} else if (FENCE.test(line)) {
|
|
43
|
+
const fence = FENCE.exec(line)![1]!
|
|
44
|
+
const code: string[] = []
|
|
45
|
+
for (i += 1; i < lines.length && !lines[i]!.trim().startsWith(fence); i += 1) code.push(lines[i]!)
|
|
46
|
+
i += 1
|
|
47
|
+
out.push(
|
|
48
|
+
<pre key={key}>
|
|
49
|
+
<code>{code.join('\n')}</code>
|
|
50
|
+
</pre>,
|
|
51
|
+
)
|
|
52
|
+
} else if (HEADING.test(line)) {
|
|
53
|
+
const [, hashes, text] = HEADING.exec(line)!
|
|
54
|
+
// The canvas title is the page's h1, so `#` becomes h2.
|
|
55
|
+
const Tag = `h${Math.min(hashes!.length + 1, 6)}` as 'h2'
|
|
56
|
+
out.push(<Tag key={key}>{inline(text!)}</Tag>)
|
|
57
|
+
i += 1
|
|
58
|
+
} else if (RULE.test(line)) {
|
|
59
|
+
out.push(<hr key={key} />)
|
|
60
|
+
i += 1
|
|
61
|
+
} else if (QUOTE.test(line)) {
|
|
62
|
+
const quoted: string[] = []
|
|
63
|
+
for (; i < lines.length && QUOTE.test(lines[i]!); i += 1) quoted.push(QUOTE.exec(lines[i]!)![1]!)
|
|
64
|
+
out.push(<blockquote key={key}>{blocks(quoted.join('\n'))}</blockquote>)
|
|
65
|
+
} else if (ITEM.test(line)) {
|
|
66
|
+
const ordered = /^\d/.test(ITEM.exec(line)![1]!)
|
|
67
|
+
const items: string[] = []
|
|
68
|
+
for (; i < lines.length; i += 1) {
|
|
69
|
+
const next = lines[i]!
|
|
70
|
+
const item = ITEM.exec(next)
|
|
71
|
+
if (item && /^\d/.test(item[1]!) === ordered) items.push(item[2]!)
|
|
72
|
+
// An indented line continues the item above it.
|
|
73
|
+
else if (items.length > 0 && /^\s{2,}\S/.test(next) && !item) items[items.length - 1] += ` ${next.trim()}`
|
|
74
|
+
else break
|
|
75
|
+
}
|
|
76
|
+
const List = ordered ? 'ol' : 'ul'
|
|
77
|
+
out.push(
|
|
78
|
+
<List key={key}>
|
|
79
|
+
{items.map((text, index) => {
|
|
80
|
+
const task = TASK.exec(text)
|
|
81
|
+
if (!task) return <li key={index}>{inline(text)}</li>
|
|
82
|
+
return (
|
|
83
|
+
<li key={index} className="task">
|
|
84
|
+
<input type="checkbox" checked={task[1] !== ' '} readOnly disabled aria-label={task[2]} />
|
|
85
|
+
<span>{inline(task[2]!)}</span>
|
|
86
|
+
</li>
|
|
87
|
+
)
|
|
88
|
+
})}
|
|
89
|
+
</List>,
|
|
90
|
+
)
|
|
91
|
+
} else {
|
|
92
|
+
const text: string[] = []
|
|
93
|
+
for (; i < lines.length && lines[i]!.trim() && !startsBlock(lines[i]!); i += 1) text.push(lines[i]!.trim())
|
|
94
|
+
out.push(<p key={key}>{inline(text.join(' '))}</p>)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const INLINE =
|
|
101
|
+
/(`+)([\s\S]*?[^`])\1(?!`)|\*\*([\s\S]+?)\*\*|(?<![\w])__([\s\S]+?)__(?![\w])|~~([\s\S]+?)~~|\*(?!\s)([\s\S]+?)\*|(?<![\w])_(?!\s)([\s\S]+?)_(?![\w])|\[([^\]]+)\]\(\s*([^)\s]+)\s*\)/g
|
|
102
|
+
|
|
103
|
+
function inline(text: string): ReactNode[] {
|
|
104
|
+
const out: ReactNode[] = []
|
|
105
|
+
let last = 0
|
|
106
|
+
for (const match of text.matchAll(INLINE)) {
|
|
107
|
+
const index = match.index
|
|
108
|
+
if (index > last) out.push(text.slice(last, index))
|
|
109
|
+
const [whole, ticks, code, bold, bold2, strike, italic, italic2, label, url] = match
|
|
110
|
+
if (ticks) out.push(<code key={index}>{code!.trim()}</code>)
|
|
111
|
+
else if (bold ?? bold2) out.push(<strong key={index}>{inline((bold ?? bold2)!)}</strong>)
|
|
112
|
+
else if (strike) out.push(<del key={index}>{inline(strike)}</del>)
|
|
113
|
+
else if (italic ?? italic2) out.push(<em key={index}>{inline((italic ?? italic2)!)}</em>)
|
|
114
|
+
else {
|
|
115
|
+
const href = safeHref(url!)
|
|
116
|
+
out.push(
|
|
117
|
+
href ? (
|
|
118
|
+
<a key={index} href={href} target="_blank" rel="noreferrer">
|
|
119
|
+
{inline(label!)}
|
|
120
|
+
</a>
|
|
121
|
+
) : (
|
|
122
|
+
whole
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
last = index + whole.length
|
|
127
|
+
}
|
|
128
|
+
if (last < text.length) out.push(text.slice(last))
|
|
129
|
+
return out
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Only web and mail links; anything else, such as `javascript:`, stays as text. */
|
|
133
|
+
function safeHref(url: string): string | undefined {
|
|
134
|
+
try {
|
|
135
|
+
const parsed = new URL(url)
|
|
136
|
+
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol) ? parsed.href : undefined
|
|
137
|
+
} catch {
|
|
138
|
+
return undefined
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes } from 'react'
|
|
2
|
+
|
|
3
|
+
/** A secondary action that reads as a link. */
|
|
4
|
+
export function TextButton({ className, ...props }: ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
5
|
+
return <button type="button" className={className ? `ui-text-button ${className}` : 'ui-text-button'} {...props} />
|
|
6
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The theme every canvas in this marketplace shares. Each canvas imports it after
|
|
3
|
+
* @rwoll/canvas-kit/styles.css, so change your brand here once.
|
|
4
|
+
*/
|
|
5
|
+
:root {
|
|
6
|
+
--accent: #8a2be2;
|
|
7
|
+
--accent-contrast: #fff;
|
|
8
|
+
--accent-bg: rgba(138, 43, 226, 0.1);
|
|
9
|
+
--accent-border: rgba(138, 43, 226, 0.5);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@media (prefers-color-scheme: dark) {
|
|
13
|
+
:root {
|
|
14
|
+
--accent: #c084fc;
|
|
15
|
+
--accent-contrast: #16171d;
|
|
16
|
+
--accent-bg: rgba(192, 132, 252, 0.15);
|
|
17
|
+
--accent-border: rgba(192, 132, 252, 0.5);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.ui-header h1 {
|
|
22
|
+
margin: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.ui-header p {
|
|
26
|
+
margin: 4px 0 0;
|
|
27
|
+
color: var(--muted);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.ui-checklist {
|
|
31
|
+
list-style: none;
|
|
32
|
+
margin: 0;
|
|
33
|
+
padding: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.ui-checklist li {
|
|
37
|
+
border-bottom: 1px solid var(--border);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.ui-checklist label {
|
|
41
|
+
display: flex;
|
|
42
|
+
gap: 8px;
|
|
43
|
+
align-items: center;
|
|
44
|
+
padding: 8px 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.ui-checklist input {
|
|
48
|
+
accent-color: var(--accent);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.ui-checklist .done .text {
|
|
52
|
+
color: var(--muted);
|
|
53
|
+
text-decoration: line-through;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.ui-checklist .note {
|
|
57
|
+
margin-left: auto;
|
|
58
|
+
font-size: 0.85em;
|
|
59
|
+
color: var(--muted);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.ui-empty {
|
|
63
|
+
color: var(--muted);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.ui-text-button {
|
|
67
|
+
align-self: flex-start;
|
|
68
|
+
padding: 0;
|
|
69
|
+
border: 0;
|
|
70
|
+
background: none;
|
|
71
|
+
color: var(--accent);
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.ui-text-button:disabled {
|
|
76
|
+
color: var(--muted);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.ui-markdown > :first-child {
|
|
80
|
+
margin-top: 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.ui-markdown > :last-child {
|
|
84
|
+
margin-bottom: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.ui-markdown :is(h2, h3, h4, h5, h6) {
|
|
88
|
+
margin: 1em 0 0.35em;
|
|
89
|
+
font-size: 1em;
|
|
90
|
+
color: var(--text-h);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.ui-markdown h2 {
|
|
94
|
+
font-size: 1.15em;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.ui-markdown :is(p, ul, ol, blockquote, pre) {
|
|
98
|
+
margin: 0.5em 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.ui-markdown :is(ul, ol) {
|
|
102
|
+
padding-left: 1.4em;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.ui-markdown li.task {
|
|
106
|
+
display: flex;
|
|
107
|
+
gap: 0.5em;
|
|
108
|
+
align-items: baseline;
|
|
109
|
+
margin-left: -1.4em;
|
|
110
|
+
list-style: none;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.ui-markdown li.task input {
|
|
114
|
+
margin: 0;
|
|
115
|
+
accent-color: var(--accent);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.ui-markdown pre {
|
|
119
|
+
padding: 0.75em 1em;
|
|
120
|
+
overflow-x: auto;
|
|
121
|
+
background: var(--code-bg);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.ui-markdown pre code {
|
|
125
|
+
padding: 0;
|
|
126
|
+
background: none;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.ui-markdown blockquote {
|
|
130
|
+
padding-left: 0.9em;
|
|
131
|
+
border-left: 2px solid var(--accent-border);
|
|
132
|
+
color: var(--muted);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.ui-markdown a {
|
|
136
|
+
color: var(--accent);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.ui-markdown hr {
|
|
140
|
+
margin: 1em 0;
|
|
141
|
+
border: 0;
|
|
142
|
+
border-top: 1px solid var(--border);
|
|
143
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2023",
|
|
4
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"types": [],
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"verbatimModuleSyntax": true,
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"strict": true,
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noUnusedParameters": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "${configDir}/node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
|
4
|
+
"target": "es2023",
|
|
5
|
+
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"types": ["vite/client", "node"],
|
|
8
|
+
"allowArbitraryExtensions": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
|
|
11
|
+
/* Bundler mode */
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"moduleDetection": "force",
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
|
|
19
|
+
/* Linting */
|
|
20
|
+
"strict": true,
|
|
21
|
+
"noUnusedLocals": true,
|
|
22
|
+
"noUnusedParameters": true,
|
|
23
|
+
"erasableSyntaxOnly": true,
|
|
24
|
+
"noFallthroughCasesInSwitch": true
|
|
25
|
+
},
|
|
26
|
+
"include": ["${configDir}/src", "${configDir}/server"]
|
|
27
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"tsBuildInfoFile": "${configDir}/node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
|
4
|
+
"target": "es2023",
|
|
5
|
+
"lib": ["ES2023"],
|
|
6
|
+
"types": ["node"],
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
|
|
9
|
+
/* Bundler mode */
|
|
10
|
+
"module": "nodenext",
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"verbatimModuleSyntax": true,
|
|
13
|
+
"moduleDetection": "force",
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
|
|
16
|
+
/* Linting */
|
|
17
|
+
"strict": true,
|
|
18
|
+
"noUnusedLocals": true,
|
|
19
|
+
"noUnusedParameters": true,
|
|
20
|
+
"erasableSyntaxOnly": true,
|
|
21
|
+
"noFallthroughCasesInSwitch": true
|
|
22
|
+
},
|
|
23
|
+
"include": ["${configDir}/vite.config.ts", "${configDir}/playwright.config.ts", "${configDir}/e2e"]
|
|
24
|
+
}
|