featuredrop 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/LICENSE +21 -0
- package/README.md +293 -0
- package/dist/index.cjs +133 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +131 -0
- package/dist/index.d.ts +131 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +197 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +154 -0
- package/dist/react.d.ts +154 -0
- package/dist/react.js +190 -0
- package/dist/react.js.map +1 -0
- package/package.json +110 -0
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import * as react from 'react';
|
|
3
|
+
import { ReactNode, CSSProperties } from 'react';
|
|
4
|
+
|
|
5
|
+
/** A single feature entry in the manifest */
|
|
6
|
+
interface FeatureEntry {
|
|
7
|
+
/** Unique identifier for the feature */
|
|
8
|
+
id: string;
|
|
9
|
+
/** Human-readable label (e.g. "Decision Journal") */
|
|
10
|
+
label: string;
|
|
11
|
+
/** Optional longer description */
|
|
12
|
+
description?: string;
|
|
13
|
+
/** ISO date when this feature was released */
|
|
14
|
+
releasedAt: string;
|
|
15
|
+
/** ISO date after which the "new" badge should stop showing */
|
|
16
|
+
showNewUntil: string;
|
|
17
|
+
/** Optional key to match navigation items (e.g. "/journal", "settings") */
|
|
18
|
+
sidebarKey?: string;
|
|
19
|
+
/** Optional grouping category (e.g. "ai", "billing", "core") */
|
|
20
|
+
category?: string;
|
|
21
|
+
/** Optional URL to link to (e.g. docs page, changelog entry) */
|
|
22
|
+
url?: string;
|
|
23
|
+
/** Optional version string when this feature shipped */
|
|
24
|
+
version?: string;
|
|
25
|
+
/** Optional arbitrary metadata */
|
|
26
|
+
meta?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
/** The full feature manifest — an array of feature entries */
|
|
29
|
+
type FeatureManifest = readonly FeatureEntry[];
|
|
30
|
+
/**
|
|
31
|
+
* Storage adapter interface — implement for your persistence layer.
|
|
32
|
+
*
|
|
33
|
+
* The adapter bridges two data sources:
|
|
34
|
+
* - **Watermark**: a server-side timestamp ("features seen at")
|
|
35
|
+
* - **Dismissed IDs**: client-side per-feature dismissals
|
|
36
|
+
*/
|
|
37
|
+
interface StorageAdapter {
|
|
38
|
+
/** Get the user's "features seen at" watermark (ISO string or null) */
|
|
39
|
+
getWatermark(): string | null;
|
|
40
|
+
/** Get the set of individually dismissed feature IDs */
|
|
41
|
+
getDismissedIds(): ReadonlySet<string>;
|
|
42
|
+
/** Dismiss a single feature by ID */
|
|
43
|
+
dismiss(id: string): void;
|
|
44
|
+
/** Dismiss all features — sets watermark to `now` and clears dismissals */
|
|
45
|
+
dismissAll(now: Date): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface FeatureDropProviderProps {
|
|
49
|
+
/** The feature manifest — typically a frozen array of FeatureEntry objects */
|
|
50
|
+
manifest: FeatureManifest;
|
|
51
|
+
/** Storage adapter instance (e.g. LocalStorageAdapter, MemoryAdapter) */
|
|
52
|
+
storage: StorageAdapter;
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Provides feature discovery state to the component tree.
|
|
57
|
+
*
|
|
58
|
+
* Wrap your app (or a subtree) with this provider to enable
|
|
59
|
+
* `useFeatureDrop`, `useNewFeature`, and `useNewCount` hooks.
|
|
60
|
+
*/
|
|
61
|
+
declare function FeatureDropProvider({ manifest, storage, children, }: FeatureDropProviderProps): react_jsx_runtime.JSX.Element;
|
|
62
|
+
|
|
63
|
+
interface FeatureDropContextValue {
|
|
64
|
+
/** All currently "new" features */
|
|
65
|
+
newFeatures: FeatureEntry[];
|
|
66
|
+
/** Count of new features */
|
|
67
|
+
newCount: number;
|
|
68
|
+
/** Check if a sidebar key has any new features */
|
|
69
|
+
isNew: (sidebarKey: string) => boolean;
|
|
70
|
+
/** Dismiss a single feature by ID */
|
|
71
|
+
dismiss: (id: string) => void;
|
|
72
|
+
/** Dismiss all features (marks all as seen) */
|
|
73
|
+
dismissAll: () => Promise<void>;
|
|
74
|
+
/** Get the feature entry for a sidebar key (if it's new) */
|
|
75
|
+
getFeature: (sidebarKey: string) => FeatureEntry | undefined;
|
|
76
|
+
}
|
|
77
|
+
declare const FeatureDropContext: react.Context<FeatureDropContextValue | null>;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Access the full feature discovery context.
|
|
81
|
+
*
|
|
82
|
+
* Returns: `{ newFeatures, newCount, isNew, dismiss, dismissAll, getFeature }`
|
|
83
|
+
*
|
|
84
|
+
* @throws Error if used outside of `<FeatureDropProvider>`
|
|
85
|
+
*/
|
|
86
|
+
declare function useFeatureDrop(): FeatureDropContextValue;
|
|
87
|
+
|
|
88
|
+
interface UseNewFeatureResult {
|
|
89
|
+
/** Whether this sidebar key has a new feature */
|
|
90
|
+
isNew: boolean;
|
|
91
|
+
/** The feature entry, if new */
|
|
92
|
+
feature: FeatureEntry | undefined;
|
|
93
|
+
/** Dismiss the feature for this sidebar key */
|
|
94
|
+
dismiss: () => void;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check if a single navigation item has a new feature.
|
|
98
|
+
*
|
|
99
|
+
* @param sidebarKey - The key to check (e.g. "/journal", "settings")
|
|
100
|
+
* @returns `{ isNew, feature, dismiss }`
|
|
101
|
+
*/
|
|
102
|
+
declare function useNewFeature(sidebarKey: string): UseNewFeatureResult;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Get the count of currently new features.
|
|
106
|
+
*
|
|
107
|
+
* Useful for rendering a badge count on a "What's New" button.
|
|
108
|
+
*
|
|
109
|
+
* @returns The number of new features
|
|
110
|
+
*/
|
|
111
|
+
declare function useNewCount(): number;
|
|
112
|
+
|
|
113
|
+
interface NewBadgeRenderProps {
|
|
114
|
+
/** Whether the feature is currently new */
|
|
115
|
+
isNew: boolean;
|
|
116
|
+
}
|
|
117
|
+
interface NewBadgeProps {
|
|
118
|
+
/** Display variant */
|
|
119
|
+
variant?: "pill" | "dot" | "count";
|
|
120
|
+
/** Whether to show the badge (typically from `useNewFeature().isNew`) */
|
|
121
|
+
show?: boolean;
|
|
122
|
+
/** Count to display when variant is "count" */
|
|
123
|
+
count?: number;
|
|
124
|
+
/** Text label for the pill variant. Default: "New" */
|
|
125
|
+
label?: string;
|
|
126
|
+
/** Dismiss callback. If set with `dismissOnClick`, clicking dismisses. */
|
|
127
|
+
onDismiss?: () => void;
|
|
128
|
+
/** Whether clicking the badge should trigger onDismiss */
|
|
129
|
+
dismissOnClick?: boolean;
|
|
130
|
+
/** Additional CSS class */
|
|
131
|
+
className?: string;
|
|
132
|
+
/** Additional inline styles (merged with defaults) */
|
|
133
|
+
style?: CSSProperties;
|
|
134
|
+
/** Render prop for full customization */
|
|
135
|
+
children?: (props: NewBadgeRenderProps) => ReactNode;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Headless "New" badge component.
|
|
139
|
+
*
|
|
140
|
+
* Styled via CSS custom properties — zero CSS framework dependency:
|
|
141
|
+
* - `--featuredrop-color` — text/dot color
|
|
142
|
+
* - `--featuredrop-bg` — pill background
|
|
143
|
+
* - `--featuredrop-font-size` — font size
|
|
144
|
+
* - `--featuredrop-dot-size` — dot diameter
|
|
145
|
+
* - `--featuredrop-glow` — dot glow color
|
|
146
|
+
* - `--featuredrop-count-size` — count badge size
|
|
147
|
+
* - `--featuredrop-count-color` — count text color
|
|
148
|
+
* - `--featuredrop-count-bg` — count background
|
|
149
|
+
*
|
|
150
|
+
* Use `data-featuredrop` attribute for CSS selector styling.
|
|
151
|
+
*/
|
|
152
|
+
declare function NewBadge({ variant, show, count, label, onDismiss, dismissOnClick, className, style, children, }: NewBadgeProps): react_jsx_runtime.JSX.Element | null;
|
|
153
|
+
|
|
154
|
+
export { FeatureDropContext, type FeatureDropContextValue, FeatureDropProvider, type FeatureDropProviderProps, NewBadge, type NewBadgeProps, type NewBadgeRenderProps, type UseNewFeatureResult, useFeatureDrop, useNewCount, useNewFeature };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { createContext, useState, useCallback, useMemo, useContext } from 'react';
|
|
3
|
+
import { jsx, Fragment } from 'react/jsx-runtime';
|
|
4
|
+
|
|
5
|
+
// src/react/provider.tsx
|
|
6
|
+
|
|
7
|
+
// src/core.ts
|
|
8
|
+
function isNew(feature, watermark, dismissedIds, now = /* @__PURE__ */ new Date()) {
|
|
9
|
+
if (dismissedIds.has(feature.id)) return false;
|
|
10
|
+
const nowMs = now.getTime();
|
|
11
|
+
const showUntilMs = new Date(feature.showNewUntil).getTime();
|
|
12
|
+
if (nowMs >= showUntilMs) return false;
|
|
13
|
+
if (watermark) {
|
|
14
|
+
const watermarkMs = new Date(watermark).getTime();
|
|
15
|
+
const releasedMs = new Date(feature.releasedAt).getTime();
|
|
16
|
+
if (releasedMs <= watermarkMs) return false;
|
|
17
|
+
}
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
function getNewFeatures(manifest, storage, now = /* @__PURE__ */ new Date()) {
|
|
21
|
+
const watermark = storage.getWatermark();
|
|
22
|
+
const dismissedIds = storage.getDismissedIds();
|
|
23
|
+
return manifest.filter((f) => isNew(f, watermark, dismissedIds, now));
|
|
24
|
+
}
|
|
25
|
+
function hasNewFeature(manifest, sidebarKey, storage, now = /* @__PURE__ */ new Date()) {
|
|
26
|
+
const watermark = storage.getWatermark();
|
|
27
|
+
const dismissedIds = storage.getDismissedIds();
|
|
28
|
+
return manifest.some(
|
|
29
|
+
(f) => f.sidebarKey === sidebarKey && isNew(f, watermark, dismissedIds, now)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
var FeatureDropContext = createContext(
|
|
33
|
+
null
|
|
34
|
+
);
|
|
35
|
+
function FeatureDropProvider({
|
|
36
|
+
manifest,
|
|
37
|
+
storage,
|
|
38
|
+
children
|
|
39
|
+
}) {
|
|
40
|
+
const [newFeatures, setNewFeatures] = useState(
|
|
41
|
+
() => getNewFeatures(manifest, storage)
|
|
42
|
+
);
|
|
43
|
+
const recompute = useCallback(() => {
|
|
44
|
+
setNewFeatures(getNewFeatures(manifest, storage));
|
|
45
|
+
}, [manifest, storage]);
|
|
46
|
+
const dismiss = useCallback(
|
|
47
|
+
(id) => {
|
|
48
|
+
storage.dismiss(id);
|
|
49
|
+
recompute();
|
|
50
|
+
},
|
|
51
|
+
[storage, recompute]
|
|
52
|
+
);
|
|
53
|
+
const dismissAll = useCallback(async () => {
|
|
54
|
+
await storage.dismissAll(/* @__PURE__ */ new Date());
|
|
55
|
+
setNewFeatures([]);
|
|
56
|
+
}, [storage]);
|
|
57
|
+
const isNewFn = useCallback(
|
|
58
|
+
(sidebarKey) => hasNewFeature(manifest, sidebarKey, storage),
|
|
59
|
+
[manifest, storage]
|
|
60
|
+
);
|
|
61
|
+
const getFeature = useCallback(
|
|
62
|
+
(sidebarKey) => newFeatures.find((f) => f.sidebarKey === sidebarKey),
|
|
63
|
+
[newFeatures]
|
|
64
|
+
);
|
|
65
|
+
const value = useMemo(
|
|
66
|
+
() => ({
|
|
67
|
+
newFeatures,
|
|
68
|
+
newCount: newFeatures.length,
|
|
69
|
+
isNew: isNewFn,
|
|
70
|
+
dismiss,
|
|
71
|
+
dismissAll,
|
|
72
|
+
getFeature
|
|
73
|
+
}),
|
|
74
|
+
[newFeatures, isNewFn, dismiss, dismissAll, getFeature]
|
|
75
|
+
);
|
|
76
|
+
return /* @__PURE__ */ jsx(FeatureDropContext.Provider, { value, children });
|
|
77
|
+
}
|
|
78
|
+
function useFeatureDrop() {
|
|
79
|
+
const context = useContext(FeatureDropContext);
|
|
80
|
+
if (!context) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
"useFeatureDrop must be used within a <FeatureDropProvider>"
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return context;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/react/hooks/use-new-feature.ts
|
|
89
|
+
function useNewFeature(sidebarKey) {
|
|
90
|
+
const { isNew: isNew2, getFeature, dismiss } = useFeatureDrop();
|
|
91
|
+
const feature = getFeature(sidebarKey);
|
|
92
|
+
const isNewValue = isNew2(sidebarKey);
|
|
93
|
+
return {
|
|
94
|
+
isNew: isNewValue,
|
|
95
|
+
feature,
|
|
96
|
+
dismiss: () => {
|
|
97
|
+
if (feature) {
|
|
98
|
+
dismiss(feature.id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// src/react/hooks/use-new-count.ts
|
|
105
|
+
function useNewCount() {
|
|
106
|
+
const { newCount } = useFeatureDrop();
|
|
107
|
+
return newCount;
|
|
108
|
+
}
|
|
109
|
+
var baseStyles = {
|
|
110
|
+
display: "inline-flex",
|
|
111
|
+
alignItems: "center",
|
|
112
|
+
justifyContent: "center",
|
|
113
|
+
fontFamily: "inherit"
|
|
114
|
+
};
|
|
115
|
+
var pillStyles = {
|
|
116
|
+
...baseStyles,
|
|
117
|
+
padding: "2px 6px",
|
|
118
|
+
borderRadius: "9999px",
|
|
119
|
+
fontSize: "var(--featuredrop-font-size, 10px)",
|
|
120
|
+
fontWeight: 700,
|
|
121
|
+
textTransform: "uppercase",
|
|
122
|
+
letterSpacing: "0.05em",
|
|
123
|
+
lineHeight: 1,
|
|
124
|
+
color: "var(--featuredrop-color, #b45309)",
|
|
125
|
+
backgroundColor: "var(--featuredrop-bg, rgba(245, 158, 11, 0.15))"
|
|
126
|
+
};
|
|
127
|
+
var dotStyles = {
|
|
128
|
+
...baseStyles,
|
|
129
|
+
width: "var(--featuredrop-dot-size, 8px)",
|
|
130
|
+
height: "var(--featuredrop-dot-size, 8px)",
|
|
131
|
+
borderRadius: "9999px",
|
|
132
|
+
backgroundColor: "var(--featuredrop-color, #f59e0b)",
|
|
133
|
+
boxShadow: "0 0 6px var(--featuredrop-glow, rgba(245, 158, 11, 0.6))",
|
|
134
|
+
animation: "featuredrop-pulse 2s ease-in-out infinite"
|
|
135
|
+
};
|
|
136
|
+
var countStyles = {
|
|
137
|
+
...baseStyles,
|
|
138
|
+
minWidth: "var(--featuredrop-count-size, 18px)",
|
|
139
|
+
height: "var(--featuredrop-count-size, 18px)",
|
|
140
|
+
padding: "0 4px",
|
|
141
|
+
borderRadius: "9999px",
|
|
142
|
+
fontSize: "var(--featuredrop-font-size, 11px)",
|
|
143
|
+
fontWeight: 700,
|
|
144
|
+
lineHeight: 1,
|
|
145
|
+
color: "var(--featuredrop-count-color, white)",
|
|
146
|
+
backgroundColor: "var(--featuredrop-count-bg, #f59e0b)"
|
|
147
|
+
};
|
|
148
|
+
function NewBadge({
|
|
149
|
+
variant = "pill",
|
|
150
|
+
show = true,
|
|
151
|
+
count,
|
|
152
|
+
label = "New",
|
|
153
|
+
onDismiss,
|
|
154
|
+
dismissOnClick = false,
|
|
155
|
+
className,
|
|
156
|
+
style,
|
|
157
|
+
children
|
|
158
|
+
}) {
|
|
159
|
+
if (children) {
|
|
160
|
+
return /* @__PURE__ */ jsx(Fragment, { children: children({ isNew: show }) });
|
|
161
|
+
}
|
|
162
|
+
if (!show) return null;
|
|
163
|
+
const handleClick = dismissOnClick && onDismiss ? onDismiss : void 0;
|
|
164
|
+
const variantStyles = variant === "dot" ? dotStyles : variant === "count" ? countStyles : pillStyles;
|
|
165
|
+
const content = variant === "dot" ? null : variant === "count" ? count ?? 0 : label;
|
|
166
|
+
const ariaLabel = variant === "count" ? `${count ?? 0} new features` : "New feature";
|
|
167
|
+
return /* @__PURE__ */ jsx(
|
|
168
|
+
"span",
|
|
169
|
+
{
|
|
170
|
+
"data-featuredrop": variant,
|
|
171
|
+
className,
|
|
172
|
+
style: { ...variantStyles, ...style },
|
|
173
|
+
onClick: handleClick,
|
|
174
|
+
role: dismissOnClick ? "button" : void 0,
|
|
175
|
+
tabIndex: dismissOnClick ? 0 : void 0,
|
|
176
|
+
onKeyDown: dismissOnClick && onDismiss ? (e) => {
|
|
177
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
onDismiss();
|
|
180
|
+
}
|
|
181
|
+
} : void 0,
|
|
182
|
+
"aria-label": ariaLabel,
|
|
183
|
+
children: content
|
|
184
|
+
}
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export { FeatureDropContext, FeatureDropProvider, NewBadge, useFeatureDrop, useNewCount, useNewFeature };
|
|
189
|
+
//# sourceMappingURL=react.js.map
|
|
190
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core.ts","../src/react/context.ts","../src/react/provider.tsx","../src/react/hooks/use-feature-drop.ts","../src/react/hooks/use-new-feature.ts","../src/react/hooks/use-new-count.ts","../src/react/components/new-badge.tsx"],"names":["isNew","jsx"],"mappings":";;;;;;AAUO,SAAS,MACd,OAAA,EACA,SAAA,EACA,cACA,GAAA,mBAAY,IAAI,MAAK,EACZ;AAET,EAAA,IAAI,YAAA,CAAa,GAAA,CAAI,OAAA,CAAQ,EAAE,GAAG,OAAO,KAAA;AAEzC,EAAA,MAAM,KAAA,GAAQ,IAAI,OAAA,EAAQ;AAC1B,EAAA,MAAM,cAAc,IAAI,IAAA,CAAK,OAAA,CAAQ,YAAY,EAAE,OAAA,EAAQ;AAG3D,EAAA,IAAI,KAAA,IAAS,aAAa,OAAO,KAAA;AAGjC,EAAA,IAAI,SAAA,EAAW;AACb,IAAA,MAAM,WAAA,GAAc,IAAI,IAAA,CAAK,SAAS,EAAE,OAAA,EAAQ;AAChD,IAAA,MAAM,aAAa,IAAI,IAAA,CAAK,OAAA,CAAQ,UAAU,EAAE,OAAA,EAAQ;AACxD,IAAA,IAAI,UAAA,IAAc,aAAa,OAAO,KAAA;AAAA,EACxC;AAEA,EAAA,OAAO,IAAA;AACT;AAKO,SAAS,eACd,QAAA,EACA,OAAA,EACA,GAAA,mBAAY,IAAI,MAAK,EACL;AAChB,EAAA,MAAM,SAAA,GAAY,QAAQ,YAAA,EAAa;AACvC,EAAA,MAAM,YAAA,GAAe,QAAQ,eAAA,EAAgB;AAC7C,EAAA,OAAO,QAAA,CAAS,OAAO,CAAC,CAAA,KAAM,MAAM,CAAA,EAAG,SAAA,EAAW,YAAA,EAAc,GAAG,CAAC,CAAA;AACtE;AAgBO,SAAS,cACd,QAAA,EACA,UAAA,EACA,SACA,GAAA,mBAAY,IAAI,MAAK,EACZ;AACT,EAAA,MAAM,SAAA,GAAY,QAAQ,YAAA,EAAa;AACvC,EAAA,MAAM,YAAA,GAAe,QAAQ,eAAA,EAAgB;AAC7C,EAAA,OAAO,QAAA,CAAS,IAAA;AAAA,IACd,CAAC,MAAM,CAAA,CAAE,UAAA,KAAe,cAAc,KAAA,CAAM,CAAA,EAAG,SAAA,EAAW,YAAA,EAAc,GAAG;AAAA,GAC7E;AACF;ACvDO,IAAM,kBAAA,GAAqB,aAAA;AAAA,EAChC;AACF;ACDO,SAAS,mBAAA,CAAoB;AAAA,EAClC,QAAA;AAAA,EACA,OAAA;AAAA,EACA;AACF,CAAA,EAA6B;AAC3B,EAAA,MAAM,CAAC,WAAA,EAAa,cAAc,CAAA,GAAI,QAAA;AAAA,IAAS,MAC7C,cAAA,CAAe,QAAA,EAAU,OAAO;AAAA,GAClC;AAEA,EAAA,MAAM,SAAA,GAAY,YAAY,MAAM;AAClC,IAAA,cAAA,CAAe,cAAA,CAAe,QAAA,EAAU,OAAO,CAAC,CAAA;AAAA,EAClD,CAAA,EAAG,CAAC,QAAA,EAAU,OAAO,CAAC,CAAA;AAEtB,EAAA,MAAM,OAAA,GAAU,WAAA;AAAA,IACd,CAAC,EAAA,KAAe;AACd,MAAA,OAAA,CAAQ,QAAQ,EAAE,CAAA;AAClB,MAAA,SAAA,EAAU;AAAA,IACZ,CAAA;AAAA,IACA,CAAC,SAAS,SAAS;AAAA,GACrB;AAEA,EAAA,MAAM,UAAA,GAAa,YAAY,YAAY;AACzC,IAAA,MAAM,OAAA,CAAQ,UAAA,iBAAW,IAAI,IAAA,EAAM,CAAA;AACnC,IAAA,cAAA,CAAe,EAAE,CAAA;AAAA,EACnB,CAAA,EAAG,CAAC,OAAO,CAAC,CAAA;AAEZ,EAAA,MAAM,OAAA,GAAU,WAAA;AAAA,IACd,CAAC,UAAA,KAAuB,aAAA,CAAc,QAAA,EAAU,YAAY,OAAO,CAAA;AAAA,IACnE,CAAC,UAAU,OAAO;AAAA,GACpB;AAEA,EAAA,MAAM,UAAA,GAAa,WAAA;AAAA,IACjB,CAAC,eACC,WAAA,CAAY,IAAA,CAAK,CAAC,CAAA,KAAM,CAAA,CAAE,eAAe,UAAU,CAAA;AAAA,IACrD,CAAC,WAAW;AAAA,GACd;AAEA,EAAA,MAAM,KAAA,GAAQ,OAAA;AAAA,IACZ,OAAO;AAAA,MACL,WAAA;AAAA,MACA,UAAU,WAAA,CAAY,MAAA;AAAA,MACtB,KAAA,EAAO,OAAA;AAAA,MACP,OAAA;AAAA,MACA,UAAA;AAAA,MACA;AAAA,KACF,CAAA;AAAA,IACA,CAAC,WAAA,EAAa,OAAA,EAAS,OAAA,EAAS,YAAY,UAAU;AAAA,GACxD;AAEA,EAAA,uBACE,GAAA,CAAC,kBAAA,CAAmB,QAAA,EAAnB,EAA4B,OAC1B,QAAA,EACH,CAAA;AAEJ;AC9DO,SAAS,cAAA,GAA0C;AACxD,EAAA,MAAM,OAAA,GAAU,WAAW,kBAAkB,CAAA;AAC7C,EAAA,IAAI,CAAC,OAAA,EAAS;AACZ,IAAA,MAAM,IAAI,KAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACA,EAAA,OAAO,OAAA;AACT;;;ACDO,SAAS,cAAc,UAAA,EAAyC;AACrE,EAAA,MAAM,EAAE,KAAA,EAAAA,MAAAA,EAAO,UAAA,EAAY,OAAA,KAAY,cAAA,EAAe;AAEtD,EAAA,MAAM,OAAA,GAAU,WAAW,UAAU,CAAA;AACrC,EAAA,MAAM,UAAA,GAAaA,OAAM,UAAU,CAAA;AAEnC,EAAA,OAAO;AAAA,IACL,KAAA,EAAO,UAAA;AAAA,IACP,OAAA;AAAA,IACA,SAAS,MAAM;AACb,MAAA,IAAI,OAAA,EAAS;AACX,QAAA,OAAA,CAAQ,QAAQ,EAAE,CAAA;AAAA,MACpB;AAAA,IACF;AAAA,GACF;AACF;;;ACxBO,SAAS,WAAA,GAAsB;AACpC,EAAA,MAAM,EAAE,QAAA,EAAS,GAAI,cAAA,EAAe;AACpC,EAAA,OAAO,QAAA;AACT;ACgBA,IAAM,UAAA,GAA4B;AAAA,EAChC,OAAA,EAAS,aAAA;AAAA,EACT,UAAA,EAAY,QAAA;AAAA,EACZ,cAAA,EAAgB,QAAA;AAAA,EAChB,UAAA,EAAY;AACd,CAAA;AAEA,IAAM,UAAA,GAA4B;AAAA,EAChC,GAAG,UAAA;AAAA,EACH,OAAA,EAAS,SAAA;AAAA,EACT,YAAA,EAAc,QAAA;AAAA,EACd,QAAA,EAAU,oCAAA;AAAA,EACV,UAAA,EAAY,GAAA;AAAA,EACZ,aAAA,EAAe,WAAA;AAAA,EACf,aAAA,EAAe,QAAA;AAAA,EACf,UAAA,EAAY,CAAA;AAAA,EACZ,KAAA,EAAO,mCAAA;AAAA,EACP,eAAA,EAAiB;AACnB,CAAA;AAEA,IAAM,SAAA,GAA2B;AAAA,EAC/B,GAAG,UAAA;AAAA,EACH,KAAA,EAAO,kCAAA;AAAA,EACP,MAAA,EAAQ,kCAAA;AAAA,EACR,YAAA,EAAc,QAAA;AAAA,EACd,eAAA,EAAiB,mCAAA;AAAA,EACjB,SAAA,EAAW,0DAAA;AAAA,EACX,SAAA,EAAW;AACb,CAAA;AAEA,IAAM,WAAA,GAA6B;AAAA,EACjC,GAAG,UAAA;AAAA,EACH,QAAA,EAAU,qCAAA;AAAA,EACV,MAAA,EAAQ,qCAAA;AAAA,EACR,OAAA,EAAS,OAAA;AAAA,EACT,YAAA,EAAc,QAAA;AAAA,EACd,QAAA,EAAU,oCAAA;AAAA,EACV,UAAA,EAAY,GAAA;AAAA,EACZ,UAAA,EAAY,CAAA;AAAA,EACZ,KAAA,EAAO,uCAAA;AAAA,EACP,eAAA,EAAiB;AACnB,CAAA;AAiBO,SAAS,QAAA,CAAS;AAAA,EACvB,OAAA,GAAU,MAAA;AAAA,EACV,IAAA,GAAO,IAAA;AAAA,EACP,KAAA;AAAA,EACA,KAAA,GAAQ,KAAA;AAAA,EACR,SAAA;AAAA,EACA,cAAA,GAAiB,KAAA;AAAA,EACjB,SAAA;AAAA,EACA,KAAA;AAAA,EACA;AACF,CAAA,EAAkB;AAEhB,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,uBAAOC,IAAA,QAAA,EAAA,EAAG,QAAA,EAAA,QAAA,CAAS,EAAE,KAAA,EAAO,IAAA,EAAM,CAAA,EAAE,CAAA;AAAA,EACtC;AAEA,EAAA,IAAI,CAAC,MAAM,OAAO,IAAA;AAElB,EAAA,MAAM,WAAA,GAAc,cAAA,IAAkB,SAAA,GAAY,SAAA,GAAY,MAAA;AAE9D,EAAA,MAAM,gBACJ,OAAA,KAAY,KAAA,GACR,SAAA,GACA,OAAA,KAAY,UACV,WAAA,GACA,UAAA;AAER,EAAA,MAAM,UACJ,OAAA,KAAY,KAAA,GACR,OACA,OAAA,KAAY,OAAA,GACT,SAAS,CAAA,GACV,KAAA;AAER,EAAA,MAAM,YACJ,OAAA,KAAY,OAAA,GACR,CAAA,EAAG,KAAA,IAAS,CAAC,CAAA,aAAA,CAAA,GACb,aAAA;AAEN,EAAA,uBACEA,GAAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MACC,kBAAA,EAAkB,OAAA;AAAA,MAClB,SAAA;AAAA,MACA,KAAA,EAAO,EAAE,GAAG,aAAA,EAAe,GAAG,KAAA,EAAM;AAAA,MACpC,OAAA,EAAS,WAAA;AAAA,MACT,IAAA,EAAM,iBAAiB,QAAA,GAAW,MAAA;AAAA,MAClC,QAAA,EAAU,iBAAiB,CAAA,GAAI,MAAA;AAAA,MAC/B,SAAA,EACE,cAAA,IAAkB,SAAA,GACd,CAAC,CAAA,KAAM;AACL,QAAA,IAAI,CAAA,CAAE,GAAA,KAAQ,OAAA,IAAW,CAAA,CAAE,QAAQ,GAAA,EAAK;AACtC,UAAA,CAAA,CAAE,cAAA,EAAe;AACjB,UAAA,SAAA,EAAU;AAAA,QACZ;AAAA,MACF,CAAA,GACA,MAAA;AAAA,MAEN,YAAA,EAAY,SAAA;AAAA,MAEX,QAAA,EAAA;AAAA;AAAA,GACH;AAEJ","file":"react.js","sourcesContent":["import type { FeatureEntry, FeatureManifest, StorageAdapter } from \"./types\";\n\n/**\n * Check if a single feature should show as \"new\".\n *\n * A feature is \"new\" when ALL of these are true:\n * 1. Current time is before `showNewUntil`\n * 2. Feature was released after the watermark (or no watermark exists)\n * 3. Feature has not been individually dismissed\n */\nexport function isNew(\n feature: FeatureEntry,\n watermark: string | null,\n dismissedIds: ReadonlySet<string>,\n now: Date = new Date(),\n): boolean {\n // Already dismissed by the user on this device\n if (dismissedIds.has(feature.id)) return false;\n\n const nowMs = now.getTime();\n const showUntilMs = new Date(feature.showNewUntil).getTime();\n\n // Past the display window\n if (nowMs >= showUntilMs) return false;\n\n // If there's a watermark, feature must have been released after it\n if (watermark) {\n const watermarkMs = new Date(watermark).getTime();\n const releasedMs = new Date(feature.releasedAt).getTime();\n if (releasedMs <= watermarkMs) return false;\n }\n\n return true;\n}\n\n/**\n * Get all features that are currently \"new\" for this user.\n */\nexport function getNewFeatures(\n manifest: FeatureManifest,\n storage: StorageAdapter,\n now: Date = new Date(),\n): FeatureEntry[] {\n const watermark = storage.getWatermark();\n const dismissedIds = storage.getDismissedIds();\n return manifest.filter((f) => isNew(f, watermark, dismissedIds, now));\n}\n\n/**\n * Get the count of new features.\n */\nexport function getNewFeatureCount(\n manifest: FeatureManifest,\n storage: StorageAdapter,\n now: Date = new Date(),\n): number {\n return getNewFeatures(manifest, storage, now).length;\n}\n\n/**\n * Check if a specific sidebar key has a new feature.\n */\nexport function hasNewFeature(\n manifest: FeatureManifest,\n sidebarKey: string,\n storage: StorageAdapter,\n now: Date = new Date(),\n): boolean {\n const watermark = storage.getWatermark();\n const dismissedIds = storage.getDismissedIds();\n return manifest.some(\n (f) => f.sidebarKey === sidebarKey && isNew(f, watermark, dismissedIds, now),\n );\n}\n","import { createContext } from \"react\";\nimport type { FeatureEntry } from \"../types\";\n\nexport interface FeatureDropContextValue {\n /** All currently \"new\" features */\n newFeatures: FeatureEntry[];\n /** Count of new features */\n newCount: number;\n /** Check if a sidebar key has any new features */\n isNew: (sidebarKey: string) => boolean;\n /** Dismiss a single feature by ID */\n dismiss: (id: string) => void;\n /** Dismiss all features (marks all as seen) */\n dismissAll: () => Promise<void>;\n /** Get the feature entry for a sidebar key (if it's new) */\n getFeature: (sidebarKey: string) => FeatureEntry | undefined;\n}\n\nexport const FeatureDropContext = createContext<FeatureDropContextValue | null>(\n null,\n);\n","import { useState, useCallback, useMemo, type ReactNode } from \"react\";\nimport type { FeatureManifest, StorageAdapter } from \"../types\";\nimport { getNewFeatures, hasNewFeature } from \"../core\";\nimport { FeatureDropContext } from \"./context\";\n\nexport interface FeatureDropProviderProps {\n /** The feature manifest — typically a frozen array of FeatureEntry objects */\n manifest: FeatureManifest;\n /** Storage adapter instance (e.g. LocalStorageAdapter, MemoryAdapter) */\n storage: StorageAdapter;\n children: ReactNode;\n}\n\n/**\n * Provides feature discovery state to the component tree.\n *\n * Wrap your app (or a subtree) with this provider to enable\n * `useFeatureDrop`, `useNewFeature`, and `useNewCount` hooks.\n */\nexport function FeatureDropProvider({\n manifest,\n storage,\n children,\n}: FeatureDropProviderProps) {\n const [newFeatures, setNewFeatures] = useState(() =>\n getNewFeatures(manifest, storage),\n );\n\n const recompute = useCallback(() => {\n setNewFeatures(getNewFeatures(manifest, storage));\n }, [manifest, storage]);\n\n const dismiss = useCallback(\n (id: string) => {\n storage.dismiss(id);\n recompute();\n },\n [storage, recompute],\n );\n\n const dismissAll = useCallback(async () => {\n await storage.dismissAll(new Date());\n setNewFeatures([]);\n }, [storage]);\n\n const isNewFn = useCallback(\n (sidebarKey: string) => hasNewFeature(manifest, sidebarKey, storage),\n [manifest, storage],\n );\n\n const getFeature = useCallback(\n (sidebarKey: string) =>\n newFeatures.find((f) => f.sidebarKey === sidebarKey),\n [newFeatures],\n );\n\n const value = useMemo(\n () => ({\n newFeatures,\n newCount: newFeatures.length,\n isNew: isNewFn,\n dismiss,\n dismissAll,\n getFeature,\n }),\n [newFeatures, isNewFn, dismiss, dismissAll, getFeature],\n );\n\n return (\n <FeatureDropContext.Provider value={value}>\n {children}\n </FeatureDropContext.Provider>\n );\n}\n","import { useContext } from \"react\";\nimport { FeatureDropContext } from \"../context\";\nimport type { FeatureDropContextValue } from \"../context\";\n\n/**\n * Access the full feature discovery context.\n *\n * Returns: `{ newFeatures, newCount, isNew, dismiss, dismissAll, getFeature }`\n *\n * @throws Error if used outside of `<FeatureDropProvider>`\n */\nexport function useFeatureDrop(): FeatureDropContextValue {\n const context = useContext(FeatureDropContext);\n if (!context) {\n throw new Error(\n \"useFeatureDrop must be used within a <FeatureDropProvider>\",\n );\n }\n return context;\n}\n","import { useFeatureDrop } from \"./use-feature-drop\";\nimport type { FeatureEntry } from \"../../types\";\n\nexport interface UseNewFeatureResult {\n /** Whether this sidebar key has a new feature */\n isNew: boolean;\n /** The feature entry, if new */\n feature: FeatureEntry | undefined;\n /** Dismiss the feature for this sidebar key */\n dismiss: () => void;\n}\n\n/**\n * Check if a single navigation item has a new feature.\n *\n * @param sidebarKey - The key to check (e.g. \"/journal\", \"settings\")\n * @returns `{ isNew, feature, dismiss }`\n */\nexport function useNewFeature(sidebarKey: string): UseNewFeatureResult {\n const { isNew, getFeature, dismiss } = useFeatureDrop();\n\n const feature = getFeature(sidebarKey);\n const isNewValue = isNew(sidebarKey);\n\n return {\n isNew: isNewValue,\n feature,\n dismiss: () => {\n if (feature) {\n dismiss(feature.id);\n }\n },\n };\n}\n","import { useFeatureDrop } from \"./use-feature-drop\";\n\n/**\n * Get the count of currently new features.\n *\n * Useful for rendering a badge count on a \"What's New\" button.\n *\n * @returns The number of new features\n */\nexport function useNewCount(): number {\n const { newCount } = useFeatureDrop();\n return newCount;\n}\n","import type { ReactNode, CSSProperties } from \"react\";\n\nexport interface NewBadgeRenderProps {\n /** Whether the feature is currently new */\n isNew: boolean;\n}\n\nexport interface NewBadgeProps {\n /** Display variant */\n variant?: \"pill\" | \"dot\" | \"count\";\n /** Whether to show the badge (typically from `useNewFeature().isNew`) */\n show?: boolean;\n /** Count to display when variant is \"count\" */\n count?: number;\n /** Text label for the pill variant. Default: \"New\" */\n label?: string;\n /** Dismiss callback. If set with `dismissOnClick`, clicking dismisses. */\n onDismiss?: () => void;\n /** Whether clicking the badge should trigger onDismiss */\n dismissOnClick?: boolean;\n /** Additional CSS class */\n className?: string;\n /** Additional inline styles (merged with defaults) */\n style?: CSSProperties;\n /** Render prop for full customization */\n children?: (props: NewBadgeRenderProps) => ReactNode;\n}\n\nconst baseStyles: CSSProperties = {\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n fontFamily: \"inherit\",\n};\n\nconst pillStyles: CSSProperties = {\n ...baseStyles,\n padding: \"2px 6px\",\n borderRadius: \"9999px\",\n fontSize: \"var(--featuredrop-font-size, 10px)\",\n fontWeight: 700,\n textTransform: \"uppercase\" as const,\n letterSpacing: \"0.05em\",\n lineHeight: 1,\n color: \"var(--featuredrop-color, #b45309)\",\n backgroundColor: \"var(--featuredrop-bg, rgba(245, 158, 11, 0.15))\",\n};\n\nconst dotStyles: CSSProperties = {\n ...baseStyles,\n width: \"var(--featuredrop-dot-size, 8px)\",\n height: \"var(--featuredrop-dot-size, 8px)\",\n borderRadius: \"9999px\",\n backgroundColor: \"var(--featuredrop-color, #f59e0b)\",\n boxShadow: \"0 0 6px var(--featuredrop-glow, rgba(245, 158, 11, 0.6))\",\n animation: \"featuredrop-pulse 2s ease-in-out infinite\",\n};\n\nconst countStyles: CSSProperties = {\n ...baseStyles,\n minWidth: \"var(--featuredrop-count-size, 18px)\",\n height: \"var(--featuredrop-count-size, 18px)\",\n padding: \"0 4px\",\n borderRadius: \"9999px\",\n fontSize: \"var(--featuredrop-font-size, 11px)\",\n fontWeight: 700,\n lineHeight: 1,\n color: \"var(--featuredrop-count-color, white)\",\n backgroundColor: \"var(--featuredrop-count-bg, #f59e0b)\",\n};\n\n/**\n * Headless \"New\" badge component.\n *\n * Styled via CSS custom properties — zero CSS framework dependency:\n * - `--featuredrop-color` — text/dot color\n * - `--featuredrop-bg` — pill background\n * - `--featuredrop-font-size` — font size\n * - `--featuredrop-dot-size` — dot diameter\n * - `--featuredrop-glow` — dot glow color\n * - `--featuredrop-count-size` — count badge size\n * - `--featuredrop-count-color` — count text color\n * - `--featuredrop-count-bg` — count background\n *\n * Use `data-featuredrop` attribute for CSS selector styling.\n */\nexport function NewBadge({\n variant = \"pill\",\n show = true,\n count,\n label = \"New\",\n onDismiss,\n dismissOnClick = false,\n className,\n style,\n children,\n}: NewBadgeProps) {\n // Render prop mode\n if (children) {\n return <>{children({ isNew: show })}</>;\n }\n\n if (!show) return null;\n\n const handleClick = dismissOnClick && onDismiss ? onDismiss : undefined;\n\n const variantStyles =\n variant === \"dot\"\n ? dotStyles\n : variant === \"count\"\n ? countStyles\n : pillStyles;\n\n const content =\n variant === \"dot\"\n ? null\n : variant === \"count\"\n ? (count ?? 0)\n : label;\n\n const ariaLabel =\n variant === \"count\"\n ? `${count ?? 0} new features`\n : \"New feature\";\n\n return (\n <span\n data-featuredrop={variant}\n className={className}\n style={{ ...variantStyles, ...style }}\n onClick={handleClick}\n role={dismissOnClick ? \"button\" : undefined}\n tabIndex={dismissOnClick ? 0 : undefined}\n onKeyDown={\n dismissOnClick && onDismiss\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onDismiss();\n }\n }\n : undefined\n }\n aria-label={ariaLabel}\n >\n {content}\n </span>\n );\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "featuredrop",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Lightweight feature discovery system. Show 'New' badges that auto-expire.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"./react": {
|
|
21
|
+
"import": {
|
|
22
|
+
"types": "./dist/react.d.ts",
|
|
23
|
+
"default": "./dist/react.js"
|
|
24
|
+
},
|
|
25
|
+
"require": {
|
|
26
|
+
"types": "./dist/react.d.cts",
|
|
27
|
+
"default": "./dist/react.cjs"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"typesVersions": {
|
|
32
|
+
"*": {
|
|
33
|
+
"react": [
|
|
34
|
+
"./dist/react.d.ts"
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist/**/*.js",
|
|
40
|
+
"dist/**/*.cjs",
|
|
41
|
+
"dist/**/*.d.ts",
|
|
42
|
+
"dist/**/*.d.cts",
|
|
43
|
+
"dist/**/*.map",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"sideEffects": false,
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "tsup",
|
|
50
|
+
"dev": "tsup --watch",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
53
|
+
"test:coverage": "vitest run --coverage",
|
|
54
|
+
"typecheck": "tsc --noEmit",
|
|
55
|
+
"lint": "tsc --noEmit",
|
|
56
|
+
"clean": "rm -rf dist coverage",
|
|
57
|
+
"prepublishOnly": "pnpm run build"
|
|
58
|
+
},
|
|
59
|
+
"keywords": [
|
|
60
|
+
"feature-discovery",
|
|
61
|
+
"new-badge",
|
|
62
|
+
"whatsnew",
|
|
63
|
+
"feature-flags",
|
|
64
|
+
"sidebar",
|
|
65
|
+
"badge",
|
|
66
|
+
"feature-announcement",
|
|
67
|
+
"whats-new",
|
|
68
|
+
"product-updates",
|
|
69
|
+
"typescript",
|
|
70
|
+
"react"
|
|
71
|
+
],
|
|
72
|
+
"author": "Glincker <hello@glincker.com>",
|
|
73
|
+
"license": "MIT",
|
|
74
|
+
"repository": {
|
|
75
|
+
"type": "git",
|
|
76
|
+
"url": "https://github.com/GLINCKER/featuredrop"
|
|
77
|
+
},
|
|
78
|
+
"homepage": "https://github.com/GLINCKER/featuredrop#readme",
|
|
79
|
+
"bugs": {
|
|
80
|
+
"url": "https://github.com/GLINCKER/featuredrop/issues"
|
|
81
|
+
},
|
|
82
|
+
"engines": {
|
|
83
|
+
"node": ">=18"
|
|
84
|
+
},
|
|
85
|
+
"peerDependencies": {
|
|
86
|
+
"react": ">=18"
|
|
87
|
+
},
|
|
88
|
+
"peerDependenciesMeta": {
|
|
89
|
+
"react": {
|
|
90
|
+
"optional": true
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
"devDependencies": {
|
|
94
|
+
"@commitlint/cli": "^19.0.0",
|
|
95
|
+
"@commitlint/config-conventional": "^19.0.0",
|
|
96
|
+
"@testing-library/jest-dom": "^6.0.0",
|
|
97
|
+
"@testing-library/react": "^16.0.0",
|
|
98
|
+
"@testing-library/user-event": "^14.6.1",
|
|
99
|
+
"@types/node": "^22.0.0",
|
|
100
|
+
"@types/react": "^19.0.0",
|
|
101
|
+
"@vitest/coverage-v8": "^2.1.0",
|
|
102
|
+
"husky": "^9.0.0",
|
|
103
|
+
"jsdom": "^25.0.0",
|
|
104
|
+
"react": "^19.0.0",
|
|
105
|
+
"react-dom": "^19.0.0",
|
|
106
|
+
"tsup": "^8.3.0",
|
|
107
|
+
"typescript": "^5.7.0",
|
|
108
|
+
"vitest": "^2.1.0"
|
|
109
|
+
}
|
|
110
|
+
}
|