@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.
Files changed (93) hide show
  1. package/AGENTS.md +6 -3
  2. package/README.md +1 -1
  3. package/dist/composites/EventCalendar.js +3 -3
  4. package/dist/composites/GlassCard.d.ts +1 -1
  5. package/dist/composites/RichTextEditor.d.ts +1 -4
  6. package/dist/composites/RichTextEditor.d.ts.map +1 -1
  7. package/dist/composites/RichTextEditor.js +12 -19
  8. package/dist/composites/SurfaceCard.d.ts +1 -1
  9. package/dist/composites/Tag.d.ts +2 -2
  10. package/dist/index.d.ts +8 -22
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +5 -11
  13. package/dist/lib/breakpoints.d.ts +17 -0
  14. package/dist/lib/breakpoints.d.ts.map +1 -0
  15. package/dist/lib/breakpoints.js +23 -0
  16. package/dist/lib/create-theme.d.ts +162 -0
  17. package/dist/lib/create-theme.d.ts.map +1 -0
  18. package/dist/lib/create-theme.js +138 -0
  19. package/dist/lib/scheduler.js +2 -2
  20. package/dist/lib/themes.d.ts +17 -7
  21. package/dist/lib/themes.d.ts.map +1 -1
  22. package/dist/lib/themes.js +22 -8
  23. package/dist/lib/use-media-query.d.ts +9 -0
  24. package/dist/lib/use-media-query.d.ts.map +1 -1
  25. package/dist/lib/use-media-query.js +28 -1
  26. package/dist/primitives/Alert.d.ts +1 -1
  27. package/dist/primitives/Badge.d.ts +2 -2
  28. package/dist/primitives/Button.d.ts +2 -2
  29. package/dist/primitives/Container.d.ts +2 -2
  30. package/dist/primitives/Fab.d.ts +4 -4
  31. package/dist/primitives/Grid.d.ts +2 -1
  32. package/dist/primitives/Grid.d.ts.map +1 -1
  33. package/dist/primitives/Grid.js +10 -2
  34. package/dist/primitives/IconButton.d.ts +2 -2
  35. package/dist/primitives/ToggleButton.d.ts +1 -1
  36. package/dist/theme/SpatikaThemeProvider.d.ts +4 -5
  37. package/dist/theme/SpatikaThemeProvider.d.ts.map +1 -1
  38. package/dist/theme/SpatikaThemeProvider.js +28 -13
  39. package/package.json +4 -2
  40. package/skills/spatika-ui/SKILL.md +25 -6
  41. package/src/composites/EventCalendar.test.tsx +293 -0
  42. package/src/composites/EventCalendar.tsx +3 -3
  43. package/src/composites/RichTextEditor.test.tsx +51 -0
  44. package/src/composites/RichTextEditor.tsx +20 -84
  45. package/src/composites/chart-ui.test.tsx +21 -0
  46. package/src/composites/scheduler.test.tsx +0 -258
  47. package/src/index.test.ts +6 -0
  48. package/src/index.ts +138 -72
  49. package/src/lib/breakpoints.test.ts +29 -0
  50. package/src/lib/breakpoints.ts +28 -0
  51. package/src/lib/create-theme.test.ts +79 -0
  52. package/src/lib/create-theme.ts +249 -0
  53. package/src/lib/scheduler.test.ts +10 -0
  54. package/src/lib/scheduler.ts +2 -2
  55. package/src/lib/themes.test.ts +17 -0
  56. package/src/lib/themes.ts +43 -12
  57. package/src/lib/use-media-query.test.tsx +80 -0
  58. package/src/lib/use-media-query.ts +35 -1
  59. package/src/primitives/Grid.test.tsx +55 -0
  60. package/src/primitives/Grid.tsx +13 -3
  61. package/src/primitives/mui-more.test.tsx +17 -0
  62. package/src/theme/SpatikaThemeProvider.test.tsx +38 -0
  63. package/src/theme/SpatikaThemeProvider.tsx +43 -15
  64. package/src/composites/ChartDataGrid.test.tsx +0 -57
  65. package/src/composites/ChartDataGrid.tsx +0 -196
  66. package/src/composites/MapChart.test.tsx +0 -39
  67. package/src/composites/MapChart.tsx +0 -130
  68. package/src/composites/ScatterWebGL.test.tsx +0 -25
  69. package/src/composites/ScatterWebGL.tsx +0 -79
  70. package/src/composites/cartesian-charts.test.tsx +0 -136
  71. package/src/composites/cartesian-charts.tsx +0 -1355
  72. package/src/composites/chart-composition.test.tsx +0 -68
  73. package/src/composites/chart-composition.tsx +0 -1039
  74. package/src/composites/chart-interaction.ts +0 -136
  75. package/src/composites/chart-ui.tsx +0 -336
  76. package/src/composites/charts-3d.test.tsx +0 -25
  77. package/src/composites/charts-3d.tsx +0 -318
  78. package/src/composites/charts.test.tsx +0 -254
  79. package/src/composites/flow-charts.test.tsx +0 -98
  80. package/src/composites/flow-charts.tsx +0 -641
  81. package/src/composites/radial-charts.test.tsx +0 -89
  82. package/src/composites/radial-charts.tsx +0 -643
  83. package/src/lib/chart-export.test.ts +0 -20
  84. package/src/lib/chart-export.ts +0 -99
  85. package/src/lib/charts-platform.test.ts +0 -93
  86. package/src/lib/charts.test.ts +0 -132
  87. package/src/lib/charts.ts +0 -1021
  88. package/src/lib/geo.test.ts +0 -42
  89. package/src/lib/geo.ts +0 -138
  90. package/src/lib/iso.test.ts +0 -22
  91. package/src/lib/iso.ts +0 -102
  92. package/src/lib/webgl-scatter.test.ts +0 -36
  93. 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 and Heatmap accept the same click and tooltip slots.
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 component markdown: `https://spatika.sweyam.com/docs/{slug}.md`
99
- 2. Read prop types from `node_modules/@spatika/react/dist/index.d.ts`
100
- 3. Icons in examples use `lucide-react`
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/70",
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-5 w-full items-center truncate rounded-md px-1.5 text-[11px] font-semibold leading-none",
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
- import { useEffect, useRef } from "react";
2
- import {
3
- Bold,
4
- Italic,
5
- List,
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
- <div
47
- data-slot="rich-text-editor"
48
- className={cn(
49
- "overflow-hidden rounded-[var(--radius-card)] border border-border/50 bg-input-background",
50
- className,
51
- )}
52
- >
53
- <div className="flex flex-wrap gap-1 border-b border-border/40 bg-muted/30 p-1.5">
54
- <Button type="button" variant="ghost" size="icon" aria-label="Bold" onClick={() => exec("bold")}>
55
- <Bold className="size-4" />
56
- </Button>
57
- <Button type="button" variant="ghost" size="icon" aria-label="Italic" onClick={() => exec("italic")}>
58
- <Italic className="size-4" />
59
- </Button>
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
+ });