@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 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. **Do not add text/font utility classes to `<h1>`–`<h4>`.** They are pre-styled.
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
- 4. **Layout is your job, with plain Tailwind.** The library has NO layout primitives.
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
- 5. **Component props are documented in `llms.txt` and in the `.d.ts` files.**
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
- 6. **Mobile forms and action-heavy modals MUST use `<BottomSheet>`, not `<Modal>`.**
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" ? notificationIsFilled ? /* @__PURE__ */ jsxRuntime.jsx(react.BellSimple, { size: 19, weight: "fill" }) : /* @__PURE__ */ jsxRuntime.jsx(react.BellSimple, { size: 19, weight: "regular" }) : icon ?? /* @__PURE__ */ jsxRuntime.jsx(react.FunnelSimple, { size: 18, weight: "regular" });
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: cn(
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: isActive ? "outline" : "outline-black",
291
- className: cn(isActive && "bg-primary-action-light border-primary-action-light"),
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: isActive ? "outline" : "outline-black",
301
- className: cn(isActive && "bg-primary-action-light border-primary-action-light"),
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] px-1",
311
- variant === "notification" ? "-right-0.5 -top-0.5 h-[14px] min-w-[14px] bg-destructive" : "-right-1 -top-[7px] h-4 min-w-4 bg-primary-action"
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-on-primary-action", children: formatCount(count, maxCount) })
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(--fill-gray-300)" : "var(--fill-p1-600)";
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
- "inline-flex gap-1 select-none",
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("span", { className: "relative inline-flex items-center justify-center w-6 h-6 shrink-0", children: [
1114
- /* @__PURE__ */ jsxRuntime.jsx(
1115
- "input",
1116
- {
1117
- ref: setRefs,
1118
- id,
1119
- name,
1120
- value,
1121
- type: "checkbox",
1122
- checked: checked === true,
1123
- disabled,
1124
- onChange: handleChange,
1125
- "aria-label": ariaLabel,
1126
- "aria-checked": checked === "indeterminate" ? "mixed" : checked,
1127
- className: "absolute inset-0 w-full h-full opacity-0 m-0 cursor-[inherit] disabled:cursor-[inherit]"
1128
- }
1129
- ),
1130
- /* @__PURE__ */ jsxRuntime.jsx(CheckboxVisual, { state, disabled })
1131
- ] }),
1132
- hasText && /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "flex flex-col", children: [
1133
- label !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
1134
- "span",
1135
- {
1136
- className: cn(
1137
- "text-base leading-6",
1138
- disabled ? "text-disabled" : "text-foreground"
1139
- ),
1140
- children: label
1141
- }
1142
- ),
1143
- description !== void 0 && /* @__PURE__ */ jsxRuntime.jsx(
1144
- "span",
1145
- {
1146
- className: cn(
1147
- "text-xs leading-4",
1148
- disabled ? "text-disabled" : "text-subtle-text"
1149
- ),
1150
- children: description
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(--accent-orange)",
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(--destructive)",
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 showUnread = Boolean(item.unread);
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.Circle, { size: 20, weight: "regular" }) }) }),
3918
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "min-w-0 flex-1", children: [
3919
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_auto] items-start gap-x-2", children: [
3920
- /* @__PURE__ */ jsxRuntime.jsx("p", { className: "min-w-0 flex-1 truncate text-base leading-6 font-bold text-foreground", children: item.title }),
3921
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [
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 unreadCount = React.useMemo(
3985
+ const newCount = React.useMemo(
3972
3986
  () => groups.reduce(
3973
- (acc, group) => acc + group.items.filter((item) => Boolean(item.unread)).length,
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 ?? unreadCount;
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 py-2", children: [
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
  )) })