@neasg/design-system 0.3.0 → 0.4.3
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/README.md +37 -1
- package/dist/alert.d.ts +15 -0
- package/dist/alert.js +24 -0
- package/dist/avatar.d.ts +13 -4
- package/dist/avatar.js +24 -7
- package/dist/back-button.d.ts +6 -0
- package/dist/back-button.js +8 -0
- package/dist/badge.d.ts +1 -1
- package/dist/breadcrumb.js +2 -2
- package/dist/button.d.ts +2 -2
- package/dist/button.js +3 -4
- package/dist/card.d.ts +3 -0
- package/dist/card.js +9 -2
- package/dist/checkbox.d.ts +13 -0
- package/dist/checkbox.js +29 -0
- package/dist/collapsible.d.ts +19 -0
- package/dist/collapsible.js +20 -0
- package/dist/command-search.js +2 -2
- package/dist/command.d.ts +1 -1
- package/dist/copy-button.d.ts +19 -0
- package/dist/copy-button.js +31 -0
- package/dist/date-input.js +2 -2
- package/dist/dialog-primitive.js +1 -1
- package/dist/draggable-tabs.js +1 -1
- package/dist/drawer.js +1 -1
- package/dist/editable-table.js +2 -2
- package/dist/empty-state.js +1 -1
- package/dist/field.d.ts +4 -1
- package/dist/field.js +3 -3
- package/dist/file-upload.d.ts +12 -0
- package/dist/file-upload.js +106 -0
- package/dist/index.d.ts +47 -6
- package/dist/index.js +25 -4
- package/dist/input-control.d.ts +1 -1
- package/dist/input-control.js +1 -1
- package/dist/input.js +2 -2
- package/dist/label-value-grid.d.ts +3 -1
- package/dist/label-value-grid.js +15 -2
- package/dist/label-value.d.ts +4 -1
- package/dist/label-value.js +4 -3
- package/dist/layout.d.ts +7 -1
- package/dist/layout.js +3 -3
- package/dist/lib/date-utils.d.ts +9 -0
- package/dist/lib/date-utils.js +34 -0
- package/dist/link.d.ts +9 -0
- package/dist/link.js +19 -0
- package/dist/multi-select.d.ts +3 -1
- package/dist/multi-select.js +12 -11
- package/dist/page-section.d.ts +14 -0
- package/dist/page-section.js +13 -0
- package/dist/pagination.js +1 -1
- package/dist/popover-menu.d.ts +35 -0
- package/dist/popover-menu.js +36 -0
- package/dist/popover-primitive.d.ts +7 -0
- package/dist/popover-primitive.js +11 -0
- package/dist/popover.d.ts +10 -5
- package/dist/popover.js +6 -9
- package/dist/progress.d.ts +10 -0
- package/dist/progress.js +17 -0
- package/dist/search-input.js +1 -1
- package/dist/section-nav.d.ts +22 -0
- package/dist/section-nav.js +25 -0
- package/dist/select-primitive.js +2 -2
- package/dist/select.js +2 -2
- package/dist/separator.d.ts +6 -0
- package/dist/separator.js +6 -0
- package/dist/skeleton.d.ts +4 -0
- package/dist/skeleton.js +6 -0
- package/dist/spinner.d.ts +22 -0
- package/dist/spinner.js +24 -0
- package/dist/styles.css +42 -0
- package/dist/switch.d.ts +12 -0
- package/dist/switch.js +16 -0
- package/dist/table.d.ts +7 -1
- package/dist/table.js +26 -12
- package/dist/tabs.d.ts +24 -1
- package/dist/tabs.js +61 -3
- package/dist/tailwind-preset.js +9 -0
- package/dist/textarea.js +1 -1
- package/dist/toaster.js +19 -10
- package/dist/typography.d.ts +1 -1
- package/dist/use-error-shake.d.ts +7 -0
- package/dist/use-error-shake.js +36 -0
- package/dist/use-file-upload.d.ts +34 -0
- package/dist/use-file-upload.js +133 -0
- package/dist/use-sticky-sentinel.d.ts +16 -0
- package/dist/use-sticky-sentinel.js +62 -0
- package/dist/use-table-sort.d.ts +28 -0
- package/dist/use-table-sort.js +53 -0
- package/dist/use-viewport-threshold.d.ts +12 -0
- package/dist/use-viewport-threshold.js +43 -0
- package/package.json +32 -6
- package/dist/tab-with-dropdown.d.ts +0 -18
- package/dist/tab-with-dropdown.js +0 -70
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export function useTableSort({ defaultField = null, defaultDirection = "asc", cycle = "asc-desc", onChange, } = {}) {
|
|
3
|
+
const [field, setField] = React.useState(defaultField);
|
|
4
|
+
const [direction, setDirection] = React.useState(defaultDirection);
|
|
5
|
+
const emit = React.useCallback((nextField, nextDirection) => {
|
|
6
|
+
setField(nextField);
|
|
7
|
+
setDirection(nextDirection);
|
|
8
|
+
onChange === null || onChange === void 0 ? void 0 : onChange(nextField, nextDirection);
|
|
9
|
+
}, [onChange]);
|
|
10
|
+
const onSort = React.useCallback((nextField) => {
|
|
11
|
+
const typed = nextField;
|
|
12
|
+
if (field !== typed) {
|
|
13
|
+
emit(typed, "asc");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (direction === "asc") {
|
|
17
|
+
emit(typed, "desc");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (cycle === "asc-desc-none") {
|
|
21
|
+
emit(null, "asc");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
emit(typed, "asc");
|
|
25
|
+
}, [field, direction, cycle, emit]);
|
|
26
|
+
const setSort = React.useCallback((nextField, nextDirection = "asc") => {
|
|
27
|
+
emit(nextField, nextDirection);
|
|
28
|
+
}, [emit]);
|
|
29
|
+
const reset = React.useCallback(() => {
|
|
30
|
+
emit(defaultField, defaultDirection);
|
|
31
|
+
}, [emit, defaultField, defaultDirection]);
|
|
32
|
+
const sortRows = React.useCallback((rows, accessor) => {
|
|
33
|
+
if (!field)
|
|
34
|
+
return rows;
|
|
35
|
+
const multiplier = direction === "asc" ? 1 : -1;
|
|
36
|
+
return [...rows].sort((a, b) => {
|
|
37
|
+
const av = accessor(a, field);
|
|
38
|
+
const bv = accessor(b, field);
|
|
39
|
+
if (av == null && bv == null)
|
|
40
|
+
return 0;
|
|
41
|
+
if (av == null)
|
|
42
|
+
return 1;
|
|
43
|
+
if (bv == null)
|
|
44
|
+
return -1;
|
|
45
|
+
if (typeof av === "number" && typeof bv === "number") {
|
|
46
|
+
return (av - bv) * multiplier;
|
|
47
|
+
}
|
|
48
|
+
return String(av).localeCompare(String(bv)) * multiplier;
|
|
49
|
+
});
|
|
50
|
+
}, [field, direction]);
|
|
51
|
+
const sorting = React.useMemo(() => ({ field, direction, onSort }), [field, direction, onSort]);
|
|
52
|
+
return { field, direction, sorting, setSort, reset, sortRows };
|
|
53
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type RefObject } from "react";
|
|
2
|
+
export interface UseViewportThresholdOptions {
|
|
3
|
+
collapseAt: number;
|
|
4
|
+
expandAt?: number;
|
|
5
|
+
initialPastThreshold?: boolean;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface UseViewportThresholdReturn<TElement extends Element> {
|
|
9
|
+
isPastThreshold: boolean;
|
|
10
|
+
sentinelRef: RefObject<TElement>;
|
|
11
|
+
}
|
|
12
|
+
export declare function useViewportThreshold<TElement extends Element = HTMLDivElement>({ collapseAt, expandAt, initialPastThreshold, disabled, }: UseViewportThresholdOptions): UseViewportThresholdReturn<TElement>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
export function useViewportThreshold({ collapseAt, expandAt = collapseAt, initialPastThreshold = false, disabled = false, }) {
|
|
4
|
+
const [isPastThreshold, setIsPastThreshold] = useState(initialPastThreshold);
|
|
5
|
+
const sentinelRef = useRef(null);
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
if (disabled)
|
|
8
|
+
return;
|
|
9
|
+
const sentinel = sentinelRef.current;
|
|
10
|
+
if (!sentinel)
|
|
11
|
+
return;
|
|
12
|
+
let frameId = null;
|
|
13
|
+
const updateThresholdState = () => {
|
|
14
|
+
frameId = null;
|
|
15
|
+
const sentinelTop = sentinel.getBoundingClientRect().top;
|
|
16
|
+
setIsPastThreshold((prev) => {
|
|
17
|
+
if (prev) {
|
|
18
|
+
return sentinelTop <= expandAt;
|
|
19
|
+
}
|
|
20
|
+
return sentinelTop <= collapseAt;
|
|
21
|
+
});
|
|
22
|
+
};
|
|
23
|
+
const requestUpdate = () => {
|
|
24
|
+
if (frameId !== null)
|
|
25
|
+
return;
|
|
26
|
+
frameId = window.requestAnimationFrame(updateThresholdState);
|
|
27
|
+
};
|
|
28
|
+
requestUpdate();
|
|
29
|
+
window.addEventListener("scroll", requestUpdate, { passive: true });
|
|
30
|
+
window.addEventListener("resize", requestUpdate);
|
|
31
|
+
return () => {
|
|
32
|
+
if (frameId !== null) {
|
|
33
|
+
window.cancelAnimationFrame(frameId);
|
|
34
|
+
}
|
|
35
|
+
window.removeEventListener("scroll", requestUpdate);
|
|
36
|
+
window.removeEventListener("resize", requestUpdate);
|
|
37
|
+
};
|
|
38
|
+
}, [collapseAt, disabled, expandAt]);
|
|
39
|
+
return {
|
|
40
|
+
isPastThreshold,
|
|
41
|
+
sentinelRef,
|
|
42
|
+
};
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neasg/design-system",
|
|
3
|
-
"version": "0.3
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "NEA shared design system primitives, theme tokens, and Storybook docs.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -35,10 +35,12 @@
|
|
|
35
35
|
"@dnd-kit/utilities": "^3.2.2",
|
|
36
36
|
"@govtechsg/sgds-web-component": "^3.8.1",
|
|
37
37
|
"@radix-ui/react-avatar": "^1.1.11",
|
|
38
|
+
"@radix-ui/react-collapsible": "^1.1.12",
|
|
38
39
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
39
40
|
"@radix-ui/react-popover": "^1.1.15",
|
|
40
41
|
"@radix-ui/react-select": "^2.2.6",
|
|
41
42
|
"@radix-ui/react-slot": "^1.2.4",
|
|
43
|
+
"@radix-ui/react-switch": "^1.2.6",
|
|
42
44
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
43
45
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
44
46
|
"class-variance-authority": "^0.7.0",
|
|
@@ -56,10 +58,10 @@
|
|
|
56
58
|
},
|
|
57
59
|
"devDependencies": {
|
|
58
60
|
"@fontsource/inter": "^5.2.8",
|
|
59
|
-
"@storybook/addon-docs": "10.
|
|
60
|
-
"@storybook/addon-
|
|
61
|
-
"@storybook/
|
|
62
|
-
"@storybook/react-vite": "10.
|
|
61
|
+
"@storybook/addon-docs": "10.3.5",
|
|
62
|
+
"@storybook/addon-mcp": "^0.5.0",
|
|
63
|
+
"@storybook/addon-themes": "10.3.5",
|
|
64
|
+
"@storybook/react-vite": "10.3.5",
|
|
63
65
|
"@testing-library/jest-dom": "^6.9.1",
|
|
64
66
|
"@types/node": "^20.10.5",
|
|
65
67
|
"@types/react": "^18.2.45",
|
|
@@ -70,7 +72,7 @@
|
|
|
70
72
|
"postcss": "^8.4.31",
|
|
71
73
|
"react": "^18.2.0",
|
|
72
74
|
"react-dom": "^18.2.0",
|
|
73
|
-
"storybook": "10.
|
|
75
|
+
"storybook": "10.3.5",
|
|
74
76
|
"tailwindcss": "^3.4.1",
|
|
75
77
|
"typescript": "^5.3.3",
|
|
76
78
|
"vitest": "^4.1.0"
|
|
@@ -92,6 +94,10 @@
|
|
|
92
94
|
"types": "./dist/badge.d.ts",
|
|
93
95
|
"import": "./dist/badge.js"
|
|
94
96
|
},
|
|
97
|
+
"./back-button": {
|
|
98
|
+
"types": "./dist/back-button.d.ts",
|
|
99
|
+
"import": "./dist/back-button.js"
|
|
100
|
+
},
|
|
95
101
|
"./button": {
|
|
96
102
|
"types": "./dist/button.d.ts",
|
|
97
103
|
"import": "./dist/button.js"
|
|
@@ -172,6 +178,14 @@
|
|
|
172
178
|
"types": "./dist/page-header.d.ts",
|
|
173
179
|
"import": "./dist/page-header.js"
|
|
174
180
|
},
|
|
181
|
+
"./page-section": {
|
|
182
|
+
"types": "./dist/page-section.d.ts",
|
|
183
|
+
"import": "./dist/page-section.js"
|
|
184
|
+
},
|
|
185
|
+
"./popover": {
|
|
186
|
+
"types": "./dist/popover.d.ts",
|
|
187
|
+
"import": "./dist/popover.js"
|
|
188
|
+
},
|
|
175
189
|
"./pagination": {
|
|
176
190
|
"types": "./dist/pagination.d.ts",
|
|
177
191
|
"import": "./dist/pagination.js"
|
|
@@ -180,6 +194,10 @@
|
|
|
180
194
|
"types": "./dist/search-input.d.ts",
|
|
181
195
|
"import": "./dist/search-input.js"
|
|
182
196
|
},
|
|
197
|
+
"./section-nav": {
|
|
198
|
+
"types": "./dist/section-nav.d.ts",
|
|
199
|
+
"import": "./dist/section-nav.js"
|
|
200
|
+
},
|
|
183
201
|
"./styles.css": "./dist/styles.css",
|
|
184
202
|
"./table": {
|
|
185
203
|
"types": "./dist/table.d.ts",
|
|
@@ -213,6 +231,14 @@
|
|
|
213
231
|
"types": "./dist/tooltip.d.ts",
|
|
214
232
|
"import": "./dist/tooltip.js"
|
|
215
233
|
},
|
|
234
|
+
"./separator": {
|
|
235
|
+
"types": "./dist/separator.d.ts",
|
|
236
|
+
"import": "./dist/separator.js"
|
|
237
|
+
},
|
|
238
|
+
"./collapsible-section": {
|
|
239
|
+
"types": "./dist/collapsible-section.d.ts",
|
|
240
|
+
"import": "./dist/collapsible-section.js"
|
|
241
|
+
},
|
|
216
242
|
"./lib/utils": {
|
|
217
243
|
"types": "./dist/lib/utils.d.ts",
|
|
218
244
|
"import": "./dist/lib/utils.js"
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export interface TabDropdownSection {
|
|
2
|
-
id: string;
|
|
3
|
-
label: string;
|
|
4
|
-
}
|
|
5
|
-
export interface TabWithDropdownProps {
|
|
6
|
-
/** Tab value — must match a TabsContent value. */
|
|
7
|
-
value: string;
|
|
8
|
-
/** Tab label shown in the trigger. */
|
|
9
|
-
label: string;
|
|
10
|
-
/** Dropdown sections shown on hover. */
|
|
11
|
-
sections: TabDropdownSection[];
|
|
12
|
-
/** Called when a section is selected. Receives the section id. */
|
|
13
|
-
onSectionSelect?: (sectionId: string) => void;
|
|
14
|
-
/** Scroll offset in pixels when scrolling to a section. */
|
|
15
|
-
scrollOffset?: number;
|
|
16
|
-
}
|
|
17
|
-
declare function TabWithDropdown({ value, label, sections, onSectionSelect, scrollOffset, }: TabWithDropdownProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
-
export { TabWithDropdown };
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
import * as ReactDOM from "react-dom";
|
|
5
|
-
import { ChevronDownIcon } from "./animated-icons/chevron-down";
|
|
6
|
-
import { cn } from "./lib/utils";
|
|
7
|
-
import { TabsTrigger, useTabsContext } from "./tabs";
|
|
8
|
-
function TabWithDropdown({ value, label, sections, onSectionSelect, scrollOffset = 200, }) {
|
|
9
|
-
const [isOpen, setIsOpen] = React.useState(false);
|
|
10
|
-
const [dropdownPosition, setDropdownPosition] = React.useState({
|
|
11
|
-
top: 0,
|
|
12
|
-
left: 0,
|
|
13
|
-
});
|
|
14
|
-
const triggerRef = React.useRef(null);
|
|
15
|
-
const timeoutRef = React.useRef(null);
|
|
16
|
-
const tabsContext = useTabsContext();
|
|
17
|
-
const updatePosition = React.useCallback(() => {
|
|
18
|
-
if (triggerRef.current) {
|
|
19
|
-
const rect = triggerRef.current.getBoundingClientRect();
|
|
20
|
-
setDropdownPosition({
|
|
21
|
-
top: rect.bottom + 8,
|
|
22
|
-
left: rect.left,
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
}, []);
|
|
26
|
-
const handleMouseEnter = () => {
|
|
27
|
-
if (timeoutRef.current)
|
|
28
|
-
clearTimeout(timeoutRef.current);
|
|
29
|
-
updatePosition();
|
|
30
|
-
setIsOpen(true);
|
|
31
|
-
};
|
|
32
|
-
const handleMouseLeave = () => {
|
|
33
|
-
timeoutRef.current = setTimeout(() => setIsOpen(false), 150);
|
|
34
|
-
};
|
|
35
|
-
const handleDropdownMouseEnter = () => {
|
|
36
|
-
if (timeoutRef.current)
|
|
37
|
-
clearTimeout(timeoutRef.current);
|
|
38
|
-
};
|
|
39
|
-
const handleDropdownMouseLeave = () => {
|
|
40
|
-
timeoutRef.current = setTimeout(() => setIsOpen(false), 150);
|
|
41
|
-
};
|
|
42
|
-
const handleSectionClick = (sectionId) => {
|
|
43
|
-
if (onSectionSelect) {
|
|
44
|
-
onSectionSelect(sectionId);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
// Default: switch tab and scroll to the section element
|
|
48
|
-
const doScroll = () => {
|
|
49
|
-
const element = document.getElementById(sectionId);
|
|
50
|
-
if (!element)
|
|
51
|
-
return;
|
|
52
|
-
const top = element.getBoundingClientRect().top + window.scrollY - scrollOffset;
|
|
53
|
-
window.scrollTo({ top, behavior: "smooth" });
|
|
54
|
-
};
|
|
55
|
-
if (tabsContext && tabsContext.value !== value) {
|
|
56
|
-
tabsContext.setValue(value);
|
|
57
|
-
setTimeout(doScroll, 50);
|
|
58
|
-
}
|
|
59
|
-
else {
|
|
60
|
-
doScroll();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
setIsOpen(false);
|
|
64
|
-
};
|
|
65
|
-
return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, onMouseEnter: handleMouseEnter, onMouseLeave: handleMouseLeave, children: _jsxs(TabsTrigger, { value: value, className: "flex items-center gap-1", children: [label, sections.length > 0 ? (_jsx(ChevronDownIcon, { className: cn("transition-transform duration-200", isOpen && "rotate-180"), size: 12 })) : null] }) }), isOpen &&
|
|
66
|
-
sections.length > 0 &&
|
|
67
|
-
typeof document !== "undefined" &&
|
|
68
|
-
ReactDOM.createPortal(_jsx("div", { className: "fixed z-[100] rounded-md border bg-popover p-1 shadow-lg animate-in fade-in-0 zoom-in-95", style: { top: dropdownPosition.top, left: dropdownPosition.left }, onMouseEnter: handleDropdownMouseEnter, onMouseLeave: handleDropdownMouseLeave, children: sections.map((section) => (_jsx("button", { type: "button", onClick: () => handleSectionClick(section.id), className: "block w-full rounded-sm px-3 py-2 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground", children: section.label }, section.id))) }), document.body)] }));
|
|
69
|
-
}
|
|
70
|
-
export { TabWithDropdown };
|