atlas-react-kit 0.12.0 → 1.0.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/COMPONENTS.md +201 -100
- package/DESIGN_RULES.md +44 -1
- package/README.md +10 -9
- package/dist/accordion.d.ts +1 -0
- package/dist/alert.d.ts +1 -1
- package/dist/avatar.d.ts +1 -0
- package/dist/badge.d.ts +4 -5
- package/dist/breadcrumb.d.ts +2 -3
- package/dist/button.d.ts +4 -5
- package/dist/calendar.d.ts +2 -0
- package/dist/card.d.ts +3 -0
- package/dist/chart.d.ts +152 -0
- package/dist/chart.js +177 -0
- package/dist/color-legend.d.ts +8 -0
- package/dist/combobox.d.ts +13 -4
- package/dist/combobox.js +1 -8
- package/dist/date-picker.d.ts +16 -6
- package/dist/date-picker.js +13 -20
- package/dist/date-time-picker.d.ts +5 -2
- package/dist/date-time-picker.js +6 -5
- package/dist/dialog.d.ts +1 -0
- package/dist/dialog.js +4 -2
- package/dist/dropdown-menu.d.ts +9 -8
- package/dist/empty-panel.d.ts +8 -2
- package/dist/empty-panel.js +2 -2
- package/dist/error-boundary.d.ts +7 -2
- package/dist/field.d.ts +4 -1
- package/dist/field.js +1 -1
- package/dist/form/checkbox-field.d.ts +45 -0
- package/dist/form/checkbox-field.js +39 -0
- package/dist/form/combobox-field.d.ts +13 -0
- package/dist/form/combobox-field.js +14 -0
- package/dist/form/control-field.d.ts +30 -0
- package/dist/form/control-field.js +22 -0
- package/dist/form/date-picker-field.d.ts +7 -21
- package/dist/form/date-picker-field.js +4 -4
- package/dist/form/date-time-picker-field.d.ts +7 -22
- package/dist/form/date-time-picker-field.js +3 -3
- package/dist/form/feedback-field.d.ts +2 -0
- package/dist/form/field-error.d.ts +49 -0
- package/dist/form/field-shell.d.ts +47 -8
- package/dist/form/field-shell.js +24 -7
- package/dist/form/markdown-field.d.ts +4 -16
- package/dist/form/markdown-field.js +2 -2
- package/dist/form/select-field.d.ts +7 -7
- package/dist/form/select-field.js +2 -2
- package/dist/form/text-field.d.ts +29 -12
- package/dist/form/text-field.js +10 -6
- package/dist/form/toggle-group-field.d.ts +30 -0
- package/dist/form/toggle-group-field.js +19 -0
- package/dist/info-tip.d.ts +10 -1
- package/dist/internal/as-child.d.ts +8 -0
- package/dist/internal/as-child.js +1 -0
- package/dist/internal/date-like.d.ts +23 -0
- package/dist/internal/form-control.d.ts +3 -2
- package/dist/internal/form-control.js +3 -2
- package/dist/internal/kanban-layout.d.ts +55 -0
- package/dist/internal/kanban-layout.js +80 -0
- package/dist/internal/kanban-plugins.d.ts +17 -0
- package/dist/internal/kanban-plugins.js +17 -0
- package/dist/internal/use-media-query.d.ts +4 -2
- package/dist/internal/use-media-query.js +11 -15
- package/dist/kanban.d.ts +84 -0
- package/dist/kanban.js +197 -0
- package/dist/markdown.d.ts +2 -0
- package/dist/meter.d.ts +7 -0
- package/dist/nav-link.d.ts +23 -0
- package/dist/nav-link.js +31 -0
- package/dist/next/nav-link.d.ts +11 -0
- package/dist/next/nav-link.js +14 -0
- package/dist/next/period-stepper.d.ts +11 -0
- package/dist/next/period-stepper.js +16 -0
- package/dist/page-header.d.ts +19 -0
- package/dist/page-header.js +11 -0
- package/dist/pagination.d.ts +2 -0
- package/dist/period-stepper.d.ts +32 -0
- package/dist/period-stepper.js +27 -0
- package/dist/picker-shell.d.ts +43 -6
- package/dist/picker-shell.js +2 -2
- package/dist/progress.d.ts +1 -0
- package/dist/search-picker.d.ts +12 -0
- package/dist/select.d.ts +1 -0
- package/dist/skip-link.d.ts +7 -0
- package/dist/skip-link.js +10 -0
- package/dist/slider.d.ts +11 -0
- package/dist/sortable-table-head.d.ts +2 -0
- package/dist/stat-tile.d.ts +5 -0
- package/dist/table-body-skeleton.d.ts +2 -0
- package/dist/table.d.ts +1 -0
- package/dist/tabs.d.ts +9 -5
- package/dist/tabs.js +1 -1
- package/dist/toast.d.ts +7 -1
- package/dist/toggle-group.d.ts +9 -2
- package/dist/toggle-group.js +2 -1
- package/dist/toggle.d.ts +1 -1
- package/package.json +85 -11
- package/skills/atlas-react-kit/SKILL.md +2 -0
- package/theme.css +11 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Column id → ordered card ids, derived from the consumer's `items`. */
|
|
2
|
+
export type KanbanGroups = Record<string, string[]>;
|
|
3
|
+
/** Drop zone ids for the drag-time action rail. */
|
|
4
|
+
export declare const KANBAN_COMPLETE_ID = "__kanban_complete__";
|
|
5
|
+
export declare const KANBAN_DELETE_ID = "__kanban_delete__";
|
|
6
|
+
export declare function buildKanbanGroups<T extends {
|
|
7
|
+
id: string;
|
|
8
|
+
}>(columns: {
|
|
9
|
+
id: string;
|
|
10
|
+
}[], items: T[], getColumnId: (item: T) => string): KanbanGroups;
|
|
11
|
+
/** What dnd-kit reports at drag end, reduced to plain ids. */
|
|
12
|
+
export type KanbanDropInput = {
|
|
13
|
+
itemId: string;
|
|
14
|
+
/** Droppable under the pointer: a column, a card, or a rail zone. */
|
|
15
|
+
targetId: string | null;
|
|
16
|
+
/** The target card's column, when the target is a card. */
|
|
17
|
+
targetGroup: string | null;
|
|
18
|
+
/** Column the card started in. */
|
|
19
|
+
initialGroup: string | null;
|
|
20
|
+
/** Column the card is over now. */
|
|
21
|
+
currentGroup: string | null;
|
|
22
|
+
/** Sortable index in the target column. */
|
|
23
|
+
index: number | undefined;
|
|
24
|
+
};
|
|
25
|
+
export type KanbanDrop = {
|
|
26
|
+
kind: "complete";
|
|
27
|
+
itemId: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "delete";
|
|
30
|
+
itemId: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: "move";
|
|
33
|
+
itemId: string;
|
|
34
|
+
toColumnId: string;
|
|
35
|
+
beforeId: string | null;
|
|
36
|
+
afterId: string | null;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Turn a drop into a complete, delete, or move with the neighbours at the
|
|
40
|
+
* insertion slot. `null` when the drop changes nothing.
|
|
41
|
+
*/
|
|
42
|
+
export declare function resolveKanbanDrop(input: KanbanDropInput, groups: KanbanGroups): KanbanDrop | null;
|
|
43
|
+
export type KanbanOverflowEdges = {
|
|
44
|
+
start: boolean;
|
|
45
|
+
end: boolean;
|
|
46
|
+
};
|
|
47
|
+
/** Whether a horizontal scroller has more content past either edge. */
|
|
48
|
+
export declare function kanbanOverflowEdges(scrollLeft: number, clientWidth: number, scrollWidth: number): KanbanOverflowEdges;
|
|
49
|
+
/** One "page" of columns: most of the viewport, at least one narrow column. */
|
|
50
|
+
export declare function kanbanScrollStepPx(clientWidth: number): number;
|
|
51
|
+
/** Smooth-scroll toward the start (`-1`) or end (`1`); no-op without a scroller. */
|
|
52
|
+
export declare function kanbanScrollBy(scroller: {
|
|
53
|
+
clientWidth: number;
|
|
54
|
+
scrollBy: (options: ScrollToOptions) => void;
|
|
55
|
+
} | null, direction: -1 | 1): void;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** Drop zone ids for the drag-time action rail. */
|
|
2
|
+
export const KANBAN_COMPLETE_ID = "__kanban_complete__";
|
|
3
|
+
export const KANBAN_DELETE_ID = "__kanban_delete__";
|
|
4
|
+
export function buildKanbanGroups(columns, items, getColumnId) {
|
|
5
|
+
const groups = {};
|
|
6
|
+
for (const column of columns)
|
|
7
|
+
groups[column.id] = [];
|
|
8
|
+
for (const item of items) {
|
|
9
|
+
const columnId = getColumnId(item);
|
|
10
|
+
const group = groups[columnId];
|
|
11
|
+
if (!group) {
|
|
12
|
+
console.warn(`[KanbanBoard] item "${item.id}" maps to unknown column "${columnId}" and is not rendered`);
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
group.push(item.id);
|
|
16
|
+
}
|
|
17
|
+
return groups;
|
|
18
|
+
}
|
|
19
|
+
function targetColumn(input, groups) {
|
|
20
|
+
const { targetId, targetGroup, currentGroup } = input;
|
|
21
|
+
if (targetId != null) {
|
|
22
|
+
if (targetId in groups)
|
|
23
|
+
return targetId;
|
|
24
|
+
if (targetGroup != null)
|
|
25
|
+
return targetGroup;
|
|
26
|
+
for (const [columnId, ids] of Object.entries(groups)) {
|
|
27
|
+
if (ids.includes(targetId))
|
|
28
|
+
return columnId;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return currentGroup;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Turn a drop into a complete, delete, or move with the neighbours at the
|
|
35
|
+
* insertion slot. `null` when the drop changes nothing.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveKanbanDrop(input, groups) {
|
|
38
|
+
const { itemId, targetId, initialGroup, index: rawIndex } = input;
|
|
39
|
+
if (targetId === KANBAN_COMPLETE_ID)
|
|
40
|
+
return { kind: "complete", itemId };
|
|
41
|
+
if (targetId === KANBAN_DELETE_ID)
|
|
42
|
+
return { kind: "delete", itemId };
|
|
43
|
+
const toColumnId = targetColumn(input, groups);
|
|
44
|
+
if (!initialGroup || !toColumnId)
|
|
45
|
+
return null;
|
|
46
|
+
const columnItems = groups[toColumnId] ?? [];
|
|
47
|
+
const remaining = columnItems.filter((id) => id !== itemId);
|
|
48
|
+
const index = Math.max(0, Math.min(rawIndex ?? remaining.length, remaining.length));
|
|
49
|
+
if (initialGroup === toColumnId && index === columnItems.indexOf(itemId)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
kind: "move",
|
|
54
|
+
itemId,
|
|
55
|
+
toColumnId,
|
|
56
|
+
beforeId: index > 0 ? (remaining[index - 1] ?? null) : null,
|
|
57
|
+
afterId: remaining[index] ?? null,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Pixel slop so sub-pixel scrollWidth vs clientWidth does not flicker cues. */
|
|
61
|
+
const OVERFLOW_EDGE_PX = 8;
|
|
62
|
+
/** Whether a horizontal scroller has more content past either edge. */
|
|
63
|
+
export function kanbanOverflowEdges(scrollLeft, clientWidth, scrollWidth) {
|
|
64
|
+
const maxScroll = Math.max(0, scrollWidth - clientWidth);
|
|
65
|
+
return {
|
|
66
|
+
start: scrollLeft > OVERFLOW_EDGE_PX,
|
|
67
|
+
end: scrollLeft < maxScroll - OVERFLOW_EDGE_PX,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** One "page" of columns: most of the viewport, at least one narrow column. */
|
|
71
|
+
export function kanbanScrollStepPx(clientWidth) {
|
|
72
|
+
return Math.max(140, Math.round(clientWidth * 0.75));
|
|
73
|
+
}
|
|
74
|
+
/** Smooth-scroll toward the start (`-1`) or end (`1`); no-op without a scroller. */
|
|
75
|
+
export function kanbanScrollBy(scroller, direction) {
|
|
76
|
+
scroller?.scrollBy({
|
|
77
|
+
left: direction * kanbanScrollStepPx(scroller.clientWidth),
|
|
78
|
+
behavior: "smooth",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ComponentProps } from "react";
|
|
2
|
+
import type { DragDropProvider } from "@dnd-kit/react";
|
|
3
|
+
type PluginsProp = NonNullable<ComponentProps<typeof DragDropProvider>["plugins"]>;
|
|
4
|
+
/**
|
|
5
|
+
* Board plugins. AutoScroller stays (edge scroll while dragging). Feedback
|
|
6
|
+
* stays because it positions and sizes `DragOverlay`; `feedback: "none"`
|
|
7
|
+
* would leave the overlay behind. The drop animation is off so it does not
|
|
8
|
+
* snap twice against an optimistic update.
|
|
9
|
+
*/
|
|
10
|
+
export declare const configureKanbanPlugins: Extract<PluginsProp, (...args: never[]) => unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Cards leave out OptimisticSortingPlugin: it moves DOM nodes under React and
|
|
13
|
+
* throws `removeChild` on re-render (dnd-kit #1747). Column layout comes from
|
|
14
|
+
* the consumer's `items`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function configureCardSortablePlugins<T>(defaults: readonly T[]): T[];
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Feedback } from "@dnd-kit/dom";
|
|
2
|
+
import { OptimisticSortingPlugin } from "@dnd-kit/dom/sortable";
|
|
3
|
+
/**
|
|
4
|
+
* Board plugins. AutoScroller stays (edge scroll while dragging). Feedback
|
|
5
|
+
* stays because it positions and sizes `DragOverlay`; `feedback: "none"`
|
|
6
|
+
* would leave the overlay behind. The drop animation is off so it does not
|
|
7
|
+
* snap twice against an optimistic update.
|
|
8
|
+
*/
|
|
9
|
+
export const configureKanbanPlugins = (defaults) => defaults.map((plugin) => plugin === Feedback ? Feedback.configure({ dropAnimation: null }) : plugin);
|
|
10
|
+
/**
|
|
11
|
+
* Cards leave out OptimisticSortingPlugin: it moves DOM nodes under React and
|
|
12
|
+
* throws `removeChild` on re-render (dnd-kit #1747). Column layout comes from
|
|
13
|
+
* the consumer's `items`.
|
|
14
|
+
*/
|
|
15
|
+
export function configureCardSortablePlugins(defaults) {
|
|
16
|
+
return defaults.filter((plugin) => plugin !== OptimisticSortingPlugin);
|
|
17
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/** Tailwind `md` breakpoint. Used for range calendars that stack on small screens. */
|
|
2
2
|
export declare const MD_MEDIA_QUERY = "(min-width: 768px)";
|
|
3
3
|
/**
|
|
4
|
-
* Subscribe to a CSS media query
|
|
5
|
-
*
|
|
4
|
+
* Subscribe to a CSS media query with `useSyncExternalStore`. The server
|
|
5
|
+
* render and hydration use `defaultMatches`, so the markup always agrees;
|
|
6
|
+
* React then re-renders with the real value. `defaultMatches` also covers
|
|
7
|
+
* environments without `matchMedia` (jsdom unless the test stubs it).
|
|
6
8
|
*/
|
|
7
9
|
export declare function useMediaQuery(query: string, defaultMatches?: boolean): boolean;
|
|
@@ -2,26 +2,22 @@
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
/** Tailwind `md` breakpoint. Used for range calendars that stack on small screens. */
|
|
4
4
|
export const MD_MEDIA_QUERY = "(min-width: 768px)";
|
|
5
|
+
function hasMatchMedia() {
|
|
6
|
+
return (typeof window !== "undefined" && typeof window.matchMedia === "function");
|
|
7
|
+
}
|
|
5
8
|
/**
|
|
6
|
-
* Subscribe to a CSS media query
|
|
7
|
-
*
|
|
9
|
+
* Subscribe to a CSS media query with `useSyncExternalStore`. The server
|
|
10
|
+
* render and hydration use `defaultMatches`, so the markup always agrees;
|
|
11
|
+
* React then re-renders with the real value. `defaultMatches` also covers
|
|
12
|
+
* environments without `matchMedia` (jsdom unless the test stubs it).
|
|
8
13
|
*/
|
|
9
14
|
export function useMediaQuery(query, defaultMatches = false) {
|
|
10
|
-
const
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
return defaultMatches;
|
|
14
|
-
}
|
|
15
|
-
return window.matchMedia(query).matches;
|
|
16
|
-
});
|
|
17
|
-
React.useEffect(() => {
|
|
18
|
-
if (typeof window.matchMedia !== "function")
|
|
19
|
-
return;
|
|
15
|
+
const subscribe = React.useCallback((onChange) => {
|
|
16
|
+
if (!hasMatchMedia())
|
|
17
|
+
return () => { };
|
|
20
18
|
const media = window.matchMedia(query);
|
|
21
|
-
const onChange = () => setMatches(media.matches);
|
|
22
|
-
onChange();
|
|
23
19
|
media.addEventListener("change", onChange);
|
|
24
20
|
return () => media.removeEventListener("change", onChange);
|
|
25
21
|
}, [query]);
|
|
26
|
-
return matches;
|
|
22
|
+
return React.useSyncExternalStore(subscribe, () => (hasMatchMedia() ? window.matchMedia(query).matches : defaultMatches), () => defaultMatches);
|
|
27
23
|
}
|
package/dist/kanban.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* One board column. Columns render in the order given. `color` (any CSS
|
|
4
|
+
* color) draws a status dot; `badge` is trailing header meta such as a total.
|
|
5
|
+
*/
|
|
6
|
+
export type KanbanColumn = {
|
|
7
|
+
/** Matches what `getColumnId` returns for the column's cards. */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Header text. */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Any CSS color; draws a status dot before the label. No dot when omitted. */
|
|
12
|
+
color?: string;
|
|
13
|
+
/** Trailing header meta under the card count, such as a total. */
|
|
14
|
+
badge?: React.ReactNode;
|
|
15
|
+
/** Replaces the board's `emptyLabel` for this column. */
|
|
16
|
+
emptyLabel?: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Positions of the cards on either side of the drop slot (`undefined` at a
|
|
20
|
+
* column edge). Feed them to a fractional-index helper to store the order.
|
|
21
|
+
*/
|
|
22
|
+
export type KanbanDropTarget = {
|
|
23
|
+
/** `position` of the card above the drop slot; `undefined` at the top. */
|
|
24
|
+
beforePosition: number | undefined;
|
|
25
|
+
/** `position` of the card below the drop slot; `undefined` at the bottom. */
|
|
26
|
+
afterPosition: number | undefined;
|
|
27
|
+
};
|
|
28
|
+
export type KanbanBoardProps<T extends {
|
|
29
|
+
id: string;
|
|
30
|
+
position: number;
|
|
31
|
+
}> = {
|
|
32
|
+
/** The board's columns, in display order. */
|
|
33
|
+
columns: KanbanColumn[];
|
|
34
|
+
/** Every card, in display order; sort by `position` first. A card whose column id is unknown is not shown. */
|
|
35
|
+
items: T[];
|
|
36
|
+
/** The id of the column a card belongs in. */
|
|
37
|
+
getColumnId: (item: T) => string;
|
|
38
|
+
/** Every drop that changes a column or the order, including same-column reorders. */
|
|
39
|
+
onMove: (item: T, toColumnId: string, target: KanbanDropTarget) => void;
|
|
40
|
+
/** Shows a Finish zone while dragging. */
|
|
41
|
+
onComplete?: (itemId: string) => void;
|
|
42
|
+
/** Shows a Remove zone while dragging. */
|
|
43
|
+
onDelete?: (itemId: string) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Card body; the board adds the grip, chrome, and sorting. Cards re-render
|
|
46
|
+
* only when their item changes while this and `onCardClick` keep their
|
|
47
|
+
* identity, so define them outside the render or wrap them in `useCallback`.
|
|
48
|
+
*/
|
|
49
|
+
renderCard: (item: T) => React.ReactNode;
|
|
50
|
+
/** Makes the card body a button. */
|
|
51
|
+
onCardClick?: (item: T) => void;
|
|
52
|
+
/** Skeleton cards on a first load with no items; dims the board on a refetch. */
|
|
53
|
+
isLoading?: boolean;
|
|
54
|
+
/** Text in a column with no cards. Default `"Drop here"`. */
|
|
55
|
+
emptyLabel?: string;
|
|
56
|
+
/** Text on the Finish zone. Default `"Finish"`. */
|
|
57
|
+
completeLabel?: string;
|
|
58
|
+
/** Text on the Remove zone. Default `"Remove"`. */
|
|
59
|
+
deleteLabel?: string;
|
|
60
|
+
/** Classes for the horizontal scroller that holds the columns. */
|
|
61
|
+
scrollerClassName?: string;
|
|
62
|
+
/** Classes for every column, e.g. its width. */
|
|
63
|
+
columnClassName?: string;
|
|
64
|
+
/** Classes for every card, including the one under the pointer while dragging. */
|
|
65
|
+
cardClassName?: string;
|
|
66
|
+
} & Omit<React.ComponentProps<"div">, "children">;
|
|
67
|
+
/**
|
|
68
|
+
* Drag-and-drop board built on dnd-kit. The consumer owns `items` and maps
|
|
69
|
+
* each to a column; the board derives its layout from them, so an optimistic
|
|
70
|
+
* cache update is the only state. Cards drag by their grip, the body stays
|
|
71
|
+
* clickable, and off-screen columns get edge fades and scroll buttons.
|
|
72
|
+
*/
|
|
73
|
+
declare function KanbanBoard<T extends {
|
|
74
|
+
id: string;
|
|
75
|
+
position: number;
|
|
76
|
+
}>({ columns, items, getColumnId, onMove, onComplete, onDelete, renderCard, onCardClick, isLoading, emptyLabel, completeLabel, deleteLabel, className, scrollerClassName, columnClassName, cardClassName, ...props }: KanbanBoardProps<T>): React.JSX.Element;
|
|
77
|
+
/** Board-shaped placeholder for a route `loading` file. */
|
|
78
|
+
declare function KanbanSkeleton({ columnCount, className, columnClassName, ...props }: {
|
|
79
|
+
/** How many placeholder columns to draw. Default 4. */
|
|
80
|
+
columnCount?: number;
|
|
81
|
+
/** Classes for every placeholder column, e.g. to match the board's width. */
|
|
82
|
+
columnClassName?: string;
|
|
83
|
+
} & Omit<React.ComponentProps<"div">, "children">): React.JSX.Element;
|
|
84
|
+
export { KanbanBoard, KanbanSkeleton };
|
package/dist/kanban.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { DragDropProvider, DragOverlay, useDragOperation, useDroppable, } from "@dnd-kit/react";
|
|
5
|
+
import { isSortable, useSortable } from "@dnd-kit/react/sortable";
|
|
6
|
+
import { CheckCircle2, ChevronLeft, ChevronRight, GripVertical, Trash2, } from "lucide-react";
|
|
7
|
+
import { Button } from "./button.js";
|
|
8
|
+
import { KANBAN_COMPLETE_ID, KANBAN_DELETE_ID, buildKanbanGroups, kanbanOverflowEdges, kanbanScrollBy, resolveKanbanDrop, } from "./internal/kanban-layout.js";
|
|
9
|
+
import { configureCardSortablePlugins, configureKanbanPlugins, } from "./internal/kanban-plugins.js";
|
|
10
|
+
import { Skeleton } from "./skeleton.js";
|
|
11
|
+
import { cn } from "./utils.js";
|
|
12
|
+
/** Column droppables lose to cards, so a drop lands between cards. */
|
|
13
|
+
const COLUMN_DROP_PRIORITY = 1;
|
|
14
|
+
/** Rail zones beat columns when both are under the pointer. */
|
|
15
|
+
const ACTION_ZONE_PRIORITY = 10;
|
|
16
|
+
const columnSize = "w-72 min-w-72 min-h-[min(70vh,32rem)]";
|
|
17
|
+
/**
|
|
18
|
+
* Drag-and-drop board built on dnd-kit. The consumer owns `items` and maps
|
|
19
|
+
* each to a column; the board derives its layout from them, so an optimistic
|
|
20
|
+
* cache update is the only state. Cards drag by their grip, the body stays
|
|
21
|
+
* clickable, and off-screen columns get edge fades and scroll buttons.
|
|
22
|
+
*/
|
|
23
|
+
function KanbanBoard({ columns, items, getColumnId, onMove, onComplete, onDelete, renderCard, onCardClick, isLoading = false, emptyLabel = "Drop here", completeLabel = "Finish", deleteLabel = "Remove", className, scrollerClassName, columnClassName, cardClassName, ...props }) {
|
|
24
|
+
const scrollerRef = React.useRef(null);
|
|
25
|
+
const overflow = useOverflowEdges(scrollerRef, columns.length, items.length);
|
|
26
|
+
const itemById = React.useMemo(() => new Map(items.map((item) => [item.id, item])), [items]);
|
|
27
|
+
const groups = React.useMemo(() => buildKanbanGroups(columns, items, getColumnId), [columns, items, getColumnId]);
|
|
28
|
+
const showSkeletons = isLoading && items.length === 0;
|
|
29
|
+
const dimmed = isLoading && !showSkeletons;
|
|
30
|
+
const handleDragOver = React.useCallback((event) => {
|
|
31
|
+
// Layout comes from `items`; stop dnd-kit moving cards between groups.
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
}, []);
|
|
34
|
+
const handleDragEnd = React.useCallback((event) => {
|
|
35
|
+
if (event.canceled)
|
|
36
|
+
return;
|
|
37
|
+
const { source, target } = event.operation;
|
|
38
|
+
if (!source || !isSortable(source))
|
|
39
|
+
return;
|
|
40
|
+
const drop = resolveKanbanDrop({
|
|
41
|
+
itemId: String(source.id),
|
|
42
|
+
targetId: target ? String(target.id) : null,
|
|
43
|
+
targetGroup: target && isSortable(target) && target.group != null
|
|
44
|
+
? String(target.group)
|
|
45
|
+
: null,
|
|
46
|
+
initialGroup: source.initialGroup != null ? String(source.initialGroup) : null,
|
|
47
|
+
currentGroup: source.group != null ? String(source.group) : null,
|
|
48
|
+
index: typeof source.index === "number" ? source.index : undefined,
|
|
49
|
+
}, groups);
|
|
50
|
+
if (!drop)
|
|
51
|
+
return;
|
|
52
|
+
if (drop.kind === "complete")
|
|
53
|
+
return onComplete?.(drop.itemId);
|
|
54
|
+
if (drop.kind === "delete")
|
|
55
|
+
return onDelete?.(drop.itemId);
|
|
56
|
+
const item = itemById.get(drop.itemId);
|
|
57
|
+
if (!item)
|
|
58
|
+
return;
|
|
59
|
+
onMove(item, drop.toColumnId, {
|
|
60
|
+
beforePosition: drop.beforeId
|
|
61
|
+
? itemById.get(drop.beforeId)?.position
|
|
62
|
+
: undefined,
|
|
63
|
+
afterPosition: drop.afterId
|
|
64
|
+
? itemById.get(drop.afterId)?.position
|
|
65
|
+
: undefined,
|
|
66
|
+
});
|
|
67
|
+
}, [groups, itemById, onComplete, onDelete, onMove]);
|
|
68
|
+
const scrollToStart = React.useCallback(() => kanbanScrollBy(scrollerRef.current, -1), []);
|
|
69
|
+
const scrollToEnd = React.useCallback(() => kanbanScrollBy(scrollerRef.current, 1), []);
|
|
70
|
+
const renderOverlay = React.useCallback((source) => {
|
|
71
|
+
const item = source ? itemById.get(String(source.id)) : undefined;
|
|
72
|
+
if (!item)
|
|
73
|
+
return null;
|
|
74
|
+
return (_jsxs("div", { "data-slot": "kanban-drag-overlay", className: cn(cardChrome, "shadow-lg ring-2 ring-ring/30", cardClassName), children: [_jsx("div", { className: cn(handleChrome, "cursor-grabbing"), children: _jsx(GripVertical, { className: "size-4", "aria-hidden": true }) }), _jsx("div", { className: "min-w-0 flex-1 p-3", children: renderCard(item) })] }));
|
|
75
|
+
}, [cardClassName, itemById, renderCard]);
|
|
76
|
+
// `min-w-0` lets a flex or grid parent shrink the board so the scroller,
|
|
77
|
+
// not the page, overflows.
|
|
78
|
+
return (_jsx("div", { "data-slot": "kanban-board", className: cn("min-w-0", className), ...props, children: _jsxs(DragDropProvider, { plugins: configureKanbanPlugins, onDragOver: handleDragOver, onDragEnd: handleDragEnd, children: [_jsxs("div", { className: "relative min-w-0", children: [_jsx("div", { ref: scrollerRef, "data-slot": "kanban-scroller", "data-loading": dimmed || undefined, "data-overflow-start": overflow.start || undefined, "data-overflow-end": overflow.end || undefined, onScroll: overflow.measure, className: cn("flex min-w-0 gap-4 overflow-x-auto pb-2",
|
|
79
|
+
// AutoScroller scrolls in small steps near the edge; mandatory
|
|
80
|
+
// snap would pull each one back, so it is off mid-drag.
|
|
81
|
+
"snap-x snap-mandatory scroll-px-2 has-data-dragging:snap-none", "data-loading:opacity-60", scrollerClassName), children: columns.map((column) => {
|
|
82
|
+
const ids = groups[column.id] ?? [];
|
|
83
|
+
return (_jsx(KanbanColumnView, { column: column, count: showSkeletons ? 0 : ids.length, emptyLabel: column.emptyLabel ?? emptyLabel, loading: showSkeletons, className: columnClassName, children: ids.map((id, index) => {
|
|
84
|
+
const item = itemById.get(id);
|
|
85
|
+
if (!item)
|
|
86
|
+
return null;
|
|
87
|
+
return (_jsx(KanbanCardView, { item: item, index: index, column: column.id, onCardClick: onCardClick, className: cardClassName, renderCard: renderCard }, id));
|
|
88
|
+
}) }, column.id));
|
|
89
|
+
}) }), _jsx(OverflowFade, { side: "start", visible: overflow.start }), _jsx(OverflowFade, { side: "end", visible: overflow.end }), overflow.start ? (_jsx(ScrollButton, { side: "start", onClick: scrollToStart })) : null, overflow.end ? (_jsx(ScrollButton, { side: "end", onClick: scrollToEnd })) : null, overflow.start || overflow.end ? (_jsx("p", { className: "sr-only", children: "More columns available. Scroll horizontally to see them." })) : null] }), onComplete || onDelete ? (_jsx(ActionRail, { completeLabel: onComplete ? completeLabel : null, deleteLabel: onDelete ? deleteLabel : null })) : null, _jsx(DragOverlay, { dropAnimation: null, children: renderOverlay })] }) }));
|
|
90
|
+
}
|
|
91
|
+
function useOverflowEdges(ref, columnCount, itemCount) {
|
|
92
|
+
const [edges, setEdges] = React.useState({
|
|
93
|
+
start: false,
|
|
94
|
+
end: false,
|
|
95
|
+
});
|
|
96
|
+
const measure = React.useCallback(() => {
|
|
97
|
+
const element = ref.current;
|
|
98
|
+
if (!element)
|
|
99
|
+
return;
|
|
100
|
+
const next = kanbanOverflowEdges(element.scrollLeft, element.clientWidth, element.scrollWidth);
|
|
101
|
+
// Scroll fires every frame; keep the old object so the board (and every
|
|
102
|
+
// card in it) re-renders only when an edge appears or goes.
|
|
103
|
+
setEdges((edges) => edges.start === next.start && edges.end === next.end ? edges : next);
|
|
104
|
+
}, [ref]);
|
|
105
|
+
React.useLayoutEffect(() => {
|
|
106
|
+
const element = ref.current;
|
|
107
|
+
if (!element || typeof ResizeObserver === "undefined")
|
|
108
|
+
return;
|
|
109
|
+
measure();
|
|
110
|
+
const observer = new ResizeObserver(measure);
|
|
111
|
+
observer.observe(element);
|
|
112
|
+
for (const child of element.children)
|
|
113
|
+
observer.observe(child);
|
|
114
|
+
return () => observer.disconnect();
|
|
115
|
+
}, [columnCount, itemCount, measure, ref]);
|
|
116
|
+
return { ...edges, measure };
|
|
117
|
+
}
|
|
118
|
+
function KanbanColumnView({ column, count, emptyLabel, loading, className, children, }) {
|
|
119
|
+
const { ref, isDropTarget } = useDroppable({
|
|
120
|
+
id: column.id,
|
|
121
|
+
type: "column",
|
|
122
|
+
accept: "item",
|
|
123
|
+
collisionPriority: COLUMN_DROP_PRIORITY,
|
|
124
|
+
});
|
|
125
|
+
const empty = count === 0;
|
|
126
|
+
return (_jsxs("div", { ref: ref, "data-slot": "kanban-column", "data-drop-target": isDropTarget || undefined, className: cn("flex shrink-0 snap-start flex-col gap-3 p-3", "bg-muted/40 ring-1 ring-foreground/5", "data-drop-target:bg-muted/60 data-drop-target:ring-2 data-drop-target:ring-ring/60", columnSize, className), children: [_jsxs("header", { "data-slot": "kanban-column-header", className: "flex items-start justify-between gap-2", children: [_jsxs("span", { className: "flex items-center gap-2 text-xs font-semibold tracking-widest uppercase", children: [column.color ? (_jsx("span", { "aria-hidden": true, className: "size-2 shrink-0 rounded-full", style: { backgroundColor: column.color } })) : null, _jsx("span", { className: "leading-tight", children: column.label })] }), _jsxs("span", { className: "flex shrink-0 flex-col items-end gap-0.5 text-muted-foreground tabular-nums", children: [_jsx("span", { className: "text-xs", children: count }), column.badge ? (_jsx("span", { className: "text-[10px] font-medium", children: column.badge })) : null] })] }), _jsx("div", { className: "flex min-h-32 flex-1 flex-col gap-2 overflow-y-auto pr-1", children: loading ? (_jsxs(_Fragment, { children: [_jsx(Skeleton, { className: "h-20" }), _jsx(Skeleton, { className: "h-16" })] })) : (_jsxs(_Fragment, { children: [children, _jsx("p", { "data-slot": "kanban-column-empty", "aria-hidden": !empty, className: cn("border border-dashed px-3 py-6 text-center text-xs text-muted-foreground", !empty && "hidden"), children: emptyLabel })] })) })] }));
|
|
127
|
+
}
|
|
128
|
+
const cardChrome = cn("flex border border-border bg-card text-card-foreground");
|
|
129
|
+
const handleChrome = cn("flex shrink-0 items-center self-stretch border-r border-border", "bg-muted/30 px-1.5 text-muted-foreground");
|
|
130
|
+
function stopClick(event) {
|
|
131
|
+
event.stopPropagation();
|
|
132
|
+
}
|
|
133
|
+
function KanbanCardViewImpl({ item, index, column, onCardClick, className, renderCard, }) {
|
|
134
|
+
const children = renderCard(item);
|
|
135
|
+
const { handleRef, ref, isDragging } = useSortable({
|
|
136
|
+
id: item.id,
|
|
137
|
+
index,
|
|
138
|
+
type: "item",
|
|
139
|
+
accept: "item",
|
|
140
|
+
group: column,
|
|
141
|
+
transition: null,
|
|
142
|
+
plugins: configureCardSortablePlugins,
|
|
143
|
+
});
|
|
144
|
+
return (_jsxs("div", { ref: ref, "data-slot": "kanban-card", "data-dragging": isDragging || undefined, className: cn(cardChrome, "shadow-sm transition-[box-shadow,opacity,border-color]",
|
|
145
|
+
// The overlay moves; the source keeps its slot.
|
|
146
|
+
"data-dragging:pointer-events-none data-dragging:invisible",
|
|
147
|
+
// One ring on the card for the grip and the body alike.
|
|
148
|
+
"has-focus-visible:ring-2 has-focus-visible:ring-ring has-focus-visible:ring-offset-2 has-focus-visible:ring-offset-background", onCardClick && "hover:border-foreground/15", className), children: [_jsx("button", { type: "button", ref: handleRef, "data-slot": "kanban-card-handle", "aria-label": "Drag to move", onClick: stopClick, className: cn(handleChrome, "cursor-grab outline-none hover:bg-muted/50 active:cursor-grabbing"), children: _jsx(GripVertical, { className: "size-4", "aria-hidden": true }) }), onCardClick ? (_jsx("button", { type: "button", "data-slot": "kanban-card-body", onClick: () => onCardClick(item), className: "min-w-0 flex-1 cursor-pointer p-3 text-left outline-none hover:bg-muted/20", children: children })) : (_jsx("div", { "data-slot": "kanban-card-body", className: "min-w-0 flex-1 p-3", children: children }))] }));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Memoized so a board update re-renders only the cards whose item, index or
|
|
152
|
+
* column changed, when `renderCard` and the other props keep their identity.
|
|
153
|
+
*/
|
|
154
|
+
const KanbanCardView = React.memo(KanbanCardViewImpl);
|
|
155
|
+
/**
|
|
156
|
+
* Finish / Remove zones under the board while a card drags. Not sticky: a
|
|
157
|
+
* sticky rail overlapping tall columns stole their drops.
|
|
158
|
+
*/
|
|
159
|
+
function ActionRail({ completeLabel, deleteLabel, }) {
|
|
160
|
+
const dragging = useDragOperation().source != null;
|
|
161
|
+
return (_jsxs("div", { "data-slot": "kanban-action-rail", "aria-hidden": !dragging, className: cn("relative z-20 -mx-1 mt-3 flex flex-col gap-2 bg-background px-1 pb-1 sm:flex-row", !dragging && "pointer-events-none"), children: [completeLabel != null ? (_jsx(ActionZone, { id: KANBAN_COMPLETE_ID, label: completeLabel, regionLabel: "Drop here to finish", icon: CheckCircle2, tone: "success", visible: dragging })) : null, deleteLabel != null ? (_jsx(ActionZone, { id: KANBAN_DELETE_ID, label: deleteLabel, regionLabel: "Drop here to remove", icon: Trash2, tone: "destructive", visible: dragging })) : null] }));
|
|
162
|
+
}
|
|
163
|
+
const zoneTone = {
|
|
164
|
+
success: {
|
|
165
|
+
idle: "border-success/60 bg-success/10 text-success",
|
|
166
|
+
active: "border-success bg-success/25 text-success ring-2 ring-success/40",
|
|
167
|
+
},
|
|
168
|
+
destructive: {
|
|
169
|
+
idle: "border-destructive/60 bg-destructive/10 text-destructive",
|
|
170
|
+
active: "border-destructive bg-destructive/25 text-destructive ring-2 ring-destructive/40",
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
function ActionZone({ id, label, regionLabel, icon: Icon, tone, visible, }) {
|
|
174
|
+
const { ref, isDropTarget } = useDroppable({
|
|
175
|
+
id,
|
|
176
|
+
type: "column",
|
|
177
|
+
accept: "item",
|
|
178
|
+
collisionPriority: ACTION_ZONE_PRIORITY,
|
|
179
|
+
disabled: !visible,
|
|
180
|
+
});
|
|
181
|
+
// A ring, not a scale, marks the target: scale clips in overflow parents.
|
|
182
|
+
return (_jsxs("div", { ref: ref, role: "region", "aria-label": regionLabel, "aria-hidden": !visible, "data-slot": "kanban-action-zone", "data-drop-target": isDropTarget || undefined, className: cn("relative z-10 flex flex-1 items-center justify-center gap-2 border-2 border-dashed p-3", "text-sm font-medium transition-[box-shadow,border-color,background-color,color] duration-150", visible ? "opacity-100" : "pointer-events-none opacity-0", isDropTarget ? zoneTone[tone].active : zoneTone[tone].idle), children: [_jsx(Icon, { className: "size-4 shrink-0", "aria-hidden": true }), label] }));
|
|
183
|
+
}
|
|
184
|
+
function ScrollButton({ side, onClick, }) {
|
|
185
|
+
const Icon = side === "start" ? ChevronLeft : ChevronRight;
|
|
186
|
+
return (_jsx(Button, { type: "button", variant: "outline", size: "icon-sm", "data-slot": "kanban-scroll-button", "aria-label": side === "start" ? "Show previous columns" : "Show next columns", onClick: onClick, className: cn("absolute top-1/2 z-20 -translate-y-1/2", side === "start" ? "left-1" : "right-1"), children: _jsx(Icon, {}) }));
|
|
187
|
+
}
|
|
188
|
+
function OverflowFade({ side, visible, }) {
|
|
189
|
+
return (_jsx("div", { "aria-hidden": true, "data-slot": "kanban-overflow-fade", "data-side": side, className: cn("pointer-events-none absolute inset-y-0 z-10 w-10 to-transparent transition-opacity", side === "start"
|
|
190
|
+
? "left-0 bg-gradient-to-r from-background"
|
|
191
|
+
: "right-0 bg-gradient-to-l from-background", visible ? "opacity-100" : "opacity-0") }));
|
|
192
|
+
}
|
|
193
|
+
/** Board-shaped placeholder for a route `loading` file. */
|
|
194
|
+
function KanbanSkeleton({ columnCount = 4, className, columnClassName, ...props }) {
|
|
195
|
+
return (_jsx("div", { "data-slot": "kanban-skeleton", className: cn("flex gap-4 overflow-hidden", className), ...props, children: Array.from({ length: columnCount }, (_, index) => (_jsx(Skeleton, { className: cn("shrink-0", columnSize, columnClassName) }, index))) }));
|
|
196
|
+
}
|
|
197
|
+
export { KanbanBoard, KanbanSkeleton };
|
package/dist/markdown.d.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function safeMarkdownUrl(url: string): string;
|
|
6
6
|
export type MarkdownProps = {
|
|
7
|
+
/** Markdown source to render. Raw HTML is dropped and unsafe URLs are removed. */
|
|
7
8
|
content: string;
|
|
9
|
+
/** Classes for the `article` wrapper. */
|
|
8
10
|
className?: string;
|
|
9
11
|
};
|
|
10
12
|
/**
|
package/dist/meter.d.ts
CHANGED
|
@@ -2,16 +2,23 @@ import type * as React from "react";
|
|
|
2
2
|
export type MeterMarker = {
|
|
3
3
|
/** Same scale as `value` / `min` / `max` (not a 0–1 fraction). */
|
|
4
4
|
position: number;
|
|
5
|
+
/** Classes for the tick, e.g. its color. */
|
|
5
6
|
className?: string;
|
|
6
7
|
};
|
|
7
8
|
export type MeterProps = Omit<React.ComponentProps<"div">, "children" | "role" | "aria-label"> & {
|
|
9
|
+
/** Current reading. Clamped to `min`–`max`; a non-finite value shows as empty. */
|
|
8
10
|
value: number;
|
|
11
|
+
/** Start of the scale. Default `0`. */
|
|
9
12
|
min?: number;
|
|
13
|
+
/** End of the scale. */
|
|
10
14
|
max: number;
|
|
11
15
|
/** App-owned fill (status, pace). Defaults to primary. */
|
|
12
16
|
fillClassName?: string;
|
|
17
|
+
/** Thin ticks drawn over the track, e.g. a cap or a typical value. */
|
|
13
18
|
markers?: MeterMarker[];
|
|
19
|
+
/** Required accessible name; the meter has no visible label. */
|
|
14
20
|
"aria-label": string;
|
|
21
|
+
/** Classes for the track. */
|
|
15
22
|
className?: string;
|
|
16
23
|
};
|
|
17
24
|
/**
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { AsChildProps } from "./internal/as-child.js";
|
|
3
|
+
/**
|
|
4
|
+
* Pill chrome for app-shell navigation. Inactive is muted text with an
|
|
5
|
+
* accent hover; active uses the shared selected wash, text, and border
|
|
6
|
+
* (`selectedState.active`) so the current page meets non-text contrast.
|
|
7
|
+
* Apps pass `active` — the kit has no router.
|
|
8
|
+
*/
|
|
9
|
+
declare const navLinkVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Pill link for app-shell destinations (Calendar, Tasks, Settings). Pass
|
|
12
|
+
* `active` from the route; the kit does not read the URL. `asChild` wraps a
|
|
13
|
+
* router `Link` with these styles, the same Slot pattern as `BreadcrumbLink`.
|
|
14
|
+
*
|
|
15
|
+
* Not a tab: in-page view switches that belong in the URL use `TabsNav`
|
|
16
|
+
* and `tabsTriggerVariants()`. Next apps can import the optional adapter
|
|
17
|
+
* from `atlas-react-kit/next/nav-link` instead of composing `asChild`.
|
|
18
|
+
*/
|
|
19
|
+
declare function NavLink({ asChild, active, className, ...props }: React.ComponentProps<"a"> & AsChildProps & {
|
|
20
|
+
/** This link is the current page: `aria-current="page"` and the selected chrome. */
|
|
21
|
+
active: boolean;
|
|
22
|
+
}): React.JSX.Element;
|
|
23
|
+
export { NavLink, navLinkVariants };
|
package/dist/nav-link.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cva } from "class-variance-authority";
|
|
4
|
+
import { Slot } from "radix-ui";
|
|
5
|
+
import { focusRing, selectedState } from "./internal/styles.js";
|
|
6
|
+
import { cn } from "./utils.js";
|
|
7
|
+
/**
|
|
8
|
+
* Pill chrome for app-shell navigation. Inactive is muted text with an
|
|
9
|
+
* accent hover; active uses the shared selected wash, text, and border
|
|
10
|
+
* (`selectedState.active`) so the current page meets non-text contrast.
|
|
11
|
+
* Apps pass `active` — the kit has no router.
|
|
12
|
+
*/
|
|
13
|
+
const navLinkVariants = cva([
|
|
14
|
+
focusRing,
|
|
15
|
+
selectedState.active,
|
|
16
|
+
"inline-flex items-center gap-1.5 rounded-none border border-transparent px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent data-[state=active]:font-medium",
|
|
17
|
+
]);
|
|
18
|
+
/**
|
|
19
|
+
* Pill link for app-shell destinations (Calendar, Tasks, Settings). Pass
|
|
20
|
+
* `active` from the route; the kit does not read the URL. `asChild` wraps a
|
|
21
|
+
* router `Link` with these styles, the same Slot pattern as `BreadcrumbLink`.
|
|
22
|
+
*
|
|
23
|
+
* Not a tab: in-page view switches that belong in the URL use `TabsNav`
|
|
24
|
+
* and `tabsTriggerVariants()`. Next apps can import the optional adapter
|
|
25
|
+
* from `atlas-react-kit/next/nav-link` instead of composing `asChild`.
|
|
26
|
+
*/
|
|
27
|
+
function NavLink({ asChild, active, className, ...props }) {
|
|
28
|
+
const Comp = asChild ? Slot.Root : "a";
|
|
29
|
+
return (_jsx(Comp, { "data-slot": "nav-link", "data-state": active ? "active" : "inactive", "aria-current": active ? "page" : undefined, className: cn(navLinkVariants(), className), ...props }));
|
|
30
|
+
}
|
|
31
|
+
export { NavLink, navLinkVariants };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import Link from "next/link.js";
|
|
3
|
+
/**
|
|
4
|
+
* Next.js adapter for the shell `NavLink` pill. Same contract as
|
|
5
|
+
* `TabsNavLink`: Next `Link` props plus a required `active` boolean. The
|
|
6
|
+
* kit still does not read the router; the app passes `active`.
|
|
7
|
+
*/
|
|
8
|
+
declare function NavLink({ active, className, ...props }: React.ComponentProps<typeof Link> & {
|
|
9
|
+
active: boolean;
|
|
10
|
+
}): React.JSX.Element;
|
|
11
|
+
export { NavLink };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import Link from "next/link.js";
|
|
4
|
+
import { navLinkVariants } from "../nav-link.js";
|
|
5
|
+
import { cn } from "../utils.js";
|
|
6
|
+
/**
|
|
7
|
+
* Next.js adapter for the shell `NavLink` pill. Same contract as
|
|
8
|
+
* `TabsNavLink`: Next `Link` props plus a required `active` boolean. The
|
|
9
|
+
* kit still does not read the router; the app passes `active`.
|
|
10
|
+
*/
|
|
11
|
+
function NavLink({ active, className, ...props }) {
|
|
12
|
+
return (_jsx(Link, { "data-slot": "nav-link", "data-state": active ? "active" : "inactive", "aria-current": active ? "page" : undefined, className: cn(navLinkVariants(), className), ...props }));
|
|
13
|
+
}
|
|
14
|
+
export { NavLink };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type PeriodStepperFrameProps } from "../period-stepper.js";
|
|
2
|
+
/**
|
|
3
|
+
* Next.js `PeriodStepper`: each step is a prefetched link, so the period
|
|
4
|
+
* lives in the URL. Leave out `todayHref` to drop the Today link.
|
|
5
|
+
*/
|
|
6
|
+
declare function PeriodStepper({ previousHref, todayHref, nextHref, ...props }: PeriodStepperFrameProps & {
|
|
7
|
+
previousHref: string;
|
|
8
|
+
todayHref?: string;
|
|
9
|
+
nextHref: string;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
export { PeriodStepper };
|