@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,127 @@
1
+ import { ChartLegend, PieChart as MantinePieChart } from '@mantine/charts'
2
+ import '@mantine/charts/styles.css'
3
+ import { lazy, Suspense, useState } from 'react'
4
+ import type { PieChartProps } from '../vendor/canvas-sdk/chart-primitives.js'
5
+ import { useSeriesColor } from './chart-shared'
6
+ import { useHostTheme } from './use-tokens'
7
+
8
+ const PieDonutChart = lazy(() =>
9
+ import('./pie-donut.js').then(m => ({ default: m.PieDonutChart })),
10
+ )
11
+
12
+ const PIE_START_ANGLE = 180
13
+ const PIE_END_ANGLE = 540
14
+
15
+ function formatPieSlicePercent(value: number, total: number): string {
16
+ if (total <= 0) {
17
+ return '0%'
18
+ }
19
+ const percent = (value / total) * 100
20
+ if (percent < 10 && Math.abs(percent - Math.round(percent)) > 0.05) {
21
+ return `${percent.toFixed(1)}%`
22
+ }
23
+ return `${Math.round(percent)}%`
24
+ }
25
+
26
+ function pieLegendPayload(
27
+ items: { name: string; color: string }[],
28
+ ): Record<string, unknown>[] {
29
+ return items.map(item => ({
30
+ dataKey: item.name,
31
+ value: item.name,
32
+ color: item.color,
33
+ type: 'circle',
34
+ }))
35
+ }
36
+
37
+ function pieLegendSeries(
38
+ items: { name: string; color: string; value: number }[],
39
+ total: number,
40
+ ) {
41
+ return items.map(item => ({
42
+ name: item.name,
43
+ color: item.color,
44
+ label: `${item.name} ${formatPieSlicePercent(item.value, total)}`,
45
+ }))
46
+ }
47
+
48
+ export function PieChart({ data, size = 200, donut, style }: PieChartProps) {
49
+ const [highlighted, setHighlighted] = useState<string | null>(null)
50
+ const theme = useHostTheme()
51
+ const seriesColor = useSeriesColor()
52
+ const mantineData = data.map((point, index) => ({
53
+ name: point.label,
54
+ value: point.value,
55
+ color: seriesColor(point.tone, index),
56
+ }))
57
+ const total = mantineData.reduce((sum, row) => sum + row.value, 0)
58
+ const pieAngles = {
59
+ startAngle: PIE_START_ANGLE,
60
+ endAngle: PIE_END_ANGLE,
61
+ }
62
+ const pieCellProps = (name: string) => ({
63
+ opacity:
64
+ highlighted === null || highlighted === name ? 1 : 0.35,
65
+ })
66
+ const chart = donut ? (
67
+ <Suspense fallback={null}>
68
+ <PieDonutChart
69
+ data={mantineData}
70
+ size={size}
71
+ highlighted={highlighted}
72
+ />
73
+ </Suspense>
74
+ ) : (
75
+ <MantinePieChart
76
+ data={mantineData}
77
+ size={size}
78
+ withTooltip
79
+ withLabelsLine={false}
80
+ cellProps={cell => pieCellProps(cell.name)}
81
+ {...pieAngles}
82
+ />
83
+ )
84
+ return (
85
+ <div
86
+ style={{
87
+ width: '100%',
88
+ display: 'flex',
89
+ flexDirection: 'column',
90
+ alignItems: 'center',
91
+ ...style,
92
+ }}
93
+ >
94
+ <div
95
+ style={{
96
+ width: '100%',
97
+ height: size,
98
+ minHeight: size,
99
+ display: 'flex',
100
+ alignItems: 'center',
101
+ justifyContent: 'center',
102
+ }}
103
+ >
104
+ {chart}
105
+ </div>
106
+ <ChartLegend
107
+ payload={pieLegendPayload(mantineData)}
108
+ series={pieLegendSeries(mantineData, total)}
109
+ legendPosition="bottom"
110
+ centered
111
+ onHighlight={setHighlighted}
112
+ mt="sm"
113
+ w="100%"
114
+ styles={{
115
+ legend: {
116
+ justifyContent: 'center',
117
+ gap: 12,
118
+ color: theme.text.primary,
119
+ },
120
+ legendItemName: {
121
+ color: theme.text.primary,
122
+ },
123
+ }}
124
+ />
125
+ </div>
126
+ )
127
+ }
@@ -0,0 +1,79 @@
1
+ import { DonutChart } from '@mantine/charts'
2
+ import { useHostTheme } from './use-tokens'
3
+
4
+ const PIE_START_ANGLE = 180
5
+ const PIE_END_ANGLE = 540
6
+
7
+ function pieDonutThickness(size: number): number {
8
+ return Math.max(28, Math.round(size * 0.22))
9
+ }
10
+
11
+ function DonutCenterLabel({
12
+ total,
13
+ family,
14
+ primary,
15
+ subtitle,
16
+ }: {
17
+ total: number;
18
+ family: string;
19
+ primary: string;
20
+ subtitle: string
21
+ }) {
22
+ return (
23
+ <text
24
+ x="50%"
25
+ y="50%"
26
+ textAnchor="middle"
27
+ dominantBaseline="central"
28
+ fontFamily={family}
29
+ >
30
+ <tspan x="50%" dy="-0.4em" fontSize={20} fontWeight={600} fill={primary}>
31
+ {total}
32
+ </tspan>
33
+ <tspan x="50%" dy="1.55em" fontSize={16} fontWeight={500} fill={subtitle}>
34
+ Total
35
+ </tspan>
36
+ </text>
37
+ )
38
+ }
39
+
40
+ type DonutSlice = { name: string; value: number; color: string }
41
+
42
+ export function PieDonutChart({
43
+ data,
44
+ size,
45
+ highlighted,
46
+ }: {
47
+ data: DonutSlice[];
48
+ size: number;
49
+ highlighted: string | null
50
+ }) {
51
+ const theme = useHostTheme()
52
+ const total = data.reduce((sum, row) => sum + row.value, 0)
53
+ const pieAngles = {
54
+ startAngle: PIE_START_ANGLE,
55
+ endAngle: PIE_END_ANGLE,
56
+ }
57
+ const pieCellProps = (name: string) => ({
58
+ opacity: highlighted === null || highlighted === name ? 1 : 0.35,
59
+ })
60
+ return (
61
+ <DonutChart
62
+ data={data}
63
+ size={size}
64
+ thickness={pieDonutThickness(size)}
65
+ withTooltip
66
+ cellProps={cell => pieCellProps(cell.name)}
67
+ {...pieAngles}
68
+ >
69
+ <DonutCenterLabel
70
+ total={total}
71
+ family={theme.typography.fontFamily}
72
+ primary={theme.text.primary}
73
+ subtitle={theme.text.secondary}
74
+ />
75
+ </DonutChart>
76
+ )
77
+ }
78
+
79
+ export type { DonutSlice }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Design tokens + tone mappings for the shim (re-exports host token runtime).
3
+ */
4
+
5
+ export {
6
+ applyPrimaryColor,
7
+ applyWorkbenchSurfaces,
8
+ buildHostTokens,
9
+ canvasFontFamilyCursor,
10
+ canvasFontFamilyGrok,
11
+ canvasPaletteDark,
12
+ canvasPaletteGrokDark,
13
+ canvasPaletteGrokLight,
14
+ canvasPaletteLight,
15
+ canvasRadius,
16
+ canvasRadiusForBrand,
17
+ canvasRadiusGrok,
18
+ canvasSpacing,
19
+ canvasTokens,
20
+ canvasTokensLight,
21
+ canvasTypography,
22
+ canvasTypographyForBrand,
23
+ canvasTypographyGrok,
24
+ categoryPaletteDark,
25
+ categoryPaletteGrokDark,
26
+ categoryPaletteGrokLight,
27
+ categoryPaletteLight,
28
+ chartColorSequence,
29
+ chartColorSequenceGrok,
30
+ chartPalette,
31
+ chartPaletteGrok,
32
+ chartThemeForBrand,
33
+ colorPalette,
34
+ parseCanvasHostThemeBrand,
35
+ parseHexColor,
36
+ resolveTheme,
37
+ resolveThemeForColorScheme,
38
+ usageColorSequence,
39
+ } from './host-tokens'
40
+
41
+ export type {
42
+ CanvasHostThemeBrand,
43
+ CanvasHostThemeOverrides,
44
+ CanvasPalette,
45
+ CanvasTokens,
46
+ CategoryPalette,
47
+ ChartPalette,
48
+ Color,
49
+ ShimHostTheme,
50
+ } from './host-tokens'
51
+
52
+ /**
53
+ * Maps every semantic tone literal used across primitives to a hex color, so
54
+ * a `Stat tone="success"` and a `ChartSeries tone="success"` match.
55
+ */
56
+ export const toneHex: Record<string, string> = {
57
+ success: '#1F8A65',
58
+ danger: '#CF2D56',
59
+ warning: '#E8A33D',
60
+ info: '#2E79B5',
61
+ neutral: '#8888A8',
62
+ added: '#1F8A65',
63
+ deleted: '#CF2D56',
64
+ renamed: '#5A6CC0',
65
+ }
66
+
67
+ /** Maps a semantic tone to a Mantine color name (for components that use it). */
68
+ export const toneColor: Record<string, string> = {
69
+ success: 'green',
70
+ danger: 'red',
71
+ warning: 'orange',
72
+ info: 'blue',
73
+ neutral: 'gray',
74
+ added: 'green',
75
+ deleted: 'red',
76
+ renamed: 'indigo',
77
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Todo primitives. `TodoList` renders clickable status rows; `TodoListCard`
3
+ * wraps them in a bordered, collapsible surface with an "N of M Done" summary.
4
+ */
5
+ import { useState } from 'react'
6
+ import { toneHex } from './theme'
7
+ import { useHostTheme } from './use-tokens'
8
+
9
+ export type {
10
+ TodoItem,
11
+ TodoListCardProps,
12
+ TodoListProps,
13
+ TodoStatus,
14
+ } from '../vendor/canvas-sdk/todo-list.js'
15
+
16
+ import type { TodoListCardProps, TodoListProps, TodoStatus } from '../vendor/canvas-sdk/todo-list.js'
17
+
18
+ function StatusIcon({ status }: { status: TodoStatus }) {
19
+ const theme = useHostTheme()
20
+ const stroke =
21
+ status === 'completed'
22
+ ? toneHex.success
23
+ : status === 'in_progress'
24
+ ? theme.accent.primary
25
+ : status === 'cancelled'
26
+ ? toneHex.danger
27
+ : theme.text.tertiary
28
+ return (
29
+ <svg
30
+ width={14}
31
+ height={14}
32
+ viewBox="0 0 14 14"
33
+ fill="none"
34
+ style={{ flex: '0 0 auto', marginTop: 3 }}
35
+ aria-hidden
36
+ >
37
+ <circle cx={7} cy={7} r={6} stroke={stroke} strokeWidth={1.4} />
38
+ {status === 'completed' && (
39
+ <path
40
+ d="M4 7.2l2 2 4-4.4"
41
+ stroke={stroke}
42
+ strokeWidth={1.4}
43
+ strokeLinecap="round"
44
+ strokeLinejoin="round"
45
+ />
46
+ )}
47
+ {status === 'in_progress' && <circle cx={7} cy={7} r={3} fill={stroke} />}
48
+ {status === 'cancelled' && (
49
+ <path
50
+ d="M4.8 4.8l4.4 4.4M9.2 4.8l-4.4 4.4"
51
+ stroke={stroke}
52
+ strokeWidth={1.4}
53
+ strokeLinecap="round"
54
+ />
55
+ )}
56
+ </svg>
57
+ )
58
+ }
59
+
60
+ export function TodoList({
61
+ todos,
62
+ dimmedTodoIds,
63
+ onTodoClick,
64
+ style,
65
+ }: TodoListProps) {
66
+ const theme = useHostTheme()
67
+ if (todos.length === 0) return null
68
+ return (
69
+ <div style={{ display: 'flex', flexDirection: 'column', ...style }}>
70
+ {todos.map((todo) => {
71
+ const dimmed = dimmedTodoIds?.has(todo.id)
72
+ return (
73
+ <button
74
+ key={todo.id}
75
+ type="button"
76
+ onClick={() => onTodoClick?.(todo)}
77
+ style={{
78
+ display: 'flex',
79
+ gap: 8,
80
+ alignItems: 'flex-start',
81
+ width: '100%',
82
+ textAlign: 'left',
83
+ padding: '4px 6px',
84
+ background: 'transparent',
85
+ border: 'none',
86
+ cursor: onTodoClick ? 'pointer' : 'default',
87
+ color: theme.text.primary,
88
+ fontSize: 14,
89
+ lineHeight: '20px',
90
+ opacity: dimmed ? 0.45 : 1,
91
+ }}
92
+ >
93
+ <StatusIcon status={todo.status} />
94
+ <span
95
+ style={{
96
+ textDecoration:
97
+ todo.status === 'cancelled' ? 'line-through' : undefined,
98
+ color:
99
+ todo.status === 'cancelled' ? theme.text.tertiary : undefined,
100
+ }}
101
+ >
102
+ {todo.content}
103
+ </span>
104
+ </button>
105
+ )
106
+ })}
107
+ </div>
108
+ )
109
+ }
110
+
111
+ export function TodoListCard({
112
+ todos,
113
+ dimmedTodoIds,
114
+ defaultExpanded = false,
115
+ onTodoClick,
116
+ style,
117
+ }: TodoListCardProps) {
118
+ const theme = useHostTheme()
119
+ const [open, setOpen] = useState(defaultExpanded)
120
+ if (todos.length === 0) return null
121
+ const done = todos.filter(t => t.status === 'completed').length
122
+ return (
123
+ <div
124
+ style={{
125
+ border: `1px solid ${theme.stroke.primary}`,
126
+ borderRadius: 6,
127
+ overflow: 'hidden',
128
+ ...style,
129
+ }}
130
+ >
131
+ <button
132
+ type="button"
133
+ onClick={() => setOpen(o => !o)}
134
+ style={{
135
+ display: 'flex',
136
+ alignItems: 'center',
137
+ gap: 8,
138
+ width: '100%',
139
+ padding: '6px 10px',
140
+ background: theme.fill.secondary,
141
+ border: 'none',
142
+ cursor: 'pointer',
143
+ color: theme.text.secondary,
144
+ fontSize: 12,
145
+ fontWeight: 600,
146
+ }}
147
+ >
148
+ <svg
149
+ width={12}
150
+ height={12}
151
+ viewBox="0 0 12 12"
152
+ fill="none"
153
+ style={{
154
+ transform: open ? 'rotate(90deg)' : 'none',
155
+ transition: 'transform 120ms',
156
+ }}
157
+ aria-hidden
158
+ >
159
+ <path
160
+ d="M4 2.5L8 6l-4 3.5"
161
+ stroke="currentColor"
162
+ strokeWidth={1.4}
163
+ strokeLinecap="round"
164
+ strokeLinejoin="round"
165
+ />
166
+ </svg>
167
+ <span>Todos</span>
168
+ <span style={{ flex: 1 }} />
169
+ <span>
170
+ {done} of {todos.length} Done
171
+ </span>
172
+ </button>
173
+ {open && (
174
+ <div style={{ padding: 6 }}>
175
+ <TodoList
176
+ todos={todos}
177
+ dimmedTodoIds={dimmedTodoIds}
178
+ onTodoClick={onTodoClick}
179
+ />
180
+ </div>
181
+ )}
182
+ </div>
183
+ )
184
+ }
@@ -0,0 +1,58 @@
1
+ import { ActionIcon, Button as MantineButton } from '@mantine/core'
2
+ import type { IconButtonProps } from '../../vendor/canvas-sdk/form-primitives.js'
3
+ import type { ButtonProps } from '../../vendor/canvas-sdk/ui-primitives.js'
4
+
5
+ const BUTTON_VARIANT = {
6
+ primary: 'filled',
7
+ secondary: 'default',
8
+ ghost: 'subtle',
9
+ } as const
10
+
11
+ export function Button({
12
+ children,
13
+ variant = 'secondary',
14
+ disabled,
15
+ type = 'button',
16
+ style,
17
+ onClick,
18
+ }: ButtonProps) {
19
+ return (
20
+ <MantineButton
21
+ variant={BUTTON_VARIANT[variant]}
22
+ color={variant === 'primary' ? 'accent' : undefined}
23
+ size="xs"
24
+ disabled={disabled}
25
+ type={type}
26
+ onClick={onClick}
27
+ style={style}
28
+ >
29
+ {children}
30
+ </MantineButton>
31
+ )
32
+ }
33
+
34
+ export function IconButton({
35
+ children,
36
+ onClick,
37
+ disabled,
38
+ title,
39
+ variant = 'default',
40
+ size = 'md',
41
+ style,
42
+ }: IconButtonProps) {
43
+ return (
44
+ <ActionIcon
45
+ variant={variant === 'circle' ? 'light' : 'subtle'}
46
+ radius={variant === 'circle' ? 'xl' : 'sm'}
47
+ size={size === 'sm' ? 'sm' : 'md'}
48
+ color="gray"
49
+ disabled={disabled}
50
+ title={title}
51
+ aria-label={title}
52
+ onClick={onClick}
53
+ style={style}
54
+ >
55
+ {children}
56
+ </ActionIcon>
57
+ )
58
+ }
@@ -0,0 +1,11 @@
1
+ import { createContext } from 'react'
2
+
3
+ export const CardContext = createContext<{
4
+ collapsible: boolean;
5
+ open: boolean;
6
+ toggle: () => void
7
+ }>({
8
+ collapsible: false,
9
+ open: true,
10
+ toggle: () => {},
11
+ })
@@ -0,0 +1,79 @@
1
+ import { Card as MantineCard } from '@mantine/core'
2
+ import { useCallback, useContext, useState } from 'react'
3
+ import type { CardBodyProps, CardHeaderProps, CardProps } from '../../vendor/canvas-sdk/ui-primitives.js'
4
+ import { useHostTheme } from '../use-tokens'
5
+ import { CardContext } from './card-context'
6
+ import { Chevron } from './chevron'
7
+
8
+ export function Card({
9
+ children,
10
+ variant = 'default',
11
+ collapsible = false,
12
+ defaultOpen = true,
13
+ open: openProp,
14
+ onOpenChange,
15
+ style,
16
+ }: CardProps) {
17
+ const host = useHostTheme()
18
+ const [internalOpen, setInternalOpen] = useState(defaultOpen)
19
+ const open = openProp ?? internalOpen
20
+ const toggle = useCallback(() => {
21
+ const next = !open
22
+ if (openProp === undefined) setInternalOpen(next)
23
+ onOpenChange?.(next)
24
+ }, [open, openProp, onOpenChange])
25
+
26
+ return (
27
+ <MantineCard
28
+ withBorder={variant !== 'borderless'}
29
+ radius={host.radius.md}
30
+ padding={0}
31
+ style={{
32
+ background: host.bg.elevated,
33
+ borderColor: host.stroke.secondary,
34
+ ...style,
35
+ }}
36
+ >
37
+ <CardContext.Provider value={{ collapsible, open, toggle }}>
38
+ {children}
39
+ </CardContext.Provider>
40
+ </MantineCard>
41
+ )
42
+ }
43
+
44
+ export function CardHeader({ children, trailing, style }: CardHeaderProps) {
45
+ const host = useHostTheme()
46
+ const { collapsible, open, toggle } = useContext(CardContext)
47
+ return (
48
+ <div
49
+ onClick={collapsible ? toggle : undefined}
50
+ style={{
51
+ display: 'flex',
52
+ alignItems: 'center',
53
+ justifyContent: 'space-between',
54
+ gap: 8,
55
+ padding: '8px 14px',
56
+ borderBottom: `1px solid ${host.stroke.secondary}`,
57
+ fontSize: host.typography.small.fontSize,
58
+ fontWeight: host.typography.h3.fontWeight,
59
+ color: host.text.primary,
60
+ fontFamily: host.typography.fontFamily,
61
+ cursor: collapsible ? 'pointer' : undefined,
62
+ userSelect: collapsible ? 'none' : undefined,
63
+ ...style,
64
+ }}
65
+ >
66
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
67
+ {collapsible && <Chevron expanded={open} />}
68
+ {children}
69
+ </span>
70
+ {trailing != null && <span>{trailing}</span>}
71
+ </div>
72
+ )
73
+ }
74
+
75
+ export function CardBody({ children, style }: CardBodyProps) {
76
+ const { collapsible, open } = useContext(CardContext)
77
+ if (collapsible && !open) return null
78
+ return <div style={{ padding: 14, ...style }}>{children}</div>
79
+ }
@@ -0,0 +1,23 @@
1
+ export function Chevron({ expanded }: { expanded: boolean }) {
2
+ return (
3
+ <svg
4
+ width={12}
5
+ height={12}
6
+ viewBox="0 0 12 12"
7
+ fill="none"
8
+ style={{
9
+ transform: expanded ? 'rotate(90deg)' : 'none',
10
+ transition: 'transform 120ms',
11
+ }}
12
+ aria-hidden
13
+ >
14
+ <path
15
+ d="M4 2.5L8 6l-4 3.5"
16
+ stroke="currentColor"
17
+ strokeWidth={1.4}
18
+ strokeLinecap="round"
19
+ strokeLinejoin="round"
20
+ />
21
+ </svg>
22
+ )
23
+ }