@spatika/react 1.3.1 → 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.
- package/AGENTS.md +6 -3
- package/README.md +1 -1
- package/dist/composites/EventCalendar.d.ts +13 -2
- package/dist/composites/EventCalendar.d.ts.map +1 -1
- package/dist/composites/EventCalendar.js +13 -27
- 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/composites/cartesian-charts.d.ts +12 -2
- package/dist/composites/cartesian-charts.d.ts.map +1 -1
- package/dist/composites/cartesian-charts.js +27 -13
- package/dist/composites/chart-composition.d.ts +19 -1
- package/dist/composites/chart-composition.d.ts.map +1 -1
- package/dist/composites/chart-composition.js +124 -62
- package/dist/composites/chart-interaction.d.ts +81 -0
- package/dist/composites/chart-interaction.d.ts.map +1 -0
- package/dist/composites/chart-interaction.js +56 -0
- package/dist/composites/chart-ui.d.ts +8 -13
- package/dist/composites/chart-ui.d.ts.map +1 -1
- package/dist/composites/chart-ui.js +9 -6
- package/dist/composites/flow-charts.d.ts +6 -1
- package/dist/composites/flow-charts.d.ts.map +1 -1
- package/dist/composites/flow-charts.js +28 -14
- package/dist/composites/radial-charts.d.ts +11 -1
- package/dist/composites/radial-charts.d.ts.map +1 -1
- package/dist/composites/radial-charts.js +33 -14
- package/dist/composites/scheduler-ui.d.ts +14 -1
- package/dist/composites/scheduler-ui.d.ts.map +1 -1
- package/dist/composites/scheduler-ui.js +23 -2
- package/dist/index.d.ts +14 -26
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -14
- 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/chips.d.ts +3 -0
- package/dist/lib/chips.d.ts.map +1 -1
- package/dist/lib/chips.js +10 -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.d.ts +12 -0
- package/dist/lib/scheduler.d.ts.map +1 -1
- package/dist/lib/scheduler.js +24 -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/NativeSelect.d.ts +10 -1
- package/dist/primitives/NativeSelect.d.ts.map +1 -1
- package/dist/primitives/NativeSelect.js +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 +28 -5
- package/src/composites/EventCalendar.test.tsx +293 -0
- package/src/composites/EventCalendar.tsx +38 -27
- 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-ui.tsx +128 -26
- package/src/composites/scheduler.test.tsx +0 -219
- package/src/index.test.ts +10 -0
- package/src/index.ts +148 -65
- package/src/lib/breakpoints.test.ts +29 -0
- package/src/lib/breakpoints.ts +28 -0
- package/src/lib/chips.ts +16 -0
- package/src/lib/create-theme.test.ts +79 -0
- package/src/lib/create-theme.ts +249 -0
- package/src/lib/scheduler.test.ts +26 -0
- package/src/lib/scheduler.ts +36 -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/NativeSelect.test.tsx +28 -0
- package/src/primitives/NativeSelect.tsx +14 -4
- 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 -52
- package/src/composites/cartesian-charts.tsx +0 -1287
- package/src/composites/chart-composition.test.tsx +0 -53
- package/src/composites/chart-composition.tsx +0 -898
- package/src/composites/chart-ui.tsx +0 -328
- 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 -79
- package/src/composites/flow-charts.tsx +0 -607
- package/src/composites/radial-charts.test.tsx +0 -74
- package/src/composites/radial-charts.tsx +0 -617
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
useEffect,
|
|
2
3
|
useMemo,
|
|
3
4
|
useRef,
|
|
4
5
|
useState,
|
|
@@ -13,6 +14,8 @@ import {
|
|
|
13
14
|
addDays,
|
|
14
15
|
addMonths,
|
|
15
16
|
agendaDays,
|
|
17
|
+
calendarVisibleRange,
|
|
18
|
+
CALENDAR_AGENDA_DAYS,
|
|
16
19
|
DEFAULT_SCHEDULER_PREFERENCES,
|
|
17
20
|
eventCanDrag,
|
|
18
21
|
eventCanResize,
|
|
@@ -44,6 +47,7 @@ import {
|
|
|
44
47
|
weekNumber,
|
|
45
48
|
weekSpanLanesForDays,
|
|
46
49
|
type CalendarView,
|
|
50
|
+
type CalendarVisibleRange,
|
|
47
51
|
type NormalizedEvent,
|
|
48
52
|
type SchedulerEvent,
|
|
49
53
|
type SchedulerPreferences,
|
|
@@ -54,9 +58,11 @@ import {
|
|
|
54
58
|
CALENDAR_VIEW_LABEL,
|
|
55
59
|
EventResizeHandle,
|
|
56
60
|
ResourceLegend,
|
|
61
|
+
SchedulerDateJump,
|
|
57
62
|
SchedulerPreferencesMenu,
|
|
58
63
|
SchedulerShell,
|
|
59
64
|
SchedulerToolbar,
|
|
65
|
+
type SchedulerToolbarDensity,
|
|
60
66
|
} from "./scheduler-ui";
|
|
61
67
|
import { DropdownMenuItem } from "../primitives/DropdownMenu";
|
|
62
68
|
|
|
@@ -125,6 +131,14 @@ export type EventCalendarProps = {
|
|
|
125
131
|
showPreferences?: boolean;
|
|
126
132
|
/** Extra controls after the view menu. Ignored when `renderToolbar` is set. */
|
|
127
133
|
toolbarTrailing?: ReactNode;
|
|
134
|
+
/** Default toolbar layout. Compact is a single desktop row. Ignored when `renderToolbar` is set. */
|
|
135
|
+
toolbarDensity?: SchedulerToolbarDensity;
|
|
136
|
+
/** Month/year native selects on the default toolbar. Ignored when `renderToolbar` is set. */
|
|
137
|
+
showDateJump?: boolean;
|
|
138
|
+
/** Years in the date-jump select. Defaults to a window around the current year. */
|
|
139
|
+
yearOptions?: number[];
|
|
140
|
+
/** Fires when the painted date window changes. `end` is exclusive. */
|
|
141
|
+
onVisibleRangeChange?: (range: CalendarVisibleRange & { view: CalendarView }) => void;
|
|
128
142
|
/** Replace event chip contents. The kit still wraps with the click/drag trigger. */
|
|
129
143
|
renderEvent?: (
|
|
130
144
|
event: NormalizedEvent,
|
|
@@ -136,7 +150,6 @@ export type EventCalendarProps = {
|
|
|
136
150
|
};
|
|
137
151
|
|
|
138
152
|
const MAX_TIMED_IN_CELL = 3;
|
|
139
|
-
const AGENDA_LENGTH = 14;
|
|
140
153
|
|
|
141
154
|
function chunkWeeks<T>(items: T[]): T[][] {
|
|
142
155
|
const weeks: T[][] = [];
|
|
@@ -153,27 +166,6 @@ function startOfToday(): Date {
|
|
|
153
166
|
return new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
|
154
167
|
}
|
|
155
168
|
|
|
156
|
-
function visibleRange(
|
|
157
|
-
view: CalendarView,
|
|
158
|
-
date: Date,
|
|
159
|
-
weekStartsOn: number,
|
|
160
|
-
): { start: Date; end: Date } {
|
|
161
|
-
if (view === "month") {
|
|
162
|
-
const grid = getMonthGrid(date, weekStartsOn);
|
|
163
|
-
return { start: grid[0], end: addDays(grid[grid.length - 1], 1) };
|
|
164
|
-
}
|
|
165
|
-
if (view === "week") {
|
|
166
|
-
const days = getWeekDays(date, weekStartsOn);
|
|
167
|
-
return { start: days[0], end: addDays(days[6], 1) };
|
|
168
|
-
}
|
|
169
|
-
if (view === "agenda") {
|
|
170
|
-
const days = agendaDays(date, AGENDA_LENGTH);
|
|
171
|
-
return { start: days[0], end: addDays(days[days.length - 1], 1) };
|
|
172
|
-
}
|
|
173
|
-
const start = startOfDay(date);
|
|
174
|
-
return { start, end: addDays(start, 1) };
|
|
175
|
-
}
|
|
176
|
-
|
|
177
169
|
function draftFromSource(source: SchedulerEvent): EventEditorDraft {
|
|
178
170
|
return {
|
|
179
171
|
id: source.id,
|
|
@@ -217,6 +209,10 @@ export function EventCalendar({
|
|
|
217
209
|
showEventEditor = true,
|
|
218
210
|
showPreferences = true,
|
|
219
211
|
toolbarTrailing,
|
|
212
|
+
toolbarDensity = "default",
|
|
213
|
+
showDateJump = false,
|
|
214
|
+
yearOptions,
|
|
215
|
+
onVisibleRangeChange,
|
|
220
216
|
renderEvent,
|
|
221
217
|
renderToolbar,
|
|
222
218
|
className,
|
|
@@ -253,10 +249,14 @@ export function EventCalendar({
|
|
|
253
249
|
const editorLocked = readOnly || Boolean(draft?.id && sourceEvent(events, draft.id)?.readOnly);
|
|
254
250
|
|
|
255
251
|
const range = useMemo(
|
|
256
|
-
() =>
|
|
252
|
+
() => calendarVisibleRange(resolvedView, resolvedDate, startOn),
|
|
257
253
|
[resolvedView, resolvedDate, startOn],
|
|
258
254
|
);
|
|
259
255
|
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
onVisibleRangeChange?.({ start: range.start, end: range.end, view: resolvedView });
|
|
258
|
+
}, [range.start, range.end, resolvedView, onVisibleRangeChange]);
|
|
259
|
+
|
|
260
260
|
const normalized = useMemo(() => {
|
|
261
261
|
const visible = visibleEvents(normalizeEvents(events, resources), hiddenIds);
|
|
262
262
|
return expandVisibleEvents(visible, range.start, range.end, events);
|
|
@@ -473,6 +473,7 @@ export function EventCalendar({
|
|
|
473
473
|
) : (
|
|
474
474
|
<SchedulerToolbar
|
|
475
475
|
title={title}
|
|
476
|
+
density={toolbarDensity}
|
|
476
477
|
onPrev={() => go(-1)}
|
|
477
478
|
onNext={() => go(1)}
|
|
478
479
|
onToday={() => setDate(new Date())}
|
|
@@ -482,6 +483,16 @@ export function EventCalendar({
|
|
|
482
483
|
{CALENDAR_VIEW_LABEL[item]}
|
|
483
484
|
</DropdownMenuItem>
|
|
484
485
|
))}
|
|
486
|
+
dateJump={
|
|
487
|
+
showDateJump ? (
|
|
488
|
+
<SchedulerDateJump
|
|
489
|
+
date={resolvedDate}
|
|
490
|
+
locale={locale}
|
|
491
|
+
yearOptions={yearOptions}
|
|
492
|
+
onDateChange={(next) => setDate(next)}
|
|
493
|
+
/>
|
|
494
|
+
) : null
|
|
495
|
+
}
|
|
485
496
|
trailing={
|
|
486
497
|
<>
|
|
487
498
|
{toolbarTrailing}
|
|
@@ -655,7 +666,7 @@ function MonthGrid({
|
|
|
655
666
|
"flex size-7 items-center justify-center rounded-full text-xs font-bold",
|
|
656
667
|
isToday && "bg-primary text-primary-foreground",
|
|
657
668
|
!isToday && inMonth && "text-foreground hover:bg-muted/50",
|
|
658
|
-
!isToday && !inMonth && "text-muted-foreground
|
|
669
|
+
!isToday && !inMonth && "text-muted-foreground",
|
|
659
670
|
)}
|
|
660
671
|
>
|
|
661
672
|
{day.getDate()}
|
|
@@ -677,7 +688,7 @@ function MonthGrid({
|
|
|
677
688
|
>
|
|
678
689
|
{renderEventTrigger(
|
|
679
690
|
slot.event,
|
|
680
|
-
"flex h-
|
|
691
|
+
"flex min-h-6 w-full items-center truncate rounded-md px-1.5 text-[11px] font-semibold leading-none",
|
|
681
692
|
slot.event.title,
|
|
682
693
|
{ variant: "month-bar" },
|
|
683
694
|
)}
|
|
@@ -712,7 +723,7 @@ function MonthGrid({
|
|
|
712
723
|
<div key={occurrenceKey(event)} className="min-w-0">
|
|
713
724
|
{renderEventTrigger(
|
|
714
725
|
event,
|
|
715
|
-
"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",
|
|
716
727
|
<>
|
|
717
728
|
<span
|
|
718
729
|
className="size-1.5 shrink-0 rounded-full"
|
|
@@ -964,7 +975,7 @@ function AgendaList({
|
|
|
964
975
|
options?: { resize?: "ns"; variant: EventCalendarEventVariant },
|
|
965
976
|
) => ReactNode;
|
|
966
977
|
}) {
|
|
967
|
-
const days = filterWeekendDays(agendaDays(date,
|
|
978
|
+
const days = filterWeekendDays(agendaDays(date, CALENDAR_AGENDA_DAYS), showWeekends);
|
|
968
979
|
|
|
969
980
|
return (
|
|
970
981
|
<div className="divide-y divide-border/50">
|
|
@@ -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
|
+
});
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type SchedulerResource,
|
|
13
13
|
type TimelineScale,
|
|
14
14
|
} from "../lib/scheduler";
|
|
15
|
+
import { NativeSelect } from "../primitives/NativeSelect";
|
|
15
16
|
import { Button } from "../primitives/Button";
|
|
16
17
|
import {
|
|
17
18
|
DropdownMenu,
|
|
@@ -36,6 +37,8 @@ export const TIMELINE_SCALE_LABEL: Record<TimelineScale, string> = {
|
|
|
36
37
|
years: "Years",
|
|
37
38
|
};
|
|
38
39
|
|
|
40
|
+
export type SchedulerToolbarDensity = "default" | "compact";
|
|
41
|
+
|
|
39
42
|
export type SchedulerToolbarProps = {
|
|
40
43
|
title: ReactNode;
|
|
41
44
|
onPrev: () => void;
|
|
@@ -44,6 +47,10 @@ export type SchedulerToolbarProps = {
|
|
|
44
47
|
menuLabel: string;
|
|
45
48
|
menu: ReactNode;
|
|
46
49
|
trailing?: ReactNode;
|
|
50
|
+
/** Compact is a single desktop row. Defaults keep the stacked marketing toolbar. */
|
|
51
|
+
density?: SchedulerToolbarDensity;
|
|
52
|
+
/** Month/year jump. Sits beside the view menu. */
|
|
53
|
+
dateJump?: ReactNode;
|
|
47
54
|
};
|
|
48
55
|
|
|
49
56
|
export function SchedulerToolbar({
|
|
@@ -54,48 +61,143 @@ export function SchedulerToolbar({
|
|
|
54
61
|
menuLabel,
|
|
55
62
|
menu,
|
|
56
63
|
trailing,
|
|
64
|
+
density = "default",
|
|
65
|
+
dateJump,
|
|
57
66
|
}: SchedulerToolbarProps) {
|
|
67
|
+
const nav = (
|
|
68
|
+
<div className="flex items-center gap-1">
|
|
69
|
+
<Button
|
|
70
|
+
type="button"
|
|
71
|
+
variant="ghost"
|
|
72
|
+
size="icon"
|
|
73
|
+
aria-label="Previous"
|
|
74
|
+
onClick={onPrev}
|
|
75
|
+
>
|
|
76
|
+
<ChevronLeft className="size-4" />
|
|
77
|
+
</Button>
|
|
78
|
+
<Button type="button" variant="outline" size="sm" className="uppercase tracking-wide" onClick={onToday}>
|
|
79
|
+
Today
|
|
80
|
+
</Button>
|
|
81
|
+
<Button type="button" variant="ghost" size="icon" aria-label="Next" onClick={onNext}>
|
|
82
|
+
<ChevronRight className="size-4" />
|
|
83
|
+
</Button>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const viewMenu = (
|
|
88
|
+
<DropdownMenu>
|
|
89
|
+
<DropdownMenuTrigger asChild>
|
|
90
|
+
<Button type="button" variant="outline" size="sm" className="min-w-[6.5rem] justify-between gap-2 uppercase tracking-wide">
|
|
91
|
+
{menuLabel}
|
|
92
|
+
<ChevronDown className="size-3.5 opacity-70" />
|
|
93
|
+
</Button>
|
|
94
|
+
</DropdownMenuTrigger>
|
|
95
|
+
<DropdownMenuContent align="end">{menu}</DropdownMenuContent>
|
|
96
|
+
</DropdownMenu>
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
if (density === "compact") {
|
|
100
|
+
return (
|
|
101
|
+
<div
|
|
102
|
+
data-slot="scheduler-toolbar"
|
|
103
|
+
data-density="compact"
|
|
104
|
+
className="grid grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 border-b border-border/50 px-3 py-2 sm:px-4 sm:py-3"
|
|
105
|
+
>
|
|
106
|
+
{nav}
|
|
107
|
+
<h2 className="min-w-0 truncate text-center text-sm font-black uppercase tracking-widest text-foreground">
|
|
108
|
+
{title}
|
|
109
|
+
</h2>
|
|
110
|
+
<div className="flex shrink-0 flex-nowrap items-center justify-end gap-2">
|
|
111
|
+
{viewMenu}
|
|
112
|
+
{dateJump}
|
|
113
|
+
{trailing}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
58
119
|
return (
|
|
59
120
|
<div
|
|
60
121
|
data-slot="scheduler-toolbar"
|
|
122
|
+
data-density="default"
|
|
61
123
|
className="flex flex-col gap-3 border-b border-border/50 px-3 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-4"
|
|
62
124
|
>
|
|
63
125
|
<h2 className="min-w-0 truncate text-xl font-black tracking-tight text-foreground sm:text-2xl">
|
|
64
126
|
{title}
|
|
65
127
|
</h2>
|
|
66
128
|
<div className="flex flex-wrap items-center gap-2">
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
variant="ghost"
|
|
71
|
-
size="icon"
|
|
72
|
-
aria-label="Previous"
|
|
73
|
-
onClick={onPrev}
|
|
74
|
-
>
|
|
75
|
-
<ChevronLeft className="size-4" />
|
|
76
|
-
</Button>
|
|
77
|
-
<Button type="button" variant="outline" size="sm" className="uppercase tracking-wide" onClick={onToday}>
|
|
78
|
-
Today
|
|
79
|
-
</Button>
|
|
80
|
-
<Button type="button" variant="ghost" size="icon" aria-label="Next" onClick={onNext}>
|
|
81
|
-
<ChevronRight className="size-4" />
|
|
82
|
-
</Button>
|
|
83
|
-
</div>
|
|
84
|
-
<DropdownMenu>
|
|
85
|
-
<DropdownMenuTrigger asChild>
|
|
86
|
-
<Button type="button" variant="outline" size="sm" className="min-w-[6.5rem] justify-between gap-2 uppercase tracking-wide">
|
|
87
|
-
{menuLabel}
|
|
88
|
-
<ChevronDown className="size-3.5 opacity-70" />
|
|
89
|
-
</Button>
|
|
90
|
-
</DropdownMenuTrigger>
|
|
91
|
-
<DropdownMenuContent align="end">{menu}</DropdownMenuContent>
|
|
92
|
-
</DropdownMenu>
|
|
129
|
+
{nav}
|
|
130
|
+
{viewMenu}
|
|
131
|
+
{dateJump}
|
|
93
132
|
{trailing}
|
|
94
133
|
</div>
|
|
95
134
|
</div>
|
|
96
135
|
);
|
|
97
136
|
}
|
|
98
137
|
|
|
138
|
+
const DATE_JUMP_SELECT =
|
|
139
|
+
"w-[5.25rem] min-w-[5.25rem] text-center [text-align-last:center]";
|
|
140
|
+
|
|
141
|
+
export type SchedulerDateJumpProps = {
|
|
142
|
+
date: Date;
|
|
143
|
+
locale?: string;
|
|
144
|
+
yearOptions?: number[];
|
|
145
|
+
onDateChange: (date: Date) => void;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
function resolveYearOptions(year: number, yearOptions?: number[]) {
|
|
149
|
+
if (yearOptions && yearOptions.length > 0) {
|
|
150
|
+
return yearOptions.includes(year) ? yearOptions : [...yearOptions, year].sort((a, b) => a - b);
|
|
151
|
+
}
|
|
152
|
+
return Array.from({ length: 13 }, (_, index) => year - 8 + index);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Compact month/year native selects for calendar toolbars. */
|
|
156
|
+
export function SchedulerDateJump({
|
|
157
|
+
date,
|
|
158
|
+
locale,
|
|
159
|
+
yearOptions,
|
|
160
|
+
onDateChange,
|
|
161
|
+
}: SchedulerDateJumpProps) {
|
|
162
|
+
const month = date.getMonth();
|
|
163
|
+
const year = date.getFullYear();
|
|
164
|
+
const years = resolveYearOptions(year, yearOptions);
|
|
165
|
+
const months = Array.from({ length: 12 }, (_, index) =>
|
|
166
|
+
new Intl.DateTimeFormat(locale, { month: "short" }).format(new Date(2020, index, 1)).toUpperCase(),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div className="flex shrink-0 items-center gap-1.5">
|
|
171
|
+
<NativeSelect
|
|
172
|
+
size="sm"
|
|
173
|
+
className={DATE_JUMP_SELECT}
|
|
174
|
+
aria-label="Month"
|
|
175
|
+
value={month}
|
|
176
|
+
onChange={(event) => onDateChange(new Date(year, Number(event.target.value), 1))}
|
|
177
|
+
>
|
|
178
|
+
{months.map((label, index) => (
|
|
179
|
+
<option key={label} value={index}>
|
|
180
|
+
{label}
|
|
181
|
+
</option>
|
|
182
|
+
))}
|
|
183
|
+
</NativeSelect>
|
|
184
|
+
<NativeSelect
|
|
185
|
+
size="sm"
|
|
186
|
+
className={cn(DATE_JUMP_SELECT, "tabular-nums")}
|
|
187
|
+
aria-label="Year"
|
|
188
|
+
value={year}
|
|
189
|
+
onChange={(event) => onDateChange(new Date(Number(event.target.value), month, 1))}
|
|
190
|
+
>
|
|
191
|
+
{years.map((yearOption) => (
|
|
192
|
+
<option key={yearOption} value={yearOption}>
|
|
193
|
+
{yearOption}
|
|
194
|
+
</option>
|
|
195
|
+
))}
|
|
196
|
+
</NativeSelect>
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
99
201
|
export function SchedulerPreferencesMenu({
|
|
100
202
|
value,
|
|
101
203
|
onChange,
|