@spatika/react 1.4.0 → 1.6.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.
- package/AGENTS.md +6 -3
- package/README.md +1 -1
- package/dist/composites/EventCalendar.js +3 -3
- package/dist/composites/GlassCard.d.ts +1 -1
- package/dist/composites/RichTextEditor.d.ts +1 -4
- package/dist/composites/RichTextEditor.d.ts.map +1 -1
- package/dist/composites/RichTextEditor.js +12 -19
- package/dist/composites/SurfaceCard.d.ts +1 -1
- package/dist/composites/Tag.d.ts +2 -2
- package/dist/index.d.ts +8 -22
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -11
- package/dist/lib/breakpoints.d.ts +17 -0
- package/dist/lib/breakpoints.d.ts.map +1 -0
- package/dist/lib/breakpoints.js +23 -0
- package/dist/lib/create-theme.d.ts +162 -0
- package/dist/lib/create-theme.d.ts.map +1 -0
- package/dist/lib/create-theme.js +138 -0
- package/dist/lib/scheduler.js +2 -2
- package/dist/lib/themes.d.ts +17 -7
- package/dist/lib/themes.d.ts.map +1 -1
- package/dist/lib/themes.js +22 -8
- package/dist/lib/use-media-query.d.ts +9 -0
- package/dist/lib/use-media-query.d.ts.map +1 -1
- package/dist/lib/use-media-query.js +28 -1
- package/dist/primitives/Alert.d.ts +1 -1
- package/dist/primitives/Badge.d.ts +2 -2
- package/dist/primitives/Button.d.ts +2 -2
- package/dist/primitives/Container.d.ts +2 -2
- package/dist/primitives/Fab.d.ts +4 -4
- package/dist/primitives/Grid.d.ts +2 -1
- package/dist/primitives/Grid.d.ts.map +1 -1
- package/dist/primitives/Grid.js +10 -2
- package/dist/primitives/IconButton.d.ts +2 -2
- package/dist/primitives/ToggleButton.d.ts +1 -1
- package/dist/theme/SpatikaThemeProvider.d.ts +4 -5
- package/dist/theme/SpatikaThemeProvider.d.ts.map +1 -1
- package/dist/theme/SpatikaThemeProvider.js +28 -13
- package/package.json +4 -2
- package/skills/spatika-ui/SKILL.md +25 -6
- package/src/composites/EventCalendar.test.tsx +293 -0
- package/src/composites/EventCalendar.tsx +3 -3
- package/src/composites/RichTextEditor.test.tsx +51 -0
- package/src/composites/RichTextEditor.tsx +20 -84
- package/src/composites/chart-ui.test.tsx +21 -0
- package/src/composites/scheduler.test.tsx +0 -258
- package/src/index.test.ts +6 -0
- package/src/index.ts +138 -72
- package/src/lib/breakpoints.test.ts +29 -0
- package/src/lib/breakpoints.ts +28 -0
- package/src/lib/create-theme.test.ts +79 -0
- package/src/lib/create-theme.ts +249 -0
- package/src/lib/scheduler.test.ts +10 -0
- package/src/lib/scheduler.ts +2 -2
- package/src/lib/themes.test.ts +17 -0
- package/src/lib/themes.ts +43 -12
- package/src/lib/use-media-query.test.tsx +80 -0
- package/src/lib/use-media-query.ts +35 -1
- package/src/primitives/Grid.test.tsx +55 -0
- package/src/primitives/Grid.tsx +13 -3
- package/src/primitives/mui-more.test.tsx +17 -0
- package/src/theme/SpatikaThemeProvider.test.tsx +38 -0
- package/src/theme/SpatikaThemeProvider.tsx +43 -15
- package/src/composites/ChartDataGrid.test.tsx +0 -57
- package/src/composites/ChartDataGrid.tsx +0 -196
- package/src/composites/MapChart.test.tsx +0 -39
- package/src/composites/MapChart.tsx +0 -130
- package/src/composites/ScatterWebGL.test.tsx +0 -25
- package/src/composites/ScatterWebGL.tsx +0 -79
- package/src/composites/cartesian-charts.test.tsx +0 -136
- package/src/composites/cartesian-charts.tsx +0 -1355
- package/src/composites/chart-composition.test.tsx +0 -68
- package/src/composites/chart-composition.tsx +0 -1039
- package/src/composites/chart-interaction.ts +0 -136
- package/src/composites/chart-ui.tsx +0 -336
- package/src/composites/charts-3d.test.tsx +0 -25
- package/src/composites/charts-3d.tsx +0 -318
- package/src/composites/charts.test.tsx +0 -254
- package/src/composites/flow-charts.test.tsx +0 -98
- package/src/composites/flow-charts.tsx +0 -641
- package/src/composites/radial-charts.test.tsx +0 -89
- package/src/composites/radial-charts.tsx +0 -643
- package/src/lib/chart-export.test.ts +0 -20
- package/src/lib/chart-export.ts +0 -99
- package/src/lib/charts-platform.test.ts +0 -93
- package/src/lib/charts.test.ts +0 -132
- package/src/lib/charts.ts +0 -1021
- package/src/lib/geo.test.ts +0 -42
- package/src/lib/geo.ts +0 -138
- package/src/lib/iso.test.ts +0 -22
- package/src/lib/iso.ts +0 -102
- package/src/lib/webgl-scatter.test.ts +0 -36
- package/src/lib/webgl-scatter.ts +0 -230
|
@@ -30,12 +30,30 @@ export function App() {
|
|
|
30
30
|
|
|
31
31
|
- Import `@spatika/tokens/styles.css` before any component
|
|
32
32
|
- Wrap the tree in `SpatikaThemeProvider`
|
|
33
|
-
- Themes: `mukta` | `neelam` | `usha` | `sandhya`
|
|
33
|
+
- Themes: `mukta` | `neelam` | `usha` | `sandhya`. Brand palettes: `createTheme` + `customThemes`
|
|
34
34
|
- Prefer composites over restyling primitives
|
|
35
35
|
- Token CSS variables only — no hardcoded hex
|
|
36
36
|
- `size="touch"` on mobile primary actions
|
|
37
37
|
- Never `transition-all` on glass
|
|
38
38
|
|
|
39
|
+
## Customize
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { Button, SpatikaThemeProvider, createTheme } from "@spatika/react";
|
|
43
|
+
|
|
44
|
+
const brand = createTheme({
|
|
45
|
+
id: "brand",
|
|
46
|
+
extends: "mukta",
|
|
47
|
+
palette: { primary: "#7c3aed", primaryForeground: "#ffffff", ring: "#7c3aed" },
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
<SpatikaThemeProvider defaultTheme="brand" customThemes={[brand]}>
|
|
51
|
+
<Button>Brand</Button>
|
|
52
|
+
</SpatikaThemeProvider>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Breakpoints: `BREAKPOINTS` (`xs` 0 / `sm` 640 / `md` 768 / `lg` 1024 / `xl` 1280). `Grid` `xs`–`xl`, `useIsMobile()`, `useBreakpoint()`. Guide: https://spatika.sweyam.com/docs/customize.md
|
|
56
|
+
|
|
39
57
|
## Pick a component
|
|
40
58
|
|
|
41
59
|
| Need | Use |
|
|
@@ -51,10 +69,10 @@ export function App() {
|
|
|
51
69
|
| Empty | `EmptyState` |
|
|
52
70
|
| Modal | `Dialog` |
|
|
53
71
|
| Toasts | `Toaster` + `useToast` |
|
|
54
|
-
| Chart | `BarChart` / `LineChart` / `ChartContainer` |
|
|
72
|
+
| Chart | `BarChart` / `LineChart` / `ChartContainer` from `@spatika/charts` |
|
|
55
73
|
| Calendar | `EventCalendar` |
|
|
56
74
|
|
|
57
|
-
Charts: `onItemClick` for mark navigation, `itemColors` for per-bar tints, `renderTooltip` / `renderMark` for host chrome, `referenceLines` for capacity guides, `fillHeight` when the parent sets height. Pie
|
|
75
|
+
Charts: install `@spatika/charts` (re-exported from `@spatika/react`). `onItemClick` for mark navigation, `syncId` to link hover across plots, `itemColors` for per-bar tints (inline fill so theme CSS cannot override), `renderTooltip` / `renderMark` for host chrome, `referenceLines` / `referenceAreas` for capacity guides, `fillHeight` or `ResponsiveContainer` when the parent sets height. Pie radii accept `"42%"`. Horizontal bars size the left gutter from category labels. Line/area series accept `strokeDasharray` and `connectNulls`. `yAxis.valueFormatter` formats ticks; `yAxis.reversed` flips the domain. `xAxis.tickAngle` rotates category labels. `stackOffset="expand"` is 100% stacked. `legendPosition`, `loading`, and `emptyText` cover layout and fetch states. Bar charts accept `showBarBackground`. Pie `labelLine` draws outside labels. SparkLineChart hover takes optional `labels`.
|
|
58
76
|
|
|
59
77
|
Prefer `toolbarDensity="compact"` and `showDateJump` for a dense product toolbar. `NativeSelect size="sm"` is the compact pill. `onVisibleRangeChange` is the fetch window (`end` exclusive). Use `showEventEditor={false}` when the host owns create/edit.
|
|
60
78
|
|
|
@@ -95,6 +113,7 @@ import { Home, Search, User } from "lucide-react";
|
|
|
95
113
|
|
|
96
114
|
## Next
|
|
97
115
|
|
|
98
|
-
1. Fetch the
|
|
99
|
-
2.
|
|
100
|
-
3.
|
|
116
|
+
1. Fetch the customize guide: `https://spatika.sweyam.com/docs/customize.md`
|
|
117
|
+
2. Fetch the component markdown: `https://spatika.sweyam.com/docs/{slug}.md`
|
|
118
|
+
3. Read prop types from `node_modules/@spatika/react/dist/index.d.ts`
|
|
119
|
+
4. Icons in examples use `lucide-react`
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import userEvent from "@testing-library/user-event";
|
|
3
|
+
import { describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { EventCalendar } from "./EventCalendar";
|
|
5
|
+
import type { SchedulerEvent, SchedulerResource } from "../lib/scheduler";
|
|
6
|
+
|
|
7
|
+
const resources: SchedulerResource[] = [
|
|
8
|
+
{ id: "work", title: "Work", color: "violet" },
|
|
9
|
+
{ id: "health", title: "Health", color: "teal" },
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
const events: SchedulerEvent[] = [
|
|
13
|
+
{
|
|
14
|
+
id: "run",
|
|
15
|
+
title: "Morning Run",
|
|
16
|
+
start: "2026-08-03T07:00:00",
|
|
17
|
+
end: "2026-08-03T07:45:00",
|
|
18
|
+
resourceId: "health",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "bday",
|
|
22
|
+
title: "Alice's Birthday",
|
|
23
|
+
start: "2026-08-06",
|
|
24
|
+
end: "2026-08-08",
|
|
25
|
+
allDay: true,
|
|
26
|
+
resourceId: "work",
|
|
27
|
+
color: "rose",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "review",
|
|
31
|
+
title: "Design Review",
|
|
32
|
+
start: "2026-08-09T14:00:00",
|
|
33
|
+
end: "2026-08-09T16:00:00",
|
|
34
|
+
resourceId: "work",
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
describe("EventCalendar", () => {
|
|
39
|
+
it("renders the month heading, weekday labels, and events", () => {
|
|
40
|
+
render(
|
|
41
|
+
<EventCalendar
|
|
42
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
43
|
+
events={events}
|
|
44
|
+
resources={resources}
|
|
45
|
+
locale="en-US"
|
|
46
|
+
/>,
|
|
47
|
+
);
|
|
48
|
+
expect(screen.getByRole("heading", { name: /August 2026/i })).toBeInTheDocument();
|
|
49
|
+
expect(screen.getByText(/Sun/)).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByText("Morning Run")).toBeInTheDocument();
|
|
51
|
+
expect(screen.getByText("Alice's Birthday")).toBeInTheDocument();
|
|
52
|
+
expect(screen.getByText("Work")).toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("jumps to today from the toolbar", async () => {
|
|
56
|
+
const user = userEvent.setup();
|
|
57
|
+
const onDateChange = vi.fn();
|
|
58
|
+
render(
|
|
59
|
+
<EventCalendar
|
|
60
|
+
defaultDate={new Date(2025, 0, 1)}
|
|
61
|
+
onDateChange={onDateChange}
|
|
62
|
+
events={events}
|
|
63
|
+
/>,
|
|
64
|
+
);
|
|
65
|
+
await user.click(screen.getByRole("button", { name: "Today" }));
|
|
66
|
+
expect(onDateChange).toHaveBeenCalled();
|
|
67
|
+
const next = onDateChange.mock.calls[0][0] as Date;
|
|
68
|
+
const now = new Date();
|
|
69
|
+
expect(next.getFullYear()).toBe(now.getFullYear());
|
|
70
|
+
expect(next.getMonth()).toBe(now.getMonth());
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("fires onEventClick for a timed event", async () => {
|
|
74
|
+
const user = userEvent.setup();
|
|
75
|
+
const onEventClick = vi.fn();
|
|
76
|
+
render(
|
|
77
|
+
<EventCalendar
|
|
78
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
79
|
+
events={events}
|
|
80
|
+
resources={resources}
|
|
81
|
+
locale="en-US"
|
|
82
|
+
onEventClick={onEventClick}
|
|
83
|
+
/>,
|
|
84
|
+
);
|
|
85
|
+
await user.click(screen.getByText("Morning Run"));
|
|
86
|
+
expect(onEventClick).toHaveBeenCalledWith(expect.objectContaining({ id: "run" }));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("switches to week view from the menu", async () => {
|
|
90
|
+
const user = userEvent.setup();
|
|
91
|
+
render(
|
|
92
|
+
<EventCalendar defaultDate={new Date(2026, 7, 15)} events={events} locale="en-US" />,
|
|
93
|
+
);
|
|
94
|
+
await user.click(screen.getByRole("button", { name: /month/i }));
|
|
95
|
+
await user.click(screen.getByRole("menuitem", { name: "Week" }));
|
|
96
|
+
expect(screen.getByRole("heading", { name: /2026/ })).toBeInTheDocument();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("lists events in agenda view", () => {
|
|
100
|
+
render(
|
|
101
|
+
<EventCalendar
|
|
102
|
+
defaultView="agenda"
|
|
103
|
+
defaultDate={new Date(2026, 7, 3)}
|
|
104
|
+
events={events}
|
|
105
|
+
locale="en-US"
|
|
106
|
+
/>,
|
|
107
|
+
);
|
|
108
|
+
expect(screen.getByRole("heading", { name: /Aug 3/ })).toBeInTheDocument();
|
|
109
|
+
expect(screen.getAllByText("No events").length).toBeGreaterThan(0);
|
|
110
|
+
expect(screen.getByText("Morning Run")).toBeInTheDocument();
|
|
111
|
+
expect(screen.getAllByText("Alice's Birthday").length).toBeGreaterThan(0);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("opens the editor and saves title changes", async () => {
|
|
115
|
+
const user = userEvent.setup();
|
|
116
|
+
const onEventChange = vi.fn();
|
|
117
|
+
render(
|
|
118
|
+
<EventCalendar
|
|
119
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
120
|
+
events={events}
|
|
121
|
+
resources={resources}
|
|
122
|
+
locale="en-US"
|
|
123
|
+
onEventChange={onEventChange}
|
|
124
|
+
/>,
|
|
125
|
+
);
|
|
126
|
+
await user.click(screen.getByText("Morning Run"));
|
|
127
|
+
const title = screen.getByRole("textbox", { name: /title/i });
|
|
128
|
+
await user.clear(title);
|
|
129
|
+
await user.type(title, "Jog");
|
|
130
|
+
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
131
|
+
expect(onEventChange).toHaveBeenCalledWith(
|
|
132
|
+
expect.objectContaining({ id: "run" }),
|
|
133
|
+
expect.objectContaining({ title: "Jog" }),
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("creates an event from a day slot", async () => {
|
|
138
|
+
const user = userEvent.setup();
|
|
139
|
+
const onEventCreate = vi.fn();
|
|
140
|
+
render(
|
|
141
|
+
<EventCalendar
|
|
142
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
143
|
+
events={events}
|
|
144
|
+
locale="en-US"
|
|
145
|
+
onEventCreate={onEventCreate}
|
|
146
|
+
/>,
|
|
147
|
+
);
|
|
148
|
+
await user.click(screen.getByRole("button", { name: "15" }));
|
|
149
|
+
expect(screen.getByRole("heading", { name: "New event" })).toBeInTheDocument();
|
|
150
|
+
await user.type(screen.getByRole("textbox", { name: /title/i }), "Focus time");
|
|
151
|
+
await user.click(screen.getByRole("button", { name: "Save" }));
|
|
152
|
+
expect(onEventCreate).toHaveBeenCalledWith(expect.objectContaining({ title: "Focus time" }));
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("skips the built-in editor when showEventEditor is false", async () => {
|
|
156
|
+
const user = userEvent.setup();
|
|
157
|
+
const onEventClick = vi.fn();
|
|
158
|
+
const onSlotClick = vi.fn();
|
|
159
|
+
render(
|
|
160
|
+
<EventCalendar
|
|
161
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
162
|
+
events={events}
|
|
163
|
+
resources={resources}
|
|
164
|
+
locale="en-US"
|
|
165
|
+
showEventEditor={false}
|
|
166
|
+
onEventClick={onEventClick}
|
|
167
|
+
onSlotClick={onSlotClick}
|
|
168
|
+
/>,
|
|
169
|
+
);
|
|
170
|
+
await user.click(screen.getByText("Morning Run"));
|
|
171
|
+
expect(onEventClick).toHaveBeenCalledWith(expect.objectContaining({ id: "run" }));
|
|
172
|
+
expect(screen.queryByRole("heading", { name: /event/i })).not.toBeInTheDocument();
|
|
173
|
+
|
|
174
|
+
await user.click(screen.getByRole("button", { name: "15" }));
|
|
175
|
+
expect(onSlotClick).toHaveBeenCalled();
|
|
176
|
+
expect(screen.queryByRole("heading", { name: "New event" })).not.toBeInTheDocument();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("renders custom event chips and toolbar", async () => {
|
|
180
|
+
const user = userEvent.setup();
|
|
181
|
+
const onDateChange = vi.fn();
|
|
182
|
+
render(
|
|
183
|
+
<EventCalendar
|
|
184
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
185
|
+
events={events}
|
|
186
|
+
resources={resources}
|
|
187
|
+
locale="en-US"
|
|
188
|
+
showEventEditor={false}
|
|
189
|
+
showPreferences={false}
|
|
190
|
+
toolbarTrailing={<button type="button">Jump year</button>}
|
|
191
|
+
renderEvent={(event) => <span>★ {event.title}</span>}
|
|
192
|
+
renderToolbar={({ title, onToday }) => (
|
|
193
|
+
<div>
|
|
194
|
+
<p>{title}</p>
|
|
195
|
+
<button type="button" onClick={onToday}>
|
|
196
|
+
Jump today
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
)}
|
|
200
|
+
onDateChange={onDateChange}
|
|
201
|
+
/>,
|
|
202
|
+
);
|
|
203
|
+
expect(screen.getByText("★ Morning Run")).toBeInTheDocument();
|
|
204
|
+
expect(screen.queryByRole("button", { name: "Preferences" })).not.toBeInTheDocument();
|
|
205
|
+
expect(screen.queryByRole("button", { name: "Jump year" })).not.toBeInTheDocument();
|
|
206
|
+
await user.click(screen.getByRole("button", { name: "Jump today" }));
|
|
207
|
+
expect(onDateChange).toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("keeps toolbarTrailing on the default toolbar", () => {
|
|
211
|
+
render(
|
|
212
|
+
<EventCalendar
|
|
213
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
214
|
+
events={events}
|
|
215
|
+
locale="en-US"
|
|
216
|
+
showPreferences={false}
|
|
217
|
+
toolbarTrailing={<button type="button">Jump year</button>}
|
|
218
|
+
/>,
|
|
219
|
+
);
|
|
220
|
+
expect(screen.getByRole("button", { name: "Jump year" })).toBeInTheDocument();
|
|
221
|
+
expect(screen.getByRole("heading", { name: /August 2026/i })).toBeInTheDocument();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("renders a compact toolbar with equal month and year jumps", () => {
|
|
225
|
+
render(
|
|
226
|
+
<EventCalendar
|
|
227
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
228
|
+
events={events}
|
|
229
|
+
locale="en-US"
|
|
230
|
+
showPreferences={false}
|
|
231
|
+
toolbarDensity="compact"
|
|
232
|
+
showDateJump
|
|
233
|
+
/>,
|
|
234
|
+
);
|
|
235
|
+
expect(document.querySelector('[data-density="compact"]')).toBeTruthy();
|
|
236
|
+
expect(screen.getByLabelText("Month")).toHaveAttribute("data-size", "sm");
|
|
237
|
+
expect(screen.getByLabelText("Year")).toHaveAttribute("data-size", "sm");
|
|
238
|
+
expect(screen.getByLabelText("Month")).toHaveValue("7");
|
|
239
|
+
expect(screen.getByLabelText("Year")).toHaveValue("2026");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("keeps adjacent-month day numbers at full muted contrast", () => {
|
|
243
|
+
render(
|
|
244
|
+
<EventCalendar
|
|
245
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
246
|
+
events={[]}
|
|
247
|
+
locale="en-US"
|
|
248
|
+
showPreferences={false}
|
|
249
|
+
/>,
|
|
250
|
+
);
|
|
251
|
+
const julyDay = document.querySelector('[data-scheduler-day="2026-07-26"] button');
|
|
252
|
+
expect(julyDay).toBeTruthy();
|
|
253
|
+
expect(julyDay?.className).toMatch(/text-muted-foreground/);
|
|
254
|
+
expect(julyDay?.className).not.toMatch(/\/70/);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it("sizes month event chips for touch-friendly targets", () => {
|
|
258
|
+
render(
|
|
259
|
+
<EventCalendar
|
|
260
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
261
|
+
events={events}
|
|
262
|
+
locale="en-US"
|
|
263
|
+
showPreferences={false}
|
|
264
|
+
showEventEditor={false}
|
|
265
|
+
/>,
|
|
266
|
+
);
|
|
267
|
+
const birthday = screen.getByRole("button", { name: "Alice's Birthday" });
|
|
268
|
+
expect(birthday.className).toMatch(/min-h-6/);
|
|
269
|
+
const timed = screen.getByRole("button", { name: /Morning Run/i });
|
|
270
|
+
expect(timed.className).toMatch(/min-h-6/);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("reports the visible range for week view", () => {
|
|
274
|
+
const onVisibleRangeChange = vi.fn();
|
|
275
|
+
render(
|
|
276
|
+
<EventCalendar
|
|
277
|
+
defaultDate={new Date(2026, 7, 15)}
|
|
278
|
+
defaultView="week"
|
|
279
|
+
locale="en-US"
|
|
280
|
+
onVisibleRangeChange={onVisibleRangeChange}
|
|
281
|
+
/>,
|
|
282
|
+
);
|
|
283
|
+
expect(onVisibleRangeChange).toHaveBeenCalled();
|
|
284
|
+
const range = onVisibleRangeChange.mock.calls[0][0] as {
|
|
285
|
+
view: string;
|
|
286
|
+
start: Date;
|
|
287
|
+
end: Date;
|
|
288
|
+
};
|
|
289
|
+
expect(range.view).toBe("week");
|
|
290
|
+
expect(range.start.getDay()).toBe(0);
|
|
291
|
+
expect(range.end.getTime() - range.start.getTime()).toBe(7 * 86_400_000);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -666,7 +666,7 @@ function MonthGrid({
|
|
|
666
666
|
"flex size-7 items-center justify-center rounded-full text-xs font-bold",
|
|
667
667
|
isToday && "bg-primary text-primary-foreground",
|
|
668
668
|
!isToday && inMonth && "text-foreground hover:bg-muted/50",
|
|
669
|
-
!isToday && !inMonth && "text-muted-foreground
|
|
669
|
+
!isToday && !inMonth && "text-muted-foreground",
|
|
670
670
|
)}
|
|
671
671
|
>
|
|
672
672
|
{day.getDate()}
|
|
@@ -688,7 +688,7 @@ function MonthGrid({
|
|
|
688
688
|
>
|
|
689
689
|
{renderEventTrigger(
|
|
690
690
|
slot.event,
|
|
691
|
-
"flex h-
|
|
691
|
+
"flex min-h-6 w-full items-center truncate rounded-md px-1.5 text-[11px] font-semibold leading-none",
|
|
692
692
|
slot.event.title,
|
|
693
693
|
{ variant: "month-bar" },
|
|
694
694
|
)}
|
|
@@ -723,7 +723,7 @@ function MonthGrid({
|
|
|
723
723
|
<div key={occurrenceKey(event)} className="min-w-0">
|
|
724
724
|
{renderEventTrigger(
|
|
725
725
|
event,
|
|
726
|
-
"flex w-full min-w-0 items-center gap-1 rounded-sm px-0.5 py-0.5 text-left text-[11px] leading-tight text-foreground hover:bg-muted/40",
|
|
726
|
+
"flex min-h-6 w-full min-w-0 items-center gap-1 rounded-sm px-0.5 py-0.5 text-left text-[11px] leading-tight text-foreground hover:bg-muted/40",
|
|
727
727
|
<>
|
|
728
728
|
<span
|
|
729
729
|
className="size-1.5 shrink-0 rounded-full"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { RichTextEditor } from "./RichTextEditor";
|
|
4
|
+
|
|
5
|
+
vi.mock("@spatika/editor", () => ({
|
|
6
|
+
SpatikaEditor: ({
|
|
7
|
+
value,
|
|
8
|
+
placeholder,
|
|
9
|
+
heightClassName,
|
|
10
|
+
toolbar,
|
|
11
|
+
}: {
|
|
12
|
+
value: string;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
heightClassName?: string;
|
|
15
|
+
toolbar?: Record<string, unknown>;
|
|
16
|
+
}) => (
|
|
17
|
+
<div
|
|
18
|
+
data-testid="spatika-editor-shim"
|
|
19
|
+
data-value={value}
|
|
20
|
+
data-placeholder={placeholder}
|
|
21
|
+
data-height={heightClassName}
|
|
22
|
+
data-toolbar={JSON.stringify(toolbar)}
|
|
23
|
+
/>
|
|
24
|
+
),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
describe("RichTextEditor", () => {
|
|
28
|
+
it("delegates to SpatikaEditor with a compact legacy toolbar", () => {
|
|
29
|
+
render(
|
|
30
|
+
<RichTextEditor
|
|
31
|
+
value="<p>Note</p>"
|
|
32
|
+
placeholder="Write something…"
|
|
33
|
+
minHeightClassName="min-h-40"
|
|
34
|
+
/>,
|
|
35
|
+
);
|
|
36
|
+
const shim = screen.getByTestId("spatika-editor-shim");
|
|
37
|
+
expect(shim).toHaveAttribute("data-value", "<p>Note</p>");
|
|
38
|
+
expect(shim).toHaveAttribute("data-placeholder", "Write something…");
|
|
39
|
+
expect(shim).toHaveAttribute("data-height", "min-h-40");
|
|
40
|
+
expect(shim).toHaveAttribute(
|
|
41
|
+
"data-toolbar",
|
|
42
|
+
JSON.stringify({
|
|
43
|
+
layout: "compact",
|
|
44
|
+
textStyle: false,
|
|
45
|
+
alignment: false,
|
|
46
|
+
insert: false,
|
|
47
|
+
ai: false,
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
ListOrdered,
|
|
7
|
-
Underline as UnderlineIcon,
|
|
8
|
-
} from "lucide-react";
|
|
9
|
-
import { cn } from "../lib/cn";
|
|
10
|
-
import { Button } from "../primitives/Button";
|
|
1
|
+
/**
|
|
2
|
+
* @deprecated Use `SpatikaEditor` from `@spatika/react` instead.
|
|
3
|
+
* Import `@spatika/editor/styles.css` alongside `@spatika/tokens/styles.css`.
|
|
4
|
+
*/
|
|
5
|
+
import { SpatikaEditor } from "@spatika/editor";
|
|
11
6
|
|
|
12
7
|
export type RichTextEditorProps = {
|
|
13
8
|
value?: string;
|
|
@@ -17,10 +12,7 @@ export type RichTextEditorProps = {
|
|
|
17
12
|
minHeightClassName?: string;
|
|
18
13
|
};
|
|
19
14
|
|
|
20
|
-
/**
|
|
21
|
-
* Lightweight rich-text surface (contenteditable) for notes and captions.
|
|
22
|
-
* Pair with TipTap or another full editor in your app when you need tables or advanced media.
|
|
23
|
-
*/
|
|
15
|
+
/** @deprecated Use `SpatikaEditor` for full rich text editing with Tiptap. */
|
|
24
16
|
export function RichTextEditor({
|
|
25
17
|
value = "",
|
|
26
18
|
onChange,
|
|
@@ -28,76 +20,20 @@ export function RichTextEditor({
|
|
|
28
20
|
className,
|
|
29
21
|
minHeightClassName = "min-h-40",
|
|
30
22
|
}: RichTextEditorProps) {
|
|
31
|
-
const ref = useRef<HTMLDivElement>(null);
|
|
32
|
-
|
|
33
|
-
useEffect(() => {
|
|
34
|
-
if (ref.current && value !== ref.current.innerHTML) {
|
|
35
|
-
ref.current.innerHTML = value;
|
|
36
|
-
}
|
|
37
|
-
}, [value]);
|
|
38
|
-
|
|
39
|
-
const exec = (command: string) => {
|
|
40
|
-
document.execCommand(command);
|
|
41
|
-
onChange?.(ref.current?.innerHTML ?? "");
|
|
42
|
-
ref.current?.focus();
|
|
43
|
-
};
|
|
44
|
-
|
|
45
23
|
return (
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<Button
|
|
61
|
-
type="button"
|
|
62
|
-
variant="ghost"
|
|
63
|
-
size="icon"
|
|
64
|
-
aria-label="Underline"
|
|
65
|
-
onClick={() => exec("underline")}
|
|
66
|
-
>
|
|
67
|
-
<UnderlineIcon className="size-4" />
|
|
68
|
-
</Button>
|
|
69
|
-
<Button
|
|
70
|
-
type="button"
|
|
71
|
-
variant="ghost"
|
|
72
|
-
size="icon"
|
|
73
|
-
aria-label="Bullet list"
|
|
74
|
-
onClick={() => exec("insertUnorderedList")}
|
|
75
|
-
>
|
|
76
|
-
<List className="size-4" />
|
|
77
|
-
</Button>
|
|
78
|
-
<Button
|
|
79
|
-
type="button"
|
|
80
|
-
variant="ghost"
|
|
81
|
-
size="icon"
|
|
82
|
-
aria-label="Numbered list"
|
|
83
|
-
onClick={() => exec("insertOrderedList")}
|
|
84
|
-
>
|
|
85
|
-
<ListOrdered className="size-4" />
|
|
86
|
-
</Button>
|
|
87
|
-
</div>
|
|
88
|
-
<div
|
|
89
|
-
ref={ref}
|
|
90
|
-
role="textbox"
|
|
91
|
-
aria-multiline="true"
|
|
92
|
-
contentEditable
|
|
93
|
-
suppressContentEditableWarning
|
|
94
|
-
data-placeholder={placeholder}
|
|
95
|
-
className={cn(
|
|
96
|
-
"px-3 py-2 text-sm outline-none empty:before:text-muted-foreground empty:before:content-[attr(data-placeholder)]",
|
|
97
|
-
minHeightClassName,
|
|
98
|
-
)}
|
|
99
|
-
onInput={() => onChange?.(ref.current?.innerHTML ?? "")}
|
|
100
|
-
/>
|
|
101
|
-
</div>
|
|
24
|
+
<SpatikaEditor
|
|
25
|
+
value={value}
|
|
26
|
+
onChange={(html) => onChange?.(html)}
|
|
27
|
+
placeholder={placeholder}
|
|
28
|
+
className={className}
|
|
29
|
+
heightClassName={minHeightClassName}
|
|
30
|
+
toolbar={{
|
|
31
|
+
layout: "compact",
|
|
32
|
+
textStyle: false,
|
|
33
|
+
alignment: false,
|
|
34
|
+
insert: false,
|
|
35
|
+
ai: false,
|
|
36
|
+
}}
|
|
37
|
+
/>
|
|
102
38
|
);
|
|
103
39
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { render } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { SparkLineChart } from "@spatika/charts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `chart-ui` moved to `@spatika/charts`. This file keeps colocated coverage
|
|
7
|
+
* next to the old path so the extract stays test-paired.
|
|
8
|
+
*/
|
|
9
|
+
describe("chart-ui (re-exported via @spatika/charts)", () => {
|
|
10
|
+
it("still renders a sparkline frame that uses ChartFrame", () => {
|
|
11
|
+
const { container } = render(
|
|
12
|
+
<SparkLineChart data={[1, 4, 2]} width={120} height={36} />,
|
|
13
|
+
);
|
|
14
|
+
expect(container.querySelector('[data-slot="sparkline"]')).toBeTruthy();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("does not paint a tooltip when nothing is hovered", () => {
|
|
18
|
+
render(<SparkLineChart data={[1, 4, 2]} width={120} height={36} />);
|
|
19
|
+
expect(document.querySelector('[role="tooltip"]')).toBeNull();
|
|
20
|
+
});
|
|
21
|
+
});
|