@happyvertical/smrt-svelte 0.57.46 → 0.57.47

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.
@@ -108,3 +108,10 @@ and `limit`. `iconOnly` (default false) swaps the text trigger for an
108
108
  panel under the trigger) or `'beside'` (panel opens at the trigger's inline
109
109
  end, top-aligned, for a collapsed rail; `data-placement` on the root).
110
110
  `variant` is passed to the trigger `Button` (default `'ghost'`).
111
+ `open` is bindable (`bind:open`) and `onopenchange(open)` reports the bell's
112
+ own changes, so a host can close the panel itself. The panel light-dismisses
113
+ (#4566): a pointer-down outside the bell (document capture, like `Popover`),
114
+ Escape at the document level wherever focus is (returns focus to the trigger),
115
+ and activating a notice link (closes without refocusing, so navigation keeps
116
+ focus). Notice titles are links styled in `--smrt-color-primary` with an
117
+ underline, not the browser default (#4567).
@@ -18,6 +18,14 @@ interface Props {
18
18
  placement?: 'below' | 'beside';
19
19
  /** Trigger button variant. */
20
20
  variant?: ComponentProps<typeof Button>['variant'];
21
+ /**
22
+ * Whether the panel is open. Bindable, so a host can close it (or open it)
23
+ * itself; the bell still toggles it from the trigger and closes it on light
24
+ * dismiss (#4566).
25
+ */
26
+ open?: boolean;
27
+ /** Called after the panel opens or closes, with the new state. */
28
+ onopenchange?: (open: boolean) => void;
21
29
  }
22
30
  const { t } = useI18n();
23
31
  let {
@@ -27,14 +35,33 @@ let {
27
35
  iconOnly = false,
28
36
  placement = 'below',
29
37
  variant = 'ghost',
38
+ open = $bindable(false),
39
+ onopenchange,
30
40
  }: Props = $props();
31
41
  const heading = $derived(label ?? t(M['ui.notification_bell.label']));
32
42
  const instanceId = $props.id();
43
+ let root = $state<HTMLDivElement>();
33
44
  let panel = $state<HTMLDivElement>();
34
- let open = $state(false);
35
45
  $effect(() => {
36
46
  if (open) panel?.focus();
37
47
  });
48
+ function setOpen(next: boolean) {
49
+ if (open === next) return;
50
+ open = next;
51
+ onopenchange?.(next);
52
+ }
53
+ // Light dismiss (#4566): a pointer-down anywhere outside the bell closes the
54
+ // panel, like `Popover`. Captured on the document so a click that lands on a
55
+ // control which stops propagation still counts as "outside".
56
+ $effect(() => {
57
+ if (!open || typeof document === 'undefined') return;
58
+ const dismiss = (event: PointerEvent) => {
59
+ if (event.target instanceof Node && !root?.contains(event.target))
60
+ setOpen(false);
61
+ };
62
+ document.addEventListener('pointerdown', dismiss, true);
63
+ return () => document.removeEventListener('pointerdown', dismiss, true);
64
+ });
38
65
  let loading = $state(true);
39
66
  let error = $state<string | null>(null);
40
67
  let unread = $state(0);
@@ -79,11 +106,14 @@ function safeHref(value: string | null | undefined): string | null {
79
106
  }
80
107
  }
81
108
  function close() {
82
- open = false;
109
+ setOpen(false);
83
110
  document.getElementById(`${instanceId}-trigger`)?.focus();
84
111
  }
112
+ // Escape closes the panel wherever focus is (the body after following a
113
+ // notice, a control elsewhere in the shell), not only inside the panel. A
114
+ // dialog or palette that owns Escape already stops it before it gets here.
85
115
  function keydown(event: KeyboardEvent) {
86
- if (event.key === 'Escape' && open) {
116
+ if (event.key === 'Escape' && open && !event.defaultPrevented) {
87
117
  event.preventDefault();
88
118
  close();
89
119
  }
@@ -123,8 +153,10 @@ $effect(() => {
123
153
  });
124
154
  </script>
125
155
 
126
- <div class="smrt-notification-bell" data-placement={placement} data-smrt-component="NotificationBell">
127
- <Button id={`${instanceId}-trigger`} aria-controls={`${instanceId}-panel`} {variant} aria-haspopup="dialog" aria-expanded={open} aria-label={iconOnly ? (unread > 0 ? `${heading}, ${unreadText}` : heading) : undefined} onclick={() => (open = !open)}>
156
+ <svelte:document onkeydown={keydown} />
157
+
158
+ <div bind:this={root} class="smrt-notification-bell" data-placement={placement} data-smrt-component="NotificationBell">
159
+ <Button id={`${instanceId}-trigger`} aria-controls={`${instanceId}-panel`} {variant} aria-haspopup="dialog" aria-expanded={open} aria-label={iconOnly ? (unread > 0 ? `${heading}, ${unreadText}` : heading) : undefined} onclick={() => setOpen(!open)}>
128
160
  {#if iconOnly}
129
161
  <span class="smrt-notification-bell__icon" aria-hidden="true">
130
162
  <svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor" focusable="false"><path d="M12 22c1.1 0 2-.9 2-2h-4a2 2 0 0 0 2 2m6-6v-5c0-3.07-1.63-5.64-4.5-6.32V4a1.5 1.5 0 0 0-3 0v.68C7.64 5.36 6 7.92 6 11v5l-2 2v1h16v-1z" /></svg>
@@ -135,12 +167,12 @@ $effect(() => {
135
167
  {/if}
136
168
  </Button>
137
169
  {#if open}
138
- <div bind:this={panel} id={`${instanceId}-panel`} role="dialog" aria-label={heading} tabindex="-1" onkeydown={keydown} class="smrt-notification-bell__panel">
170
+ <div bind:this={panel} id={`${instanceId}-panel`} role="dialog" aria-label={heading} tabindex="-1" class="smrt-notification-bell__panel">
139
171
  <header><strong>{heading}</strong><Button variant="ghost" size="sm" disabled={unread === 0 || loading} onclick={() => mutate()}>{t(M['ui.notification_bell.mark_all'])}</Button></header>
140
172
  {#if error}<p role="alert">{t(error)}</p>{/if}
141
173
  {#if loading}<p role="status">{t(M['ui.notification_bell.loading'])}</p>{:else if items.length === 0}<p>{t(M['ui.notification_bell.empty'])}</p>{:else}<ol>
142
174
  {#each items as item (item.id)}<li data-read={item.readAt ? 'true' : 'false'}>
143
- {#if safeHref(item.href)}<a href={safeHref(item.href) ?? undefined}>{item.title}</a>{:else}<strong>{item.title}</strong>{/if}
175
+ {#if safeHref(item.href)}<a href={safeHref(item.href) ?? undefined} onclick={() => setOpen(false)}>{item.title}</a>{:else}<strong>{item.title}</strong>{/if}
144
176
  {#if item.body}<p>{item.body}</p>{/if}
145
177
  {#if !item.readAt}<Button variant="ghost" size="sm" onclick={() => mutate(item.id)}>{t(M['ui.notification_bell.mark_one'])}</Button>{/if}
146
178
  </li>{/each}
@@ -161,6 +193,9 @@ $effect(() => {
161
193
  ol { display: grid; gap: var(--smrt-spacing-2); padding: 0; list-style: none; }
162
194
  li { padding-block: var(--smrt-spacing-2); border-block-end: var(--smrt-hairline); }
163
195
  li[data-read='false'] { border-inline-start: 3px solid var(--smrt-color-primary); padding-inline-start: var(--smrt-spacing-2); }
196
+ /* Notice titles are links: themed like the panel's other text, not the browser's default link blue (#4567). */
197
+ li a { color: var(--smrt-color-primary); font-weight: var(--smrt-typography-weight-semibold, 600); text-decoration: underline; text-underline-offset: 0.15em; }
198
+ li a:hover, li a:focus-visible { color: var(--smrt-color-on-surface); }
164
199
  p { overflow-wrap: anywhere; }
165
200
  }
166
201
  </style>
@@ -14,8 +14,16 @@ interface Props {
14
14
  placement?: 'below' | 'beside';
15
15
  /** Trigger button variant. */
16
16
  variant?: ComponentProps<typeof Button>['variant'];
17
+ /**
18
+ * Whether the panel is open. Bindable, so a host can close it (or open it)
19
+ * itself; the bell still toggles it from the trigger and closes it on light
20
+ * dismiss (#4566).
21
+ */
22
+ open?: boolean;
23
+ /** Called after the panel opens or closes, with the new state. */
24
+ onopenchange?: (open: boolean) => void;
17
25
  }
18
- declare const NotificationBell: import("svelte").Component<Props, {}, "">;
26
+ declare const NotificationBell: import("svelte").Component<Props, {}, "open">;
19
27
  type NotificationBell = ReturnType<typeof NotificationBell>;
20
28
  export default NotificationBell;
21
29
  //# sourceMappingURL=NotificationBell.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"NotificationBell.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/notifications/NotificationBell.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAE7C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGvD,UAAU,KAAK;IACb,wEAAwE;IACxE,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uFAAuF;IACvF,SAAS,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC/B,8BAA8B;IAC9B,OAAO,CAAC,EAAE,cAAc,CAAC,OAAO,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC;CACpD;AA8ID,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"NotificationBell.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/notifications/NotificationBell.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAE7C,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAGvD,UAAU,KAAK;IACb,wEAAwE;IACxE,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,4CAA4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uFAAuF;IACvF,SAAS,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC/B,8BAA8B;IAC9B,OAAO,CAAC,EAAE,cAAc,CAAC,OAAO,MAAM,CAAC,CAAC,SAAS,CAAC,CAAC;IACnD;;;;OAIG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,kEAAkE;IAClE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CACxC;AAsKD,QAAA,MAAM,gBAAgB,+CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-svelte",
3
- "version": "0.57.46",
3
+ "version": "0.57.47",
4
4
  "smrtJsdoc": "strict",
5
5
  "description": "Svelte 5 components for SMRT user management - auth, users, tenants, roles, permissions, groups",
6
6
  "type": "module",
@@ -209,12 +209,12 @@
209
209
  "access": "public"
210
210
  },
211
211
  "dependencies": {
212
- "@happyvertical/logger": "^0.111.0",
213
- "@happyvertical/smrt-languages": "0.57.46",
214
- "@happyvertical/smrt-mcp-apps": "0.57.46",
215
- "@happyvertical/smrt-types": "0.57.46",
216
- "@happyvertical/smrt-ui": "0.57.46",
217
- "@happyvertical/smrt-web": "0.57.46",
212
+ "@happyvertical/logger": "^0.111.3",
213
+ "@happyvertical/smrt-languages": "0.57.47",
214
+ "@happyvertical/smrt-mcp-apps": "0.57.47",
215
+ "@happyvertical/smrt-types": "0.57.47",
216
+ "@happyvertical/smrt-ui": "0.57.47",
217
+ "@happyvertical/smrt-web": "0.57.47",
218
218
  "@tanstack/db": "^1.0.0-rc.1",
219
219
  "@tanstack/svelte-db": "^1.0.0-rc.1",
220
220
  "esm-env": "^1.2.2"
@@ -249,15 +249,15 @@
249
249
  }
250
250
  },
251
251
  "devDependencies": {
252
- "@happyvertical/smrt-agents": "0.57.46",
253
- "@happyvertical/smrt-chat": "0.57.46",
254
- "@happyvertical/smrt-content": "0.57.46",
255
- "@happyvertical/smrt-core": "0.57.46",
256
- "@happyvertical/smrt-reports": "0.57.46",
257
- "@happyvertical/smrt-scanner": "0.57.46",
258
- "@happyvertical/smrt-tenancy": "0.57.46",
259
- "@happyvertical/smrt-users": "0.57.46",
260
- "@happyvertical/speech": "^0.111.0",
252
+ "@happyvertical/smrt-agents": "0.57.47",
253
+ "@happyvertical/smrt-chat": "0.57.47",
254
+ "@happyvertical/smrt-content": "0.57.47",
255
+ "@happyvertical/smrt-core": "0.57.47",
256
+ "@happyvertical/smrt-reports": "0.57.47",
257
+ "@happyvertical/smrt-scanner": "0.57.47",
258
+ "@happyvertical/smrt-tenancy": "0.57.47",
259
+ "@happyvertical/smrt-users": "0.57.47",
260
+ "@happyvertical/speech": "^0.111.3",
261
261
  "@sveltejs/package": "^2.5.8",
262
262
  "@sveltejs/vite-plugin-svelte": "^7.3.0",
263
263
  "@testing-library/jest-dom": "^7.0.1",