@campfire-interactive/side-nav 0.1.2
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/dist/index.d.ts +58 -0
- package/dist/index.js +169 -0
- package/package.json +40 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface NavItem {
|
|
5
|
+
/** Stable key. Used for React keying and as default fallback for `path`. */
|
|
6
|
+
id: string;
|
|
7
|
+
/** Already-resolved display text. Consumer handles i18n before passing in. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** Optional icon (typically a lucide component or SVG). */
|
|
10
|
+
icon?: ReactNode;
|
|
11
|
+
/** Route to navigate to. Defaults to `/${id}` if omitted. */
|
|
12
|
+
path?: string;
|
|
13
|
+
/** Optional badge (count, "new", etc). */
|
|
14
|
+
badge?: string | number;
|
|
15
|
+
}
|
|
16
|
+
interface NavGroup {
|
|
17
|
+
id: string;
|
|
18
|
+
/** Section header text shown above the items. */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Items in this group. */
|
|
21
|
+
items: NavItem[];
|
|
22
|
+
}
|
|
23
|
+
/** Discriminated by presence of `items` — groups have it, items don't. */
|
|
24
|
+
type NavEntry = NavItem | NavGroup;
|
|
25
|
+
declare function isNavGroup(entry: NavEntry): entry is NavGroup;
|
|
26
|
+
interface SideNavProps {
|
|
27
|
+
/** Primary nav entries. Mix of items and groups, in render order. */
|
|
28
|
+
items: NavEntry[];
|
|
29
|
+
/** Pinned at the bottom of the nav, above the footer slot. */
|
|
30
|
+
footerItems?: NavEntry[];
|
|
31
|
+
/** Current route path — used to highlight the active item. */
|
|
32
|
+
activeRoute?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Called when an item is activated. Receives the item and the click event.
|
|
35
|
+
* Consumer handles routing — component does not import a router.
|
|
36
|
+
*/
|
|
37
|
+
onNavigate?: (item: NavItem, event: React.MouseEvent) => void;
|
|
38
|
+
/** Enables the icon-only collapse mode + circular toggle button. */
|
|
39
|
+
collapsible?: boolean;
|
|
40
|
+
/** Controlled collapse state. Required when `collapsible` is true. */
|
|
41
|
+
collapsed?: boolean;
|
|
42
|
+
/** Called when the user clicks the collapse toggle. */
|
|
43
|
+
onCollapseToggle?: () => void;
|
|
44
|
+
/**
|
|
45
|
+
* Mobile drawer mode. Below the mobile breakpoint the nav is hidden by
|
|
46
|
+
* default and slides in when `isOpen` is true. Pass both props together.
|
|
47
|
+
*/
|
|
48
|
+
isOpen?: boolean;
|
|
49
|
+
onClose?: () => void;
|
|
50
|
+
/** Custom footer content rendered at the very bottom. */
|
|
51
|
+
footer?: ReactNode;
|
|
52
|
+
/** Optional additional className on the root element. */
|
|
53
|
+
className?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
declare function SideNav({ items, footerItems, activeRoute, onNavigate, collapsible, collapsed: collapsedProp, onCollapseToggle, isOpen, onClose, footer, className, }: SideNavProps): react_jsx_runtime.JSX.Element;
|
|
57
|
+
|
|
58
|
+
export { type NavEntry, type NavGroup, type NavItem, SideNav, type SideNavProps, isNavGroup };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// src/SideNav.tsx
|
|
2
|
+
import "@campfire-interactive/design-tokens/tokens.css";
|
|
3
|
+
|
|
4
|
+
// #style-inject:#style-inject
|
|
5
|
+
function styleInject(css, { insertAt } = {}) {
|
|
6
|
+
if (!css || typeof document === "undefined") return;
|
|
7
|
+
const head = document.head || document.getElementsByTagName("head")[0];
|
|
8
|
+
const style = document.createElement("style");
|
|
9
|
+
style.type = "text/css";
|
|
10
|
+
if (insertAt === "top") {
|
|
11
|
+
if (head.firstChild) {
|
|
12
|
+
head.insertBefore(style, head.firstChild);
|
|
13
|
+
} else {
|
|
14
|
+
head.appendChild(style);
|
|
15
|
+
}
|
|
16
|
+
} else {
|
|
17
|
+
head.appendChild(style);
|
|
18
|
+
}
|
|
19
|
+
if (style.styleSheet) {
|
|
20
|
+
style.styleSheet.cssText = css;
|
|
21
|
+
} else {
|
|
22
|
+
style.appendChild(document.createTextNode(css));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// src/styles.css
|
|
27
|
+
styleInject(".cfi-sn {\n --cfi-sidenav-width: 240px;\n --cfi-sidenav-width-collapsed: 64px;\n --cfi-sidenav-mobile-breakpoint: 1024px;\n --cfi-sidenav-bg: var(--cfi-app-surface, white);\n --cfi-sidenav-border: var(--cfi-color-gray-200, #e5e7eb);\n --cfi-sidenav-fg: var(--cfi-color-gray-700, #374151);\n --cfi-sidenav-fg-muted: var(--cfi-color-gray-400, #9ca3af);\n --cfi-sidenav-item-hover-bg: var(--cfi-color-gray-100, #f3f4f6);\n --cfi-sidenav-item-hover-fg: var(--cfi-color-gray-900, #111827);\n --cfi-sidenav-item-active-bg: var(--cfi-app-primary, var(--cfi-color-primary-600, #ea580c));\n --cfi-sidenav-item-active-fg: var(--cfi-app-primary-fg, white);\n --cfi-sidenav-group-label-fg: var(--cfi-color-gray-500, #6b7280);\n --cfi-sidenav-toggle-bg: var(--cfi-sidenav-bg);\n --cfi-sidenav-toggle-border: var(--cfi-color-gray-300, #d1d5db);\n --cfi-sidenav-toggle-fg: var(--cfi-color-gray-500, #6b7280);\n --cfi-sidenav-scrollbar-thumb: var(--cfi-color-gray-300, #d1d5db);\n --cfi-sidenav-scrollbar-track: transparent;\n position: relative;\n display: flex;\n flex-direction: column;\n flex-shrink: 0;\n width: var(--cfi-sidenav-width);\n height: 100%;\n background: var(--cfi-sidenav-bg);\n color: var(--cfi-sidenav-fg);\n border-right: 1px solid var(--cfi-sidenav-border);\n font-family: var(--cfi-font-family, system-ui, sans-serif);\n font-size: var(--cfi-font-size-sm, 0.875rem);\n transition: width 200ms ease;\n}\n.cfi-sn--collapsed {\n width: var(--cfi-sidenav-width-collapsed);\n}\n.cfi-sn-list {\n list-style: none;\n margin: 0;\n padding: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n.cfi-sn-list--primary {\n flex: 1;\n padding: var(--cfi-spacing-sm, 0.5rem);\n overflow-y: auto;\n overflow-x: hidden;\n scrollbar-width: thin;\n scrollbar-color: var(--cfi-sidenav-scrollbar-thumb) var(--cfi-sidenav-scrollbar-track);\n}\n.cfi-sn-list--primary::-webkit-scrollbar {\n width: 6px;\n}\n.cfi-sn-list--primary::-webkit-scrollbar-track {\n background: var(--cfi-sidenav-scrollbar-track);\n}\n.cfi-sn-list--primary::-webkit-scrollbar-thumb {\n background: var(--cfi-sidenav-scrollbar-thumb);\n border-radius: 3px;\n}\n.cfi-sn-list--footer {\n padding: var(--cfi-spacing-sm, 0.5rem);\n border-top: 1px solid var(--cfi-sidenav-border);\n}\n.cfi-sn-item {\n display: flex;\n align-items: center;\n gap: var(--cfi-spacing-sm, 0.5rem);\n padding: 10px 12px;\n border-radius: var(--cfi-radius-md, 0.375rem);\n color: var(--cfi-sidenav-fg);\n font-size: inherit;\n font-weight: 500;\n text-decoration: none;\n cursor: pointer;\n transition: background-color 100ms, color 100ms;\n white-space: nowrap;\n}\n.cfi-sn-item:hover {\n background-color: var(--cfi-sidenav-item-hover-bg);\n color: var(--cfi-sidenav-item-hover-fg);\n}\n.cfi-sn-item--active,\n.cfi-sn-item--active:hover {\n background-color: var(--cfi-sidenav-item-active-bg);\n color: var(--cfi-sidenav-item-active-fg);\n}\n.cfi-sn-icon {\n flex-shrink: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 20px;\n height: 20px;\n}\n.cfi-sn-label {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n.cfi-sn-badge {\n flex-shrink: 0;\n min-width: 18px;\n height: 18px;\n padding: 0 5px;\n border-radius: 9px;\n background: var(--cfi-color-error, #ef4444);\n color: white;\n font-size: 11px;\n font-weight: 500;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.cfi-sn--collapsed .cfi-sn-item {\n justify-content: center;\n padding: 10px;\n}\n.cfi-sn-group {\n list-style: none;\n margin-top: var(--cfi-spacing-sm, 0.5rem);\n}\n.cfi-sn-group:first-child {\n margin-top: 0;\n}\n.cfi-sn-group-label {\n padding: var(--cfi-spacing-xs, 0.25rem) 12px;\n color: var(--cfi-sidenav-group-label-fg);\n font-size: var(--cfi-font-size-xs, 0.75rem);\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n}\n.cfi-sn-toggle {\n position: absolute;\n top: var(--cfi-spacing-md, 1rem);\n right: -12px;\n z-index: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n padding: 0;\n background: var(--cfi-sidenav-toggle-bg);\n border: 1px solid var(--cfi-sidenav-toggle-border);\n border-radius: 50%;\n color: var(--cfi-sidenav-toggle-fg);\n cursor: pointer;\n box-shadow: var(--cfi-shadow-sm, 0 1px 2px rgba(0, 0, 0, 0.05));\n transition: color 100ms;\n}\n.cfi-sn-toggle:hover {\n color: var(--cfi-sidenav-fg);\n}\n.cfi-sn-footer {\n padding: var(--cfi-spacing-sm, 0.5rem);\n border-top: 1px solid var(--cfi-sidenav-border);\n color: var(--cfi-sidenav-fg-muted);\n font-size: var(--cfi-font-size-xs, 0.75rem);\n}\n.cfi-sn-backdrop {\n display: none;\n}\n@media (max-width: 1023px) {\n .cfi-sn--drawer {\n position: fixed;\n top: 0;\n left: 0;\n bottom: 0;\n z-index: 90;\n transform: translateX(0);\n transition: transform 200ms ease;\n }\n .cfi-sn--drawer.cfi-sn--drawer-closed {\n transform: translateX(-100%);\n }\n .cfi-sn-backdrop {\n display: block;\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.3);\n z-index: 80;\n }\n}\n");
|
|
28
|
+
|
|
29
|
+
// src/SideNav.tsx
|
|
30
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
31
|
+
|
|
32
|
+
// src/types.ts
|
|
33
|
+
function isNavGroup(entry) {
|
|
34
|
+
return "items" in entry && Array.isArray(entry.items);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// src/SideNav.tsx
|
|
38
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
39
|
+
function isActive(activeRoute, item) {
|
|
40
|
+
if (!activeRoute) return false;
|
|
41
|
+
const target = item.path ?? `/${item.id}`;
|
|
42
|
+
if (target === "/") return activeRoute === "/";
|
|
43
|
+
return activeRoute === target || activeRoute.startsWith(`${target}/`);
|
|
44
|
+
}
|
|
45
|
+
function NavItemRow({ item, active, collapsed, onNavigate, onClose }) {
|
|
46
|
+
const href = item.path ?? `/${item.id}`;
|
|
47
|
+
const handleClick = (e) => {
|
|
48
|
+
onNavigate?.(item, e);
|
|
49
|
+
onClose?.();
|
|
50
|
+
};
|
|
51
|
+
return /* @__PURE__ */ jsx("li", { children: /* @__PURE__ */ jsxs(
|
|
52
|
+
"a",
|
|
53
|
+
{
|
|
54
|
+
href,
|
|
55
|
+
className: active ? "cfi-sn-item cfi-sn-item--active" : "cfi-sn-item",
|
|
56
|
+
onClick: handleClick,
|
|
57
|
+
title: collapsed ? item.label : void 0,
|
|
58
|
+
"aria-current": active ? "page" : void 0,
|
|
59
|
+
children: [
|
|
60
|
+
item.icon && /* @__PURE__ */ jsx("span", { className: "cfi-sn-icon", children: item.icon }),
|
|
61
|
+
!collapsed && /* @__PURE__ */ jsx("span", { className: "cfi-sn-label", children: item.label }),
|
|
62
|
+
!collapsed && item.badge !== void 0 && /* @__PURE__ */ jsx("span", { className: "cfi-sn-badge", children: item.badge })
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
) });
|
|
66
|
+
}
|
|
67
|
+
function NavEntries({ entries, activeRoute, collapsed, onNavigate, onClose }) {
|
|
68
|
+
return /* @__PURE__ */ jsx(Fragment, { children: entries.map((entry) => {
|
|
69
|
+
if (isNavGroup(entry)) {
|
|
70
|
+
const group = entry;
|
|
71
|
+
return /* @__PURE__ */ jsxs("li", { className: "cfi-sn-group", children: [
|
|
72
|
+
!collapsed && /* @__PURE__ */ jsx("div", { className: "cfi-sn-group-label", children: group.label }),
|
|
73
|
+
/* @__PURE__ */ jsx("ul", { className: "cfi-sn-list", children: group.items.map((item) => /* @__PURE__ */ jsx(
|
|
74
|
+
NavItemRow,
|
|
75
|
+
{
|
|
76
|
+
item,
|
|
77
|
+
active: isActive(activeRoute, item),
|
|
78
|
+
collapsed,
|
|
79
|
+
onNavigate,
|
|
80
|
+
onClose
|
|
81
|
+
},
|
|
82
|
+
item.id
|
|
83
|
+
)) })
|
|
84
|
+
] }, group.id);
|
|
85
|
+
}
|
|
86
|
+
return /* @__PURE__ */ jsx(
|
|
87
|
+
NavItemRow,
|
|
88
|
+
{
|
|
89
|
+
item: entry,
|
|
90
|
+
active: isActive(activeRoute, entry),
|
|
91
|
+
collapsed,
|
|
92
|
+
onNavigate,
|
|
93
|
+
onClose
|
|
94
|
+
},
|
|
95
|
+
entry.id
|
|
96
|
+
);
|
|
97
|
+
}) });
|
|
98
|
+
}
|
|
99
|
+
function SideNav({
|
|
100
|
+
items,
|
|
101
|
+
footerItems,
|
|
102
|
+
activeRoute,
|
|
103
|
+
onNavigate,
|
|
104
|
+
collapsible,
|
|
105
|
+
collapsed: collapsedProp,
|
|
106
|
+
onCollapseToggle,
|
|
107
|
+
isOpen,
|
|
108
|
+
onClose,
|
|
109
|
+
footer,
|
|
110
|
+
className
|
|
111
|
+
}) {
|
|
112
|
+
const collapsed = !!(collapsible && collapsedProp);
|
|
113
|
+
const drawerMode = isOpen !== void 0;
|
|
114
|
+
const rootClass = [
|
|
115
|
+
"cfi-sn",
|
|
116
|
+
collapsed ? "cfi-sn--collapsed" : "",
|
|
117
|
+
drawerMode && !isOpen ? "cfi-sn--drawer-closed" : "",
|
|
118
|
+
drawerMode ? "cfi-sn--drawer" : "",
|
|
119
|
+
className ?? ""
|
|
120
|
+
].filter(Boolean).join(" ");
|
|
121
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
122
|
+
drawerMode && isOpen && /* @__PURE__ */ jsx(
|
|
123
|
+
"div",
|
|
124
|
+
{
|
|
125
|
+
className: "cfi-sn-backdrop",
|
|
126
|
+
onClick: onClose,
|
|
127
|
+
"aria-hidden": "true"
|
|
128
|
+
}
|
|
129
|
+
),
|
|
130
|
+
/* @__PURE__ */ jsxs("nav", { className: rootClass, "aria-label": "Primary", children: [
|
|
131
|
+
collapsible && /* @__PURE__ */ jsx(
|
|
132
|
+
"button",
|
|
133
|
+
{
|
|
134
|
+
type: "button",
|
|
135
|
+
className: "cfi-sn-toggle",
|
|
136
|
+
onClick: onCollapseToggle,
|
|
137
|
+
"aria-label": collapsed ? "Expand navigation" : "Collapse navigation",
|
|
138
|
+
"aria-pressed": collapsed,
|
|
139
|
+
children: collapsed ? /* @__PURE__ */ jsx(ChevronRight, { size: 12 }) : /* @__PURE__ */ jsx(ChevronLeft, { size: 12 })
|
|
140
|
+
}
|
|
141
|
+
),
|
|
142
|
+
/* @__PURE__ */ jsx("ul", { className: "cfi-sn-list cfi-sn-list--primary", children: /* @__PURE__ */ jsx(
|
|
143
|
+
NavEntries,
|
|
144
|
+
{
|
|
145
|
+
entries: items,
|
|
146
|
+
activeRoute,
|
|
147
|
+
collapsed,
|
|
148
|
+
onNavigate,
|
|
149
|
+
onClose
|
|
150
|
+
}
|
|
151
|
+
) }),
|
|
152
|
+
footerItems && footerItems.length > 0 && /* @__PURE__ */ jsx("ul", { className: "cfi-sn-list cfi-sn-list--footer", children: /* @__PURE__ */ jsx(
|
|
153
|
+
NavEntries,
|
|
154
|
+
{
|
|
155
|
+
entries: footerItems,
|
|
156
|
+
activeRoute,
|
|
157
|
+
collapsed,
|
|
158
|
+
onNavigate,
|
|
159
|
+
onClose
|
|
160
|
+
}
|
|
161
|
+
) }),
|
|
162
|
+
footer && /* @__PURE__ */ jsx("div", { className: "cfi-sn-footer", children: footer })
|
|
163
|
+
] })
|
|
164
|
+
] });
|
|
165
|
+
}
|
|
166
|
+
export {
|
|
167
|
+
SideNav,
|
|
168
|
+
isNavGroup
|
|
169
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@campfire-interactive/side-nav",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Shared side-navigation component for Campfire Suite apps",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
23
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@campfire-interactive/design-tokens": "*",
|
|
27
|
+
"lucide-react": "^0.460.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/react": "^19.0.0",
|
|
31
|
+
"react": "^19.0.0",
|
|
32
|
+
"react-dom": "^19.0.0",
|
|
33
|
+
"tsup": "^8.0.0",
|
|
34
|
+
"typescript": "^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"registry": "https://registry.npmjs.org",
|
|
38
|
+
"access": "public"
|
|
39
|
+
}
|
|
40
|
+
}
|