@flesler/cursor-canvas-web 1.3.0 → 1.5.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 (42) hide show
  1. package/dist/cursor-canvas-web.css +1 -0
  2. package/dist/cursor-canvas.cjs +1 -1
  3. package/dist/cursor-canvas.js +753 -766
  4. package/dist/pie-donut-DNbA0GeH.cjs +1 -0
  5. package/dist/pie-donut-qRBFP0tT.js +63 -0
  6. package/package.json +10 -3
  7. package/src/runtime/index.tsx +64 -0
  8. package/src/shim/__tests__/barChart.test.ts +61 -0
  9. package/src/shim/__tests__/canvas.smoke.test.tsx +209 -0
  10. package/src/shim/__tests__/category-theme.test.tsx +58 -0
  11. package/src/shim/__tests__/dag.test.ts +49 -0
  12. package/src/shim/__tests__/host-tokens.test.ts +106 -0
  13. package/src/shim/__tests__/lineChart.test.ts +28 -0
  14. package/src/shim/__tests__/useCanvasState.test.tsx +35 -0
  15. package/src/shim/__tests__/useHostTheme.test.tsx +43 -0
  16. package/src/shim/__tests__/valueAxis.test.ts +39 -0
  17. package/src/shim/bar-chart.tsx +77 -0
  18. package/src/shim/canvas-hooks.tsx +57 -0
  19. package/src/shim/canvas-mantine-root.tsx +129 -0
  20. package/src/shim/chart-shared.tsx +252 -0
  21. package/src/shim/cursor-canvas.tsx +136 -0
  22. package/src/shim/dag.ts +179 -0
  23. package/src/shim/diff.tsx +161 -0
  24. package/src/shim/host-tokens.ts +614 -0
  25. package/src/shim/line-chart.tsx +62 -0
  26. package/src/shim/pie-chart.tsx +127 -0
  27. package/src/shim/pie-donut.tsx +79 -0
  28. package/src/shim/theme.ts +77 -0
  29. package/src/shim/todo.tsx +184 -0
  30. package/src/shim/ui/actions.tsx +58 -0
  31. package/src/shim/ui/card-context.tsx +11 -0
  32. package/src/shim/ui/card.tsx +79 -0
  33. package/src/shim/ui/chevron.tsx +23 -0
  34. package/src/shim/ui/display.tsx +83 -0
  35. package/src/shim/ui/form.tsx +102 -0
  36. package/src/shim/ui/layout.tsx +67 -0
  37. package/src/shim/ui/rich.tsx +136 -0
  38. package/src/shim/ui/table.tsx +71 -0
  39. package/src/shim/ui/typography.tsx +129 -0
  40. package/src/shim/use-tokens.ts +13 -0
  41. package/src/superset/chart-extra.d.ts +13 -7
  42. package/src/superset/index.d.ts +4 -4
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Mantine-backed implementation of the `cursor/canvas` module.
3
+ *
4
+ * A canvas authored against the real Cursor SDK imports only from
5
+ * `cursor/canvas`. A Vite alias + tsconfig path redirect that import here, so
6
+ * the same untouched canvas source renders in a plain web app on GitHub Pages.
7
+ *
8
+ * This barrel re-exports split UI/chart/diff/dag/todo modules so export builds
9
+ * can tree-shake unused Mantine and Recharts code. API-compatible with the real
10
+ * SDK at `~/.cursor/skills-cursor/canvas/sdk/*.d.ts`.
11
+ */
12
+
13
+ export { useEffect, useMemo, useRef, useState } from 'react'
14
+ export type { CSSProperties, RefObject } from 'react'
15
+ export type {
16
+ CanvasHostThemeBrand,
17
+ CategoryPalette,
18
+ ChartPalette,
19
+ Color,
20
+ } from '../vendor/canvas-sdk/canvas-tokens.js'
21
+ export type { CollapsibleSectionProps } from '../vendor/canvas-sdk/collapsible-section.js'
22
+ export type {
23
+ CheckboxProps,
24
+ IconButtonProps,
25
+ SelectOption,
26
+ SelectProps,
27
+ TextAreaProps,
28
+ TextInputProps,
29
+ ToggleProps,
30
+ } from '../vendor/canvas-sdk/form-primitives.js'
31
+ export type {
32
+ CanvasAction,
33
+ CanvasHostTheme,
34
+ SetCanvasState,
35
+ } from '../vendor/canvas-sdk/hooks.js'
36
+ export type { SwatchProps } from '../vendor/canvas-sdk/swatch.js'
37
+ export type {
38
+ CanvasPalette,
39
+ CanvasTokens,
40
+ } from '../vendor/canvas-sdk/theme.js'
41
+ export type {
42
+ ButtonProps,
43
+ CalloutProps,
44
+ CalloutTone,
45
+ CardBodyProps,
46
+ CardHeaderProps,
47
+ CardProps,
48
+ CardSize,
49
+ CardVariant,
50
+ CodeProps,
51
+ DividerProps,
52
+ GridProps,
53
+ H1Props,
54
+ H2Props,
55
+ H3Props,
56
+ LinkProps,
57
+ PillProps,
58
+ PillSize,
59
+ PillTone,
60
+ RowProps,
61
+ StackProps,
62
+ StatProps,
63
+ StatTone,
64
+ TableColumnAlign,
65
+ TableProps,
66
+ TableRowTone,
67
+ TextProps,
68
+ TextWeight,
69
+ } from '../vendor/canvas-sdk/ui-primitives.js'
70
+ export type {
71
+ UsageBarProps,
72
+ UsageBarSegment,
73
+ } from '../vendor/canvas-sdk/usage-bar.js'
74
+
75
+ export { mergeStyle, useCanvasAction, useCanvasState } from './canvas-hooks'
76
+ export { BarChart } from './bar-chart'
77
+ export { LineChart } from './line-chart'
78
+ export { PieChart } from './pie-chart'
79
+ export type {
80
+ BarChartProps,
81
+ CanvasBarChartExtra,
82
+ CanvasChartExtra,
83
+ CanvasChartScale,
84
+ CanvasChartValueAxisExtra,
85
+ CanvasLineChartExtra,
86
+ ChartDataPoint,
87
+ ChartReferenceLine,
88
+ ChartSeries,
89
+ ChartTone,
90
+ LineChartProps,
91
+ PieChartProps,
92
+ } from './chart-shared'
93
+ export { computeDAGLayout } from './dag'
94
+ export type {
95
+ DAGLayoutEdge,
96
+ DAGLayoutNode,
97
+ DAGLayoutOptions,
98
+ DAGLayoutRank,
99
+ DAGLayoutResult,
100
+ } from './dag'
101
+ export { DiffStats, DiffView } from './diff'
102
+ export type {
103
+ DiffLineData,
104
+ DiffLineType,
105
+ DiffStatsProps,
106
+ DiffViewProps,
107
+ } from './diff'
108
+ export {
109
+ canvasPaletteDark,
110
+ canvasPaletteGrokDark,
111
+ canvasPaletteGrokLight,
112
+ canvasPaletteLight,
113
+ canvasTokens,
114
+ canvasTokensLight,
115
+ categoryPaletteDark,
116
+ categoryPaletteLight,
117
+ colorPalette,
118
+ usageColorSequence,
119
+ } from './theme'
120
+ export { TodoList, TodoListCard } from './todo'
121
+ export type {
122
+ TodoItem,
123
+ TodoListCardProps,
124
+ TodoListProps,
125
+ TodoStatus,
126
+ } from './todo'
127
+ export { useHostTheme } from './use-tokens'
128
+
129
+ export { Button, IconButton } from './ui/actions'
130
+ export { Card, CardBody, CardHeader } from './ui/card'
131
+ export { Callout, Pill, Stat } from './ui/display'
132
+ export { Checkbox, Select, TextArea, TextInput, Toggle } from './ui/form'
133
+ export { Divider, Grid, Row, Spacer, Stack } from './ui/layout'
134
+ export { CollapsibleSection, Swatch, UsageBar } from './ui/rich'
135
+ export { Table } from './ui/table'
136
+ export { Code, H1, H2, H3, Link, Text } from './ui/typography'
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Pure layered DAG layout, matching the SDK's `computeDAGLayout` contract.
3
+ *
4
+ * Back-edges (cycles) are detected via DFS, excluded from ranking, and flagged
5
+ * in the output so callers can render them differently. Rendering is the
6
+ * caller's responsibility — this returns only coordinates and anchor points.
7
+ */
8
+
9
+ export type {
10
+ DAGLayoutEdge,
11
+ DAGLayoutNode,
12
+ DAGLayoutOptions,
13
+ DAGLayoutRank,
14
+ DAGLayoutResult,
15
+ } from '../vendor/canvas-sdk/dag-layout.js'
16
+
17
+ import type { DAGLayoutEdge, DAGLayoutNode, DAGLayoutOptions, DAGLayoutRank, DAGLayoutResult } from '../vendor/canvas-sdk/dag-layout.js'
18
+
19
+ export function computeDAGLayout(options: DAGLayoutOptions): DAGLayoutResult {
20
+ const {
21
+ nodes,
22
+ edges,
23
+ direction = 'vertical',
24
+ nodeWidth = 160,
25
+ nodeHeight = 40,
26
+ rankGap = 64,
27
+ nodeGap = 48,
28
+ padding = 24,
29
+ } = options
30
+
31
+ const ids = nodes.map(n => n.id)
32
+ const idSet = new Set(ids)
33
+ const adjacency = new Map<string, string[]>()
34
+ for (const id of ids) adjacency.set(id, [])
35
+ for (const e of edges) {
36
+ if (idSet.has(e.from) && idSet.has(e.to)) {
37
+ adjacency.get(e.from)!.push(e.to)
38
+ }
39
+ }
40
+
41
+ // Detect back-edges with a white/gray/black DFS.
42
+ const WHITE = 0
43
+ const GRAY = 1
44
+ const BLACK = 2
45
+ const state = new Map<string, number>(ids.map(id => [id, WHITE]))
46
+ const backEdges = new Set<string>()
47
+ const edgeKey = (from: string, to: string) => `${from}\u0000${to}`
48
+
49
+ const visit = (id: string) => {
50
+ state.set(id, GRAY)
51
+ for (const to of adjacency.get(id) ?? []) {
52
+ const s = state.get(to)
53
+ if (s === GRAY) backEdges.add(edgeKey(id, to))
54
+ else if (s === WHITE) visit(to)
55
+ }
56
+ state.set(id, BLACK)
57
+ }
58
+ for (const id of ids) if (state.get(id) === WHITE) visit(id)
59
+
60
+ // Longest-path ranking over the DAG (back-edges removed).
61
+ const forwardEdges = edges.filter(
62
+ e =>
63
+ idSet.has(e.from) &&
64
+ idSet.has(e.to) &&
65
+ !backEdges.has(edgeKey(e.from, e.to)),
66
+ )
67
+ const indegree = new Map<string, number>(ids.map(id => [id, 0]))
68
+ const forwardAdj = new Map<string, string[]>(ids.map(id => [id, []]))
69
+ for (const e of forwardEdges) {
70
+ forwardAdj.get(e.from)!.push(e.to)
71
+ indegree.set(e.to, (indegree.get(e.to) ?? 0) + 1)
72
+ }
73
+ const rank = new Map<string, number>(ids.map(id => [id, 0]))
74
+ const queue = ids.filter(id => (indegree.get(id) ?? 0) === 0)
75
+ while (queue.length > 0) {
76
+ const id = queue.shift()!
77
+ const r = rank.get(id)!
78
+ for (const to of forwardAdj.get(id)!) {
79
+ rank.set(to, Math.max(rank.get(to)!, r + 1))
80
+ indegree.set(to, indegree.get(to)! - 1)
81
+ if (indegree.get(to) === 0) queue.push(to)
82
+ }
83
+ }
84
+
85
+ // Group by rank, preserving input order within each rank.
86
+ const byRank = new Map<number, string[]>()
87
+ for (const id of ids) {
88
+ const r = rank.get(id)!
89
+ if (!byRank.has(r)) byRank.set(r, [])
90
+ byRank.get(r)!.push(id)
91
+ }
92
+ const rankIndices = [...byRank.keys()].sort((a, b) => a - b)
93
+
94
+ const isVertical = direction === 'vertical'
95
+ // Cross-axis span (px) of a rank with `count` nodes.
96
+ const span = (count: number) =>
97
+ count * (isVertical ? nodeWidth : nodeHeight) +
98
+ Math.max(0, count - 1) * nodeGap
99
+ const maxSpan = Math.max(
100
+ 0,
101
+ ...rankIndices.map(r => span(byRank.get(r)!.length)),
102
+ )
103
+
104
+ const nodeOut = new Map<string, DAGLayoutNode>()
105
+ const ranksOut: DAGLayoutRank[] = []
106
+
107
+ rankIndices.forEach((r, rankPos) => {
108
+ const rankNodes = byRank.get(r)!
109
+ const rankSpan = span(rankNodes.length)
110
+ const crossStart = padding + (maxSpan - rankSpan) / 2
111
+ const mainPos =
112
+ padding + rankPos * ((isVertical ? nodeHeight : nodeWidth) + rankGap)
113
+
114
+ rankNodes.forEach((id, order) => {
115
+ const crossPos =
116
+ crossStart + order * ((isVertical ? nodeWidth : nodeHeight) + nodeGap)
117
+ const x = isVertical ? crossPos : mainPos
118
+ const y = isVertical ? mainPos : crossPos
119
+ nodeOut.set(id, { id, x, y, rank: r, order })
120
+ })
121
+
122
+ ranksOut.push({
123
+ rank: r,
124
+ x: isVertical ? crossStart : mainPos,
125
+ y: isVertical ? mainPos : crossStart,
126
+ width: isVertical ? rankSpan : nodeWidth,
127
+ height: isVertical ? nodeHeight : rankSpan,
128
+ nodeIds: rankNodes,
129
+ })
130
+ })
131
+
132
+ const anchor = (
133
+ node: DAGLayoutNode | undefined,
134
+ role: 'source' | 'target',
135
+ ) => {
136
+ if (!node) return { x: 0, y: 0 }
137
+ if (isVertical) {
138
+ return {
139
+ x: node.x + nodeWidth / 2,
140
+ y: role === 'source' ? node.y + nodeHeight : node.y,
141
+ }
142
+ }
143
+ return {
144
+ x: role === 'source' ? node.x + nodeWidth : node.x,
145
+ y: node.y + nodeHeight / 2,
146
+ }
147
+ }
148
+
149
+ const edgesOut: DAGLayoutEdge[] = edges
150
+ .filter(e => idSet.has(e.from) && idSet.has(e.to))
151
+ .map((e) => {
152
+ const source = anchor(nodeOut.get(e.from), 'source')
153
+ const target = anchor(nodeOut.get(e.to), 'target')
154
+ return {
155
+ from: e.from,
156
+ to: e.to,
157
+ sourceX: source.x,
158
+ sourceY: source.y,
159
+ targetX: target.x,
160
+ targetY: target.y,
161
+ isBackEdge: backEdges.has(edgeKey(e.from, e.to)),
162
+ }
163
+ })
164
+
165
+ const totalMain =
166
+ padding * 2 +
167
+ rankIndices.length * (isVertical ? nodeHeight : nodeWidth) +
168
+ Math.max(0, rankIndices.length - 1) * rankGap
169
+ const totalCross = padding * 2 + maxSpan
170
+
171
+ return {
172
+ nodes: [...nodeOut.values()],
173
+ edges: edgesOut,
174
+ ranks: ranksOut,
175
+ direction,
176
+ width: isVertical ? totalCross : totalMain,
177
+ height: isVertical ? totalMain : totalCross,
178
+ }
179
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Diff primitives. `DiffView` renders a unified diff body (colored line
3
+ * backgrounds, gutter, accent strip); `DiffStats` is the `+N`/`-N` glyph pair.
4
+ *
5
+ * `path` / `language` are accepted for API parity with the IDE SDK. Token
6
+ * coloring on the web is plain text until a lightweight highlighter lands
7
+ * (see open-canvas `docs/TODO.md` — Shiki is IDE-only and too heavy to ship).
8
+ */
9
+ import type { CSSProperties } from 'react'
10
+ import { toneHex } from './theme'
11
+ import { useHostTheme } from './use-tokens'
12
+
13
+ export type DiffStatsProps = {
14
+ additions?: number;
15
+ deletions?: number;
16
+ style?: CSSProperties
17
+ }
18
+
19
+ export function DiffStats({
20
+ additions = 0,
21
+ deletions = 0,
22
+ style,
23
+ }: DiffStatsProps) {
24
+ if (additions === 0 && deletions === 0) return null
25
+ return (
26
+ <span
27
+ style={{
28
+ display: 'inline-flex',
29
+ gap: 8,
30
+ fontVariantNumeric: 'tabular-nums',
31
+ fontSize: 12,
32
+ fontWeight: 600,
33
+ ...style,
34
+ }}
35
+ >
36
+ {additions > 0 && (
37
+ <span style={{ color: toneHex.added }}>+{additions}</span>
38
+ )}
39
+ {deletions > 0 && (
40
+ <span style={{ color: toneHex.deleted }}>-{deletions}</span>
41
+ )}
42
+ </span>
43
+ )
44
+ }
45
+
46
+ export type DiffLineType = 'added' | 'removed' | 'unchanged'
47
+ export type DiffLineData = {
48
+ type: DiffLineType;
49
+ content: string;
50
+ lineNumber?: number
51
+ }
52
+
53
+ export type DiffViewProps = {
54
+ lines: DiffLineData[];
55
+ path?: string;
56
+ language?: string;
57
+ showLineNumbers?: boolean;
58
+ coloredLineNumbers?: boolean;
59
+ showAccentStrip?: boolean;
60
+ style?: CSSProperties
61
+ }
62
+
63
+ const SIGN: Record<DiffLineType, string> = {
64
+ added: '+',
65
+ removed: '-',
66
+ unchanged: ' ',
67
+ }
68
+
69
+ export function DiffView({
70
+ lines,
71
+ showLineNumbers = true,
72
+ coloredLineNumbers = true,
73
+ showAccentStrip = true,
74
+ style,
75
+ }: DiffViewProps) {
76
+ const theme = useHostTheme()
77
+
78
+ const lineBg = (type: DiffLineType) =>
79
+ type === 'added'
80
+ ? theme.diff.insertedLine
81
+ : type === 'removed'
82
+ ? theme.diff.removedLine
83
+ : 'transparent'
84
+
85
+ const stripColor = (type: DiffLineType) =>
86
+ type === 'added'
87
+ ? theme.diff.stripAdded
88
+ : type === 'removed'
89
+ ? theme.diff.stripRemoved
90
+ : 'transparent'
91
+
92
+ const numberColor = (type: DiffLineType) => {
93
+ if (coloredLineNumbers && type === 'added') return toneHex.added
94
+ if (coloredLineNumbers && type === 'removed') return toneHex.deleted
95
+ return theme.text.tertiary
96
+ }
97
+
98
+ return (
99
+ <div
100
+ style={{
101
+ fontFamily:
102
+ 'ui-monospace, SFMono-Regular, Menlo, Monaco, "Cascadia Code", monospace',
103
+ fontSize: 12.5,
104
+ lineHeight: '18px',
105
+ color: theme.text.primary,
106
+ overflowX: 'auto',
107
+ ...style,
108
+ }}
109
+ >
110
+ {lines.map((line, index) => (
111
+ <div
112
+ key={index}
113
+ style={{
114
+ display: 'flex',
115
+ alignItems: 'stretch',
116
+ background: lineBg(line.type),
117
+ borderLeft: showAccentStrip
118
+ ? `3px solid ${stripColor(line.type)}`
119
+ : undefined,
120
+ }}
121
+ >
122
+ {showLineNumbers && (
123
+ <span
124
+ style={{
125
+ flex: '0 0 auto',
126
+ width: 40,
127
+ textAlign: 'right',
128
+ paddingRight: 10,
129
+ userSelect: 'none',
130
+ color: numberColor(line.type),
131
+ }}
132
+ >
133
+ {line.lineNumber ?? ''}
134
+ </span>
135
+ )}
136
+ <span
137
+ style={{
138
+ flex: '0 0 auto',
139
+ width: 14,
140
+ textAlign: 'center',
141
+ userSelect: 'none',
142
+ color: numberColor(line.type),
143
+ }}
144
+ >
145
+ {SIGN[line.type]}
146
+ </span>
147
+ <pre
148
+ style={{
149
+ margin: 0,
150
+ padding: '0 8px',
151
+ whiteSpace: 'pre',
152
+ fontFamily: 'inherit',
153
+ }}
154
+ >
155
+ {line.content}
156
+ </pre>
157
+ </div>
158
+ ))}
159
+ </div>
160
+ )
161
+ }