@sarunyu/system-one 4.5.2 → 4.6.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/AGENTS.md +39 -4
- package/DESIGN.md +1 -0
- package/README.md +21 -0
- package/dist/index.cjs +146 -96
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +147 -97
- package/dist/index.js.map +1 -1
- package/dist/src/components/badge.d.ts.map +1 -1
- package/dist/src/components/checkbox.d.ts +4 -0
- package/dist/src/components/checkbox.d.ts.map +1 -1
- package/dist/src/components/notification.d.ts +6 -1
- package/dist/src/components/notification.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/style.css +1 -1
- package/llms.txt +45 -5
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -32,14 +32,35 @@ in this package.** This file is the short version: the rules you must follow.
|
|
|
32
32
|
`bg-background`, `bg-card`, `bg-muted`, `bg-primary-action`,
|
|
33
33
|
`border-border`, `border-divider`, etc. See `llms.txt` for the full table.
|
|
34
34
|
|
|
35
|
-
3. **
|
|
35
|
+
3. **No arbitrary bracket values for spacing/sizing/typography.** Use scale utilities only.
|
|
36
|
+
- **Forbidden**: `max-w-[1100px]`, `h-[317px]`, `min-h-[calc(100vh-64px)]`,
|
|
37
|
+
`w-[272px]`, `gap-[14px]`, `p-[10px]`, `text-[13px]`, `leading-[22px]`,
|
|
38
|
+
`rounded-[10px]`, `top-[7px]`.
|
|
39
|
+
- **Allowed**: scale values — `max-w-5xl` / `max-w-2xl`, `h-80` (=320px),
|
|
40
|
+
`w-64` (=256px), `gap-4`, `p-6`, `text-sm`, `leading-6`, `rounded-lg`,
|
|
41
|
+
`top-2`. The spacing scale is 4px-based: `0.5` (2px), `1` (4px), `2` (8px),
|
|
42
|
+
`3` (12px), `4` (16px), `5` (20px), `6` (24px), `8` (32px), `10` (40px),
|
|
43
|
+
`12` (48px), `16` (64px), `20` (80px), `24` (96px).
|
|
44
|
+
- **Exception — container widths only**: these specific arbitrary widths are
|
|
45
|
+
safelisted and may be used: `max-w-[480px]`, `max-w-[640px]`, `max-w-[720px]`,
|
|
46
|
+
`max-w-[800px]`, `max-w-[960px]`, `max-w-[1024px]`, `max-w-[1200px]`,
|
|
47
|
+
`max-w-[1280px]`, `max-w-[1440px]`. For other widths, pick the nearest
|
|
48
|
+
`max-w-{xs,sm,md,lg,xl,2xl,3xl,4xl,5xl,6xl,7xl}`.
|
|
49
|
+
- **Why**: the library's shipped `styles.css` only contains scale utilities
|
|
50
|
+
+ the safelisted arbitrary container widths. Any other `[...]` value needs
|
|
51
|
+
host-side Tailwind to compile — in a plain host (e.g. Claude Code / Cursor
|
|
52
|
+
vibe-coded Vite project without Tailwind setup) the class is a no-op and
|
|
53
|
+
layout collapses. If a design truly requires an odd value, snap to the
|
|
54
|
+
nearest scale step.
|
|
55
|
+
|
|
56
|
+
4. **Do not add text/font utility classes to `<h1>`–`<h4>`.** They are pre-styled.
|
|
36
57
|
Use them as-is.
|
|
37
58
|
|
|
38
|
-
|
|
59
|
+
5. **Layout is your job, with plain Tailwind.** The library has NO layout primitives.
|
|
39
60
|
Do not import `Page`, `Section`, `Stack`, `CardGrid`, `Toolbar` — they do not exist.
|
|
40
61
|
Build structure with `<div>` + `flex` / `grid` / `max-w-*` / `gap-*` / `p-*`.
|
|
41
62
|
|
|
42
|
-
|
|
63
|
+
6. **Component props are documented in `llms.txt` and in the `.d.ts` files.**
|
|
43
64
|
- `Input.onChange` receives `(value: string)`, not an event.
|
|
44
65
|
- `placeholder` IS the label (floats up on fill). Don't add a separate `<label>`.
|
|
45
66
|
- Checkbox/Radio take their `label` as a prop. Don't wrap them in `<label>`.
|
|
@@ -53,7 +74,7 @@ in this package.** This file is the short version: the rules you must follow.
|
|
|
53
74
|
- `Badge variant="notification"` — internal to `<Notification>`; never use standalone or attach your own `onClick` to it.
|
|
54
75
|
- `Notification` manages its own popover; pass `groups` (array of `{ label, items }`). It renders both the bell trigger and the panel. This is the only correct way to show a notification list.
|
|
55
76
|
|
|
56
|
-
|
|
77
|
+
7. **Mobile forms and action-heavy modals MUST use `<BottomSheet>`, not `<Modal>`.**
|
|
57
78
|
Login, signup, settings panels, profile editors, any multi-field form,
|
|
58
79
|
multi-step flow, long picker list, or action menu — on mobile (< 768px)
|
|
59
80
|
these render as `<BottomSheet>`. Only simple `variant="alert"` and short
|
|
@@ -75,6 +96,20 @@ import "@sarunyu/system-one/styles.css";
|
|
|
75
96
|
|
|
76
97
|
If the screen looks unstyled, this import is missing.
|
|
77
98
|
|
|
99
|
+
**If the host project has Tailwind (v4) in a separate CSS file, import this
|
|
100
|
+
library BEFORE Tailwind:**
|
|
101
|
+
|
|
102
|
+
```css
|
|
103
|
+
/* app/globals.css */
|
|
104
|
+
@import "@sarunyu/system-one/styles.css"; /* first */
|
|
105
|
+
@import "tailwindcss"; /* second */
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The library ships its CSS wrapped in `@layer system-one` so host utilities can
|
|
109
|
+
override library utilities — but that only works if host's Tailwind layer is
|
|
110
|
+
declared AFTER the library's. Reversing the import order makes library
|
|
111
|
+
utilities (`p-6`, `gap-4`, `max-w-*`, etc.) win over host-written ones.
|
|
112
|
+
|
|
78
113
|
## Dark mode
|
|
79
114
|
|
|
80
115
|
Add `.dark` to any ancestor (usually `<html>`). All tokens adapt automatically.
|
package/DESIGN.md
CHANGED
|
@@ -242,6 +242,7 @@ Use sparingly. Corporate UIs prefer border separation over heavy elevation.
|
|
|
242
242
|
| Keep whitespace controlled: `gap-4`–`gap-6` inside sections | Over-pad with `gap-10`+ inside cards |
|
|
243
243
|
| One `variant="primary"` Button per context | Two or more primary buttons side by side |
|
|
244
244
|
| Separate sections with `gap-12` or a `border-divider` line | Use heavy drop shadows to separate sections |
|
|
245
|
+
| Use scale values — `max-w-5xl`, `h-80`, `w-64`, `gap-4`, `p-6`, `text-sm` | Use arbitrary brackets — `max-w-[1100px]`, `h-[317px]`, `gap-[14px]`, `text-[13px]` (won't compile in hosts without Tailwind; see AGENTS.md rule 3) |
|
|
245
246
|
|
|
246
247
|
---
|
|
247
248
|
|
package/README.md
CHANGED
|
@@ -27,6 +27,27 @@ import "@sarunyu/system-one/styles.css";
|
|
|
27
27
|
|
|
28
28
|
No provider, no wrapper. Components ship with `"use client"`.
|
|
29
29
|
|
|
30
|
+
### Import order matters (if host has Tailwind)
|
|
31
|
+
|
|
32
|
+
The library's CSS is pre-wrapped in `@layer system-one` so host utilities can
|
|
33
|
+
override library utilities. For that to hold, import this library **before**
|
|
34
|
+
`tailwindcss` / your own utility CSS:
|
|
35
|
+
|
|
36
|
+
```css
|
|
37
|
+
/* app/globals.css — CORRECT */
|
|
38
|
+
@import "@sarunyu/system-one/styles.css";
|
|
39
|
+
@import "tailwindcss";
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```css
|
|
43
|
+
/* app/globals.css — WRONG (library utilities will beat yours) */
|
|
44
|
+
@import "tailwindcss";
|
|
45
|
+
@import "@sarunyu/system-one/styles.css";
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For hosts without Tailwind (Claude Code / Cursor vibe-coded projects), order
|
|
49
|
+
doesn't matter — unlayered host rules always beat the library's layered rules.
|
|
50
|
+
|
|
30
51
|
## Use
|
|
31
52
|
|
|
32
53
|
```tsx
|
package/dist/index.cjs
CHANGED
|
@@ -263,11 +263,10 @@ function Badge({
|
|
|
263
263
|
...props
|
|
264
264
|
}) {
|
|
265
265
|
const hasCount = count > 0;
|
|
266
|
-
const isActive = hasCount;
|
|
267
266
|
const resolvedNotificationState = notificationState ?? (hasCount ? "noti" : "default");
|
|
268
267
|
const notificationIsFilled = resolvedNotificationState === "active" || resolvedNotificationState === "noti";
|
|
269
268
|
const showNotificationDot = resolvedNotificationState === "noti" && hasCount;
|
|
270
|
-
const visualIcon = variant === "notification" ?
|
|
269
|
+
const visualIcon = variant === "notification" ? /* @__PURE__ */ jsxRuntime.jsx(react.BellSimple, { size: 18, weight: notificationIsFilled ? "fill" : "regular" }) : icon ?? /* @__PURE__ */ jsxRuntime.jsx(react.FunnelSimple, { size: 18, weight: "regular" });
|
|
271
270
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cn("relative inline-flex", className), children: [
|
|
272
271
|
variant === "notification" ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
273
272
|
Button,
|
|
@@ -275,10 +274,7 @@ function Badge({
|
|
|
275
274
|
"aria-label": "Notification",
|
|
276
275
|
size: "icon-xs",
|
|
277
276
|
variant: "plain-black",
|
|
278
|
-
className:
|
|
279
|
-
"text-subtle-text",
|
|
280
|
-
notificationIsFilled && "text-primary-action"
|
|
281
|
-
),
|
|
277
|
+
className: "text-icon-brand [&>span]:!h-[18px] [&>span]:!w-[18px]",
|
|
282
278
|
...props,
|
|
283
279
|
children: visualIcon
|
|
284
280
|
}
|
|
@@ -287,8 +283,8 @@ function Badge({
|
|
|
287
283
|
{
|
|
288
284
|
"aria-label": label,
|
|
289
285
|
size: "icon-md",
|
|
290
|
-
variant:
|
|
291
|
-
className: cn(
|
|
286
|
+
variant: hasCount ? "outline" : "outline-black",
|
|
287
|
+
className: cn(hasCount && "bg-primary-action-light border-primary-action-light"),
|
|
292
288
|
...props,
|
|
293
289
|
children: visualIcon
|
|
294
290
|
}
|
|
@@ -297,8 +293,8 @@ function Badge({
|
|
|
297
293
|
{
|
|
298
294
|
size: "md",
|
|
299
295
|
leftIcon: visualIcon,
|
|
300
|
-
variant:
|
|
301
|
-
className: cn(
|
|
296
|
+
variant: hasCount ? "outline" : "outline-black",
|
|
297
|
+
className: cn(hasCount && "bg-primary-action-light border-primary-action-light"),
|
|
302
298
|
...props,
|
|
303
299
|
children: label
|
|
304
300
|
}
|
|
@@ -307,10 +303,10 @@ function Badge({
|
|
|
307
303
|
"div",
|
|
308
304
|
{
|
|
309
305
|
className: cn(
|
|
310
|
-
"absolute flex items-center justify-center rounded-[60px]
|
|
311
|
-
variant === "notification" ? "-right-0.5 -top-0.5
|
|
306
|
+
"absolute flex h-[14px] min-h-[14px] min-w-[14px] items-center justify-center rounded-[60px]",
|
|
307
|
+
variant === "notification" ? "-right-0.5 -top-0.5 bg-destructive px-[2.5px]" : "-right-1 -top-[7px] h-4 min-w-4 bg-primary-action"
|
|
312
308
|
),
|
|
313
|
-
children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-center text-xs leading-4 text-
|
|
309
|
+
children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-center text-xs leading-4 font-normal text-text-default-white", children: formatCount(count, maxCount) })
|
|
314
310
|
}
|
|
315
311
|
)
|
|
316
312
|
] });
|
|
@@ -991,7 +987,8 @@ function NewsContent({
|
|
|
991
987
|
}
|
|
992
988
|
function CheckboxVisual({
|
|
993
989
|
state,
|
|
994
|
-
disabled
|
|
990
|
+
disabled,
|
|
991
|
+
error
|
|
995
992
|
}) {
|
|
996
993
|
if (state === "default") {
|
|
997
994
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -1000,12 +997,12 @@ function CheckboxVisual({
|
|
|
1000
997
|
"aria-hidden": "true",
|
|
1001
998
|
className: cn(
|
|
1002
999
|
"block w-4 h-4 rounded-[2px] border-[1.5px]",
|
|
1003
|
-
disabled ? "bg-disabled-bg border-[var(--fill-black-100)]" : "bg-background border-[var(--fill-black-200)]"
|
|
1000
|
+
disabled ? "bg-disabled-bg border-[var(--fill-black-100)]" : error ? "bg-background border-destructive" : "bg-background border-[var(--fill-black-200)]"
|
|
1004
1001
|
)
|
|
1005
1002
|
}
|
|
1006
1003
|
);
|
|
1007
1004
|
}
|
|
1008
|
-
const containerFill = disabled ? "var(--
|
|
1005
|
+
const containerFill = disabled ? "var(--disabled-bg)" : "var(--fill-p1-600)";
|
|
1009
1006
|
const iconFill = disabled ? "var(--fill-gray-400)" : "var(--fill-white-1000)";
|
|
1010
1007
|
if (state === "checked") {
|
|
1011
1008
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
@@ -1067,6 +1064,8 @@ function CheckboxVisual({
|
|
|
1067
1064
|
const Checkbox = React.forwardRef(function Checkbox2({
|
|
1068
1065
|
checked = false,
|
|
1069
1066
|
disabled = false,
|
|
1067
|
+
error = false,
|
|
1068
|
+
errorMessage = "Error message",
|
|
1070
1069
|
label,
|
|
1071
1070
|
description,
|
|
1072
1071
|
variant = "text",
|
|
@@ -1098,59 +1097,72 @@ const Checkbox = React.forwardRef(function Checkbox2({
|
|
|
1098
1097
|
const hasText = label !== void 0 || description !== void 0;
|
|
1099
1098
|
const hasActiveBorder = state === "checked" || state === "indeterminate";
|
|
1100
1099
|
const isButton = variant === "button";
|
|
1101
|
-
const buttonBorder = disabled ? "border-[var(--fill-black-100)]" : hasActiveBorder ? "border-primary-action" : "border-[var(--fill-black-200)]";
|
|
1100
|
+
const buttonBorder = disabled ? "border-[var(--fill-black-100)]" : error ? "border-destructive" : hasActiveBorder ? "border-primary-action" : "border-[var(--fill-black-200)]";
|
|
1101
|
+
const showError = error && !disabled;
|
|
1102
1102
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1103
1103
|
"label",
|
|
1104
1104
|
{
|
|
1105
1105
|
className: cn(
|
|
1106
|
-
"
|
|
1107
|
-
description ? "items-start" : "items-center",
|
|
1106
|
+
"flex flex-col select-none",
|
|
1108
1107
|
disabled ? "cursor-not-allowed" : "cursor-pointer",
|
|
1109
|
-
isButton && cn("bg-background rounded-lg border py-2.5 pl-3 pr-4", buttonBorder),
|
|
1110
1108
|
className
|
|
1111
1109
|
),
|
|
1112
1110
|
children: [
|
|
1113
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1111
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
1112
|
+
"span",
|
|
1113
|
+
{
|
|
1114
|
+
className: cn(
|
|
1115
|
+
"inline-flex gap-1",
|
|
1116
|
+
description ? "items-start" : "items-center",
|
|
1117
|
+
isButton && cn("bg-background rounded-lg border py-2.5 pl-3 pr-4", buttonBorder)
|
|
1118
|
+
),
|
|
1119
|
+
children: [
|
|
1120
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative inline-flex items-center justify-center w-6 h-6 shrink-0", children: [
|
|
1121
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1122
|
+
"input",
|
|
1123
|
+
{
|
|
1124
|
+
ref: setRefs,
|
|
1125
|
+
id,
|
|
1126
|
+
name,
|
|
1127
|
+
value,
|
|
1128
|
+
type: "checkbox",
|
|
1129
|
+
checked: checked === true,
|
|
1130
|
+
disabled,
|
|
1131
|
+
onChange: handleChange,
|
|
1132
|
+
"aria-label": ariaLabel,
|
|
1133
|
+
"aria-checked": checked === "indeterminate" ? "mixed" : checked,
|
|
1134
|
+
"aria-invalid": showError || void 0,
|
|
1135
|
+
className: "absolute inset-0 w-full h-full opacity-0 m-0 cursor-[inherit] disabled:cursor-[inherit]"
|
|
1136
|
+
}
|
|
1137
|
+
),
|
|
1138
|
+
/* @__PURE__ */ jsxRuntime.jsx(CheckboxVisual, { state, disabled, error: showError })
|
|
1139
|
+
] }),
|
|
1140
|
+
hasText && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex flex-col", children: [
|
|
1141
|
+
label !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1142
|
+
"span",
|
|
1143
|
+
{
|
|
1144
|
+
className: cn(
|
|
1145
|
+
"text-base leading-6",
|
|
1146
|
+
disabled ? "text-disabled" : "text-foreground"
|
|
1147
|
+
),
|
|
1148
|
+
children: label
|
|
1149
|
+
}
|
|
1150
|
+
),
|
|
1151
|
+
description !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1152
|
+
"span",
|
|
1153
|
+
{
|
|
1154
|
+
className: cn(
|
|
1155
|
+
"text-xs leading-4",
|
|
1156
|
+
disabled ? "text-disabled" : "text-subtle-text"
|
|
1157
|
+
),
|
|
1158
|
+
children: description
|
|
1159
|
+
}
|
|
1160
|
+
)
|
|
1161
|
+
] })
|
|
1162
|
+
]
|
|
1163
|
+
}
|
|
1164
|
+
),
|
|
1165
|
+
showError && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "mt-1 ml-7 text-xs text-destructive", children: errorMessage })
|
|
1154
1166
|
]
|
|
1155
1167
|
}
|
|
1156
1168
|
);
|
|
@@ -3668,7 +3680,7 @@ const Input = React.forwardRef(function Input2({
|
|
|
3668
3680
|
Input.displayName = "Input";
|
|
3669
3681
|
const ALERT_CONFIG = {
|
|
3670
3682
|
warning: {
|
|
3671
|
-
titleColor: "var(--
|
|
3683
|
+
titleColor: "var(--text-warning-primary)",
|
|
3672
3684
|
background: "https://www.figma.com/api/mcp/asset/f4ca68ad-5732-4124-9ff4-cfb69330cc02",
|
|
3673
3685
|
layers: [
|
|
3674
3686
|
{
|
|
@@ -3686,7 +3698,7 @@ const ALERT_CONFIG = {
|
|
|
3686
3698
|
]
|
|
3687
3699
|
},
|
|
3688
3700
|
success: {
|
|
3689
|
-
titleColor: "var(--success)",
|
|
3701
|
+
titleColor: "var(--text-success-primary)",
|
|
3690
3702
|
background: "https://www.figma.com/api/mcp/asset/2a865e6f-8a92-4496-88b5-71ac99e2c385",
|
|
3691
3703
|
layers: [
|
|
3692
3704
|
{
|
|
@@ -3700,7 +3712,7 @@ const ALERT_CONFIG = {
|
|
|
3700
3712
|
]
|
|
3701
3713
|
},
|
|
3702
3714
|
danger: {
|
|
3703
|
-
titleColor: "var(--
|
|
3715
|
+
titleColor: "var(--text-danger-primary)",
|
|
3704
3716
|
background: "https://www.figma.com/api/mcp/asset/c7a65595-684e-4a04-b7fd-d443951f680a",
|
|
3705
3717
|
layers: [
|
|
3706
3718
|
{
|
|
@@ -3885,18 +3897,19 @@ function NotificationDivider({ label }) {
|
|
|
3885
3897
|
}
|
|
3886
3898
|
function NotificationRow({
|
|
3887
3899
|
item,
|
|
3888
|
-
onItemClick
|
|
3900
|
+
onItemClick,
|
|
3901
|
+
hideIndicator = false,
|
|
3902
|
+
demoteNewBackground = false
|
|
3889
3903
|
}) {
|
|
3890
3904
|
const rowType = item.type ?? "icon";
|
|
3891
3905
|
const showImage = rowType === "image";
|
|
3892
|
-
const
|
|
3906
|
+
const status = item.status ?? (item.unread ? "unread" : "read");
|
|
3907
|
+
const showIndicator = (status === "new" || status === "unread") && !hideIndicator;
|
|
3908
|
+
const rowBackground = status === "new" && !demoteNewBackground ? "bg-muted" : "bg-background";
|
|
3893
3909
|
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
3894
3910
|
"div",
|
|
3895
3911
|
{
|
|
3896
|
-
className: cn(
|
|
3897
|
-
"flex w-full items-start gap-3 px-4 py-3",
|
|
3898
|
-
showUnread ? "bg-primary-action-light/40" : "bg-background"
|
|
3899
|
-
),
|
|
3912
|
+
className: cn("flex w-full items-start gap-3 px-4 py-3", rowBackground),
|
|
3900
3913
|
role: "button",
|
|
3901
3914
|
tabIndex: 0,
|
|
3902
3915
|
onClick: () => onItemClick == null ? void 0 : onItemClick(item),
|
|
@@ -3914,22 +3927,11 @@ function NotificationRow({
|
|
|
3914
3927
|
className: "h-10 w-10 rounded object-cover",
|
|
3915
3928
|
src: item.imageSrc
|
|
3916
3929
|
}
|
|
3917
|
-
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded bg-disabled-bg text-disabled", children: /* @__PURE__ */ jsxRuntime.jsx(react.ImageSquare, { size: 20, weight: "regular" }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-6 w-6 items-center justify-center text-subtle-text", children: item.icon ?? /* @__PURE__ */ jsxRuntime.jsx(react.
|
|
3918
|
-
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
|
|
3919
|
-
/* @__PURE__ */ jsxRuntime.
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
showUnread && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3923
|
-
"span",
|
|
3924
|
-
{
|
|
3925
|
-
"aria-hidden": "true",
|
|
3926
|
-
className: "h-2 w-2 rounded-full bg-primary-action"
|
|
3927
|
-
}
|
|
3928
|
-
),
|
|
3929
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs leading-4 text-muted-foreground", children: item.time })
|
|
3930
|
-
] }),
|
|
3931
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "col-start-1 mt-1 line-clamp-2 text-sm leading-5 text-muted-foreground", children: item.description })
|
|
3932
|
-
] }),
|
|
3930
|
+
) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-10 w-10 items-center justify-center rounded bg-disabled-bg text-disabled", children: /* @__PURE__ */ jsxRuntime.jsx(react.ImageSquare, { size: 20, weight: "regular" }) }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex h-6 w-6 items-center justify-center text-subtle-text", children: item.icon ?? /* @__PURE__ */ jsxRuntime.jsx(react.Gift, { size: 20, weight: "regular" }) }) }),
|
|
3931
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1 space-y-1", children: [
|
|
3932
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-2 text-base leading-6 font-semibold text-foreground", children: item.title }),
|
|
3933
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "line-clamp-3 text-sm leading-5 text-muted-foreground", children: item.description }),
|
|
3934
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs leading-4 text-muted-foreground", children: item.time }),
|
|
3933
3935
|
item.actionLabel && /* @__PURE__ */ jsxRuntime.jsx(
|
|
3934
3936
|
Button,
|
|
3935
3937
|
{
|
|
@@ -3944,7 +3946,14 @@ function NotificationRow({
|
|
|
3944
3946
|
children: item.actionLabel
|
|
3945
3947
|
}
|
|
3946
3948
|
)
|
|
3947
|
-
] })
|
|
3949
|
+
] }),
|
|
3950
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex w-2 shrink-0 items-start justify-center pt-2", children: showIndicator ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3951
|
+
"span",
|
|
3952
|
+
{
|
|
3953
|
+
"aria-hidden": "true",
|
|
3954
|
+
className: "h-2 w-2 rounded-full bg-destructive"
|
|
3955
|
+
}
|
|
3956
|
+
) : null })
|
|
3948
3957
|
]
|
|
3949
3958
|
}
|
|
3950
3959
|
);
|
|
@@ -3955,6 +3964,7 @@ const Notification = React.forwardRef(
|
|
|
3955
3964
|
badgeCount,
|
|
3956
3965
|
panelWidth = 375,
|
|
3957
3966
|
emptyText = "No notifications",
|
|
3967
|
+
showGroupLabels = true,
|
|
3958
3968
|
clearBadgeOnOpen = true,
|
|
3959
3969
|
open,
|
|
3960
3970
|
defaultOpen,
|
|
@@ -3966,16 +3976,23 @@ const Notification = React.forwardRef(
|
|
|
3966
3976
|
}, ref) {
|
|
3967
3977
|
const [internalOpen, setInternalOpen] = React.useState(defaultOpen ?? false);
|
|
3968
3978
|
const [isBadgeCleared, setIsBadgeCleared] = React.useState(false);
|
|
3979
|
+
const [clickedItemIds, setClickedItemIds] = React.useState(/* @__PURE__ */ new Set());
|
|
3980
|
+
const [wasDismissed, setWasDismissed] = React.useState(false);
|
|
3981
|
+
const [mobileAlign, setMobileAlign] = React.useState(null);
|
|
3982
|
+
const triggerRef = React.useRef(null);
|
|
3969
3983
|
const controlled = open !== void 0;
|
|
3970
3984
|
const resolvedOpen = controlled ? open : internalOpen;
|
|
3971
|
-
const
|
|
3985
|
+
const newCount = React.useMemo(
|
|
3972
3986
|
() => groups.reduce(
|
|
3973
|
-
(acc, group) => acc + group.items.filter((item) =>
|
|
3987
|
+
(acc, group) => acc + group.items.filter((item) => {
|
|
3988
|
+
const status = item.status ?? (item.unread ? "unread" : "read");
|
|
3989
|
+
return status === "new";
|
|
3990
|
+
}).length,
|
|
3974
3991
|
0
|
|
3975
3992
|
),
|
|
3976
3993
|
[groups]
|
|
3977
3994
|
);
|
|
3978
|
-
const nextCount = badgeCount ??
|
|
3995
|
+
const nextCount = badgeCount ?? newCount;
|
|
3979
3996
|
const prevCountRef = React.useRef(nextCount);
|
|
3980
3997
|
React.useEffect(() => {
|
|
3981
3998
|
const prevCount = prevCountRef.current;
|
|
@@ -3984,9 +4001,29 @@ const Notification = React.forwardRef(
|
|
|
3984
4001
|
}
|
|
3985
4002
|
prevCountRef.current = nextCount;
|
|
3986
4003
|
}, [nextCount]);
|
|
4004
|
+
React.useEffect(() => {
|
|
4005
|
+
const update = () => {
|
|
4006
|
+
if (window.innerWidth > 640 || !triggerRef.current) {
|
|
4007
|
+
setMobileAlign(null);
|
|
4008
|
+
return;
|
|
4009
|
+
}
|
|
4010
|
+
const contentWidth = Math.min(panelWidth, window.innerWidth - 32);
|
|
4011
|
+
const triggerLeft = triggerRef.current.getBoundingClientRect().left;
|
|
4012
|
+
setMobileAlign({
|
|
4013
|
+
alignOffset: (window.innerWidth - contentWidth) / 2 - triggerLeft,
|
|
4014
|
+
width: contentWidth
|
|
4015
|
+
});
|
|
4016
|
+
};
|
|
4017
|
+
update();
|
|
4018
|
+
window.addEventListener("resize", update);
|
|
4019
|
+
return () => window.removeEventListener("resize", update);
|
|
4020
|
+
}, [panelWidth]);
|
|
3987
4021
|
const displayCount = clearBadgeOnOpen && isBadgeCleared ? 0 : nextCount;
|
|
3988
4022
|
const hasItems = groups.some((group) => group.items.length > 0);
|
|
3989
4023
|
const handleOpenChange = (next) => {
|
|
4024
|
+
if (resolvedOpen && !next) {
|
|
4025
|
+
setWasDismissed(true);
|
|
4026
|
+
}
|
|
3990
4027
|
if (next && clearBadgeOnOpen && nextCount > 0) {
|
|
3991
4028
|
setIsBadgeCleared(true);
|
|
3992
4029
|
onBadgeCleared == null ? void 0 : onBadgeCleared();
|
|
@@ -3994,8 +4031,17 @@ const Notification = React.forwardRef(
|
|
|
3994
4031
|
if (!controlled) setInternalOpen(next);
|
|
3995
4032
|
onOpenChange == null ? void 0 : onOpenChange(next);
|
|
3996
4033
|
};
|
|
4034
|
+
const handleItemClick = (item) => {
|
|
4035
|
+
setClickedItemIds((prev) => {
|
|
4036
|
+
if (prev.has(item.id)) return prev;
|
|
4037
|
+
const next = new Set(prev);
|
|
4038
|
+
next.add(item.id);
|
|
4039
|
+
return next;
|
|
4040
|
+
});
|
|
4041
|
+
onItemClick == null ? void 0 : onItemClick(item);
|
|
4042
|
+
};
|
|
3997
4043
|
return /* @__PURE__ */ jsxRuntime.jsxs(Popover__namespace.Root, { open: resolvedOpen, onOpenChange: handleOpenChange, children: [
|
|
3998
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: cn("inline-flex", className), children: /* @__PURE__ */ jsxRuntime.jsx(Popover__namespace.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "relative", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4044
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { ref, className: cn("inline-flex", className), children: /* @__PURE__ */ jsxRuntime.jsx(Popover__namespace.Trigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx("div", { ref: triggerRef, className: "relative", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
3999
4045
|
Badge,
|
|
4000
4046
|
{
|
|
4001
4047
|
variant: "notification",
|
|
@@ -4008,22 +4054,26 @@ const Notification = React.forwardRef(
|
|
|
4008
4054
|
/* @__PURE__ */ jsxRuntime.jsx(Popover__namespace.Portal, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
4009
4055
|
Popover__namespace.Content,
|
|
4010
4056
|
{
|
|
4011
|
-
align: "end",
|
|
4057
|
+
align: mobileAlign ? "start" : "end",
|
|
4058
|
+
alignOffset: (mobileAlign == null ? void 0 : mobileAlign.alignOffset) ?? 0,
|
|
4059
|
+
avoidCollisions: !mobileAlign,
|
|
4012
4060
|
sideOffset: 10,
|
|
4013
4061
|
className: cn(
|
|
4014
4062
|
"z-50 overflow-hidden rounded-lg border border-border bg-background shadow-lg",
|
|
4015
4063
|
panelClassName
|
|
4016
4064
|
),
|
|
4017
|
-
style: { width: panelWidth },
|
|
4018
|
-
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-h-[480px] overflow-y-auto
|
|
4065
|
+
style: { width: (mobileAlign == null ? void 0 : mobileAlign.width) ?? panelWidth },
|
|
4066
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "max-h-[480px] overflow-y-auto", children: [
|
|
4019
4067
|
!hasItems && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "px-4 py-8 text-center text-sm text-muted-foreground", children: emptyText }),
|
|
4020
4068
|
groups.map((group) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "w-full", children: [
|
|
4021
|
-
/* @__PURE__ */ jsxRuntime.jsx(NotificationDivider, { label: group.label }),
|
|
4069
|
+
showGroupLabels && group.label ? /* @__PURE__ */ jsxRuntime.jsx(NotificationDivider, { label: group.label }) : null,
|
|
4022
4070
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "divide-y divide-divider", children: group.items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
4023
4071
|
NotificationRow,
|
|
4024
4072
|
{
|
|
4025
4073
|
item,
|
|
4026
|
-
onItemClick
|
|
4074
|
+
onItemClick: handleItemClick,
|
|
4075
|
+
hideIndicator: clickedItemIds.has(item.id),
|
|
4076
|
+
demoteNewBackground: wasDismissed
|
|
4027
4077
|
},
|
|
4028
4078
|
item.id
|
|
4029
4079
|
)) })
|