@designa8/vibedesign-stripe-deepseek-2-176b 0.2.0 → 0.2.1
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @designa8/vibedesign-stripe-deepseek-2-176b
|
|
2
2
|
|
|
3
|
-
Complete design system generated by Atoms Design, version **0.2.
|
|
3
|
+
Complete design system generated by Atoms Design, version **0.2.1**.
|
|
4
4
|
Tokens, atoms, molecules, organisms, and templates — import from the package root;
|
|
5
5
|
never hardcode visual values in consumer code.
|
|
6
6
|
|
package/package.json
CHANGED
|
@@ -40,6 +40,9 @@ export interface NotificationFeedMode {
|
|
|
40
40
|
label: string;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** `card` draws the bordered surface; `bare` fills its parent (e.g. a Drawer body). */
|
|
44
|
+
export type NotificationFeedChrome = "card" | "bare";
|
|
45
|
+
|
|
43
46
|
export interface NotificationFeedProps
|
|
44
47
|
extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "title"> {
|
|
45
48
|
/** Feed heading. */
|
|
@@ -66,6 +69,19 @@ export interface NotificationFeedProps
|
|
|
66
69
|
feedLabel?: string;
|
|
67
70
|
/** Shown when the active filter has no rows. */
|
|
68
71
|
emptyMessage?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Fires with the clicked row when its action link is followed. Unless a
|
|
74
|
+
* modifier key is held, the browser navigation is prevented so an SPA router
|
|
75
|
+
* can take over (and the consumer can mark the row read).
|
|
76
|
+
*/
|
|
77
|
+
onItemClick?: (
|
|
78
|
+
item: NotificationFeedItem,
|
|
79
|
+
event: React.MouseEvent<HTMLAnchorElement>,
|
|
80
|
+
) => void;
|
|
81
|
+
/** Right-hand slot on the title row — e.g. a "Mark all read" button. */
|
|
82
|
+
actions?: React.ReactNode;
|
|
83
|
+
/** Outer surface treatment. `bare` drops the card's border, radius, shadow and padding. */
|
|
84
|
+
chrome?: NotificationFeedChrome;
|
|
69
85
|
}
|
|
70
86
|
|
|
71
87
|
const defaultFilters: NotificationFeedFilter[] = [
|
|
@@ -149,10 +165,12 @@ const defaultItems: NotificationFeedItem[] = [
|
|
|
149
165
|
];
|
|
150
166
|
|
|
151
167
|
/**
|
|
152
|
-
* A notifications timeline: heading
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
168
|
+
* A notifications timeline: heading with an optional right-hand action slot, a
|
|
169
|
+
* row of filter pills, a mode switch, and one bordered card whose rows are
|
|
170
|
+
* threaded on a hairline rail. The rail leaves each row from its vertical
|
|
171
|
+
* centre, the node is indigo while the row is unread, and the unread row
|
|
172
|
+
* carries the brand accent bar at the card's left edge. Pass `chrome="bare"` to
|
|
173
|
+
* drop the card so the feed fills a Drawer body.
|
|
156
174
|
*/
|
|
157
175
|
export const NotificationFeed = React.forwardRef<
|
|
158
176
|
HTMLDivElement,
|
|
@@ -162,7 +180,7 @@ export const NotificationFeed = React.forwardRef<
|
|
|
162
180
|
{
|
|
163
181
|
title = "Notification center / feed",
|
|
164
182
|
filters = defaultFilters,
|
|
165
|
-
items
|
|
183
|
+
items,
|
|
166
184
|
filter,
|
|
167
185
|
defaultFilter = "all",
|
|
168
186
|
onFilterChange,
|
|
@@ -172,6 +190,9 @@ export const NotificationFeed = React.forwardRef<
|
|
|
172
190
|
onModeChange,
|
|
173
191
|
feedLabel = "Notifications",
|
|
174
192
|
emptyMessage = "Nothing here yet.",
|
|
193
|
+
onItemClick,
|
|
194
|
+
actions,
|
|
195
|
+
chrome = "card",
|
|
175
196
|
className,
|
|
176
197
|
...rest
|
|
177
198
|
},
|
|
@@ -196,17 +217,69 @@ export const NotificationFeed = React.forwardRef<
|
|
|
196
217
|
onModeChange?.(next);
|
|
197
218
|
};
|
|
198
219
|
|
|
199
|
-
|
|
220
|
+
// An explicitly passed array — including an empty one — always wins, so a
|
|
221
|
+
// consumer with no notifications sees the empty state, never the demo rows.
|
|
222
|
+
const rows = items ?? defaultItems;
|
|
223
|
+
|
|
224
|
+
const visible = rows.filter(
|
|
200
225
|
(item) =>
|
|
201
226
|
activeFilter === "all" ||
|
|
202
227
|
item.filter === undefined ||
|
|
203
228
|
item.filter === activeFilter,
|
|
204
229
|
);
|
|
205
230
|
|
|
231
|
+
const unreadCount = visible.filter((item) => item.unread).length;
|
|
232
|
+
|
|
233
|
+
const isBare = chrome === "bare";
|
|
234
|
+
|
|
235
|
+
const handleItemClick = (
|
|
236
|
+
item: NotificationFeedItem,
|
|
237
|
+
event: React.MouseEvent<HTMLAnchorElement>,
|
|
238
|
+
) => {
|
|
239
|
+
if (!onItemClick) return;
|
|
240
|
+
// Let the browser handle new-tab / new-window intents.
|
|
241
|
+
if (
|
|
242
|
+
event.metaKey ||
|
|
243
|
+
event.altKey ||
|
|
244
|
+
event.ctrlKey ||
|
|
245
|
+
event.shiftKey ||
|
|
246
|
+
event.button !== 0
|
|
247
|
+
) {
|
|
248
|
+
onItemClick(item, event);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
event.preventDefault();
|
|
252
|
+
onItemClick(item, event);
|
|
253
|
+
};
|
|
254
|
+
|
|
206
255
|
return (
|
|
207
256
|
<div ref={ref} className={cn("w-full", className)} {...rest}>
|
|
208
257
|
<div className="flex w-full flex-col gap-lg">
|
|
209
|
-
<
|
|
258
|
+
<div className="flex w-full flex-wrap items-start justify-between gap-md">
|
|
259
|
+
<div className="flex min-w-0 items-center gap-sm">
|
|
260
|
+
<h2 className="text-display-lg text-ink">{title}</h2>
|
|
261
|
+
{unreadCount > 0 ? (
|
|
262
|
+
<span
|
|
263
|
+
className={cn(
|
|
264
|
+
"inline-flex shrink-0 items-center justify-center rounded-full bg-primary px-xs text-body-sm text-on-primary",
|
|
265
|
+
)}
|
|
266
|
+
style={tabular}
|
|
267
|
+
>
|
|
268
|
+
{unreadCount}
|
|
269
|
+
<span className="sr-only">
|
|
270
|
+
{" "}
|
|
271
|
+
unread {unreadCount === 1 ? "notification" : "notifications"}
|
|
272
|
+
</span>
|
|
273
|
+
</span>
|
|
274
|
+
) : null}
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
{actions ? (
|
|
278
|
+
<div className="ml-auto flex shrink-0 items-center gap-sm">
|
|
279
|
+
{actions}
|
|
280
|
+
</div>
|
|
281
|
+
) : null}
|
|
282
|
+
</div>
|
|
210
283
|
|
|
211
284
|
<div className="flex w-full flex-wrap items-center justify-between gap-md">
|
|
212
285
|
<div
|
|
@@ -240,10 +313,20 @@ export const NotificationFeed = React.forwardRef<
|
|
|
240
313
|
|
|
241
314
|
<section
|
|
242
315
|
aria-label={feedLabel}
|
|
243
|
-
className=
|
|
316
|
+
className={cn(
|
|
317
|
+
"w-full",
|
|
318
|
+
isBare
|
|
319
|
+
? "overflow-hidden"
|
|
320
|
+
: "overflow-hidden rounded-lg border border-hairline bg-canvas shadow-card",
|
|
321
|
+
)}
|
|
244
322
|
>
|
|
245
323
|
{visible.length === 0 ? (
|
|
246
|
-
<p
|
|
324
|
+
<p
|
|
325
|
+
className={cn(
|
|
326
|
+
"text-body-md text-muted",
|
|
327
|
+
isBare ? "py-xl" : "px-xxl py-xl",
|
|
328
|
+
)}
|
|
329
|
+
>
|
|
247
330
|
{emptyMessage}
|
|
248
331
|
</p>
|
|
249
332
|
) : (
|
|
@@ -314,6 +397,7 @@ export const NotificationFeed = React.forwardRef<
|
|
|
314
397
|
size="md"
|
|
315
398
|
underline="always"
|
|
316
399
|
className="whitespace-nowrap"
|
|
400
|
+
onClick={(event) => handleItemClick(item, event)}
|
|
317
401
|
>
|
|
318
402
|
{item.linkLabel ?? "View details"}
|
|
319
403
|
</Link>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import { Button } from "../Button";
|
|
2
3
|
import {
|
|
3
4
|
NotificationFeed,
|
|
4
5
|
type NotificationFeedItem,
|
|
5
6
|
} from "./NotificationFeed";
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const seedItems: NotificationFeedItem[] = [
|
|
8
9
|
{
|
|
9
10
|
id: "n-1",
|
|
10
11
|
title: "Payout paid",
|
|
@@ -44,15 +45,6 @@ const items: NotificationFeedItem[] = [
|
|
|
44
45
|
},
|
|
45
46
|
{
|
|
46
47
|
id: "n-5",
|
|
47
|
-
title: "Payout paid",
|
|
48
|
-
description: "$1,200.00 sent to Acme Store",
|
|
49
|
-
timestamp: "Jan 26, 09:12",
|
|
50
|
-
href: "#payout-5",
|
|
51
|
-
linkLabel: "View payout",
|
|
52
|
-
filter: "payouts",
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
id: "n-6",
|
|
56
48
|
title: "Payment received",
|
|
57
49
|
description: "$1,480.00 from Acme Supply",
|
|
58
50
|
timestamp: "Jan 26, 08:48",
|
|
@@ -62,7 +54,7 @@ const items: NotificationFeedItem[] = [
|
|
|
62
54
|
unread: true,
|
|
63
55
|
},
|
|
64
56
|
{
|
|
65
|
-
id: "n-
|
|
57
|
+
id: "n-6",
|
|
66
58
|
title: "Risk review opened",
|
|
67
59
|
description: "Case 4821 flagged for manual review",
|
|
68
60
|
timestamp: "Jan 25, 18:22",
|
|
@@ -75,26 +67,81 @@ const items: NotificationFeedItem[] = [
|
|
|
75
67
|
export default function NotificationFeedPreview() {
|
|
76
68
|
const [filter, setFilter] = React.useState("all");
|
|
77
69
|
const [mode, setMode] = React.useState("live");
|
|
70
|
+
const [drawerFilter, setDrawerFilter] = React.useState("all");
|
|
71
|
+
const [items, setItems] = React.useState(seedItems);
|
|
72
|
+
const [lastOpened, setLastOpened] = React.useState<string | null>(null);
|
|
73
|
+
|
|
74
|
+
const unreadCount = items.filter((item) => item.unread).length;
|
|
75
|
+
|
|
76
|
+
const markRead = (id: string) => {
|
|
77
|
+
setItems((current) =>
|
|
78
|
+
current.map((item) =>
|
|
79
|
+
item.id === id ? { ...item, unread: false } : item,
|
|
80
|
+
),
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const markAllRead = () => {
|
|
85
|
+
setItems((current) => current.map((item) => ({ ...item, unread: false })));
|
|
86
|
+
};
|
|
78
87
|
|
|
79
88
|
return (
|
|
80
89
|
<div className="w-full max-w-5xl mx-auto p-lg space-y-xl">
|
|
81
|
-
<
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
<div className="space-y-md">
|
|
91
|
+
<NotificationFeed
|
|
92
|
+
title="Notifications"
|
|
93
|
+
items={items}
|
|
94
|
+
filter={filter}
|
|
95
|
+
onFilterChange={setFilter}
|
|
96
|
+
mode={mode}
|
|
97
|
+
onModeChange={setMode}
|
|
98
|
+
actions={
|
|
99
|
+
<Button
|
|
100
|
+
variant="outline-soft"
|
|
101
|
+
size="sm"
|
|
102
|
+
onClick={markAllRead}
|
|
103
|
+
disabled={unreadCount === 0}
|
|
104
|
+
>
|
|
105
|
+
Mark all read
|
|
106
|
+
</Button>
|
|
107
|
+
}
|
|
108
|
+
onItemClick={(item) => {
|
|
109
|
+
setLastOpened(item.title);
|
|
110
|
+
markRead(item.id);
|
|
111
|
+
}}
|
|
112
|
+
/>
|
|
113
|
+
<p aria-live="polite" className="text-body-sm text-muted">
|
|
114
|
+
{lastOpened
|
|
115
|
+
? `Router would navigate to the ${lastOpened} detail view; the row is marked read.`
|
|
116
|
+
: "Click a row action — it marks that item read instead of navigating."}
|
|
117
|
+
</p>
|
|
118
|
+
</div>
|
|
88
119
|
|
|
89
120
|
<div className="rounded-lg border border-hairline bg-surface-soft p-lg">
|
|
90
|
-
<p className="mb-md text-caption font-weight-label
|
|
91
|
-
|
|
121
|
+
<p className="mb-md text-caption font-weight-label text-muted">
|
|
122
|
+
Bare chrome inside a drawer body
|
|
123
|
+
</p>
|
|
124
|
+
<div className="rounded-lg border border-hairline bg-canvas p-lg shadow-card">
|
|
125
|
+
<NotificationFeed
|
|
126
|
+
chrome="bare"
|
|
127
|
+
title="Notifications"
|
|
128
|
+
items={items.slice(0, 3)}
|
|
129
|
+
filter={drawerFilter}
|
|
130
|
+
onFilterChange={setDrawerFilter}
|
|
131
|
+
onItemClick={(item) => markRead(item.id)}
|
|
132
|
+
/>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<div className="space-y-md">
|
|
137
|
+
<p className="text-caption font-weight-label text-muted">
|
|
138
|
+
Explicit empty array — demo rows never leak in
|
|
92
139
|
</p>
|
|
93
140
|
<NotificationFeed
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
items={
|
|
97
|
-
emptyMessage="No
|
|
141
|
+
chrome="bare"
|
|
142
|
+
title="Notifications"
|
|
143
|
+
items={[]}
|
|
144
|
+
emptyMessage="No notifications yet — payouts and payments land here."
|
|
98
145
|
/>
|
|
99
146
|
</div>
|
|
100
147
|
</div>
|