@flesler/cursor-canvas-web 1.4.0 → 1.5.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/dist/cursor-canvas-web.css +1 -0
- package/dist/cursor-canvas.cjs +1 -1
- package/dist/cursor-canvas.js +584 -612
- package/dist/pie-donut-DNbA0GeH.cjs +1 -0
- package/dist/pie-donut-qRBFP0tT.js +63 -0
- package/package.json +6 -2
- package/src/runtime/index.tsx +64 -0
- package/src/shim/__tests__/barChart.test.ts +61 -0
- package/src/shim/__tests__/canvas.smoke.test.tsx +209 -0
- package/src/shim/__tests__/category-theme.test.tsx +58 -0
- package/src/shim/__tests__/chartLegendOrder.test.tsx +34 -0
- package/src/shim/__tests__/dag.test.ts +49 -0
- package/src/shim/__tests__/host-tokens.test.ts +106 -0
- package/src/shim/__tests__/lineChart.test.ts +28 -0
- package/src/shim/__tests__/useCanvasState.test.tsx +35 -0
- package/src/shim/__tests__/useHostTheme.test.tsx +43 -0
- package/src/shim/__tests__/valueAxis.test.ts +39 -0
- package/src/shim/bar-chart.tsx +78 -0
- package/src/shim/canvas-hooks.tsx +57 -0
- package/src/shim/canvas-mantine-root.tsx +129 -0
- package/src/shim/chart-shared.tsx +259 -0
- package/src/shim/cursor-canvas.tsx +136 -0
- package/src/shim/dag.ts +179 -0
- package/src/shim/diff.tsx +161 -0
- package/src/shim/host-tokens.ts +614 -0
- package/src/shim/line-chart.tsx +65 -0
- package/src/shim/pie-chart.tsx +127 -0
- package/src/shim/pie-donut.tsx +79 -0
- package/src/shim/theme.ts +77 -0
- package/src/shim/todo.tsx +184 -0
- package/src/shim/ui/actions.tsx +58 -0
- package/src/shim/ui/card-context.tsx +11 -0
- package/src/shim/ui/card.tsx +79 -0
- package/src/shim/ui/chevron.tsx +23 -0
- package/src/shim/ui/display.tsx +83 -0
- package/src/shim/ui/form.tsx +102 -0
- package/src/shim/ui/layout.tsx +67 -0
- package/src/shim/ui/rich.tsx +136 -0
- package/src/shim/ui/table.tsx +71 -0
- package/src/shim/ui/typography.tsx +129 -0
- package/src/shim/use-tokens.ts +13 -0
- package/src/superset/chart-extra.d.ts +11 -11
- 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,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
|
+
}
|