flexdesk 0.13.0 → 0.14.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/README.md CHANGED
@@ -1671,6 +1671,43 @@ const nc = new NotificationCenter({ eventBus, maxVisible: 3, overflow: 'collapse
1671
1671
  nc.mount(document.body);
1672
1672
  ```
1673
1673
 
1674
+ ### Alerts with history: a log, a pane, a tab host and a bell (0.14.0)
1675
+
1676
+ A toast is the moment. The **alert log** keeps what came in until it is
1677
+ dismissed, and four pieces draw it. Each works without the others:
1678
+
1679
+ | Piece | Entry point | Does |
1680
+ |---|---|---|
1681
+ | `createAlertLog({ source, limit })` | `widgets` | Entries `{id, at, level, title, body?, ref?, read}`, newest first; `add`, `receive`, `markRead`, `dismiss`, `unreadCount`, `worstUnread`, `subscribe`, `loadMore` |
1682
+ | `localSource({ read, write })` | `widgets` | Keeps the whole log in one stored value (localStorage, a workspace file) |
1683
+ | `mountAlertsPane(el, { log, onOpen, onBadge, rules?, stillMatches? })` | `widgets` | Level chips with counts, a filter, day groups, relative times, Open / Dismiss / Mark all read / Clear, *Load older* |
1684
+ | `createBottomPanelTabs(el, { panes, remember, store, eventBus })` | `wm` | The `.twm-bp` tab strip: `select`, `setBadge(id, n, level)`, `bottompanel:show {tab}` |
1685
+ | `new NotificationHistory({ alertLog, mountPoint, alertsTab, onOpen })` | `widgets` | The bell shows the log's unread count in the worst level's colour; a click (and "+N more") opens the Alerts tab |
1686
+
1687
+ A **source** is where entries live: `load({offset, limit})` answers
1688
+ `{entries, more?, unread?}`, plus `markRead(ids | null)` and, optionally,
1689
+ `dismiss(ids | null)`. Without `dismiss`, the pane offers no Dismiss or Clear.
1690
+ Levels are `info`, `warning` and `alert`; `warn`, `error` and `danger` are read as
1691
+ those.
1692
+
1693
+ ```js
1694
+ const log = createAlertLog({ source: {
1695
+ load: ({ offset, limit }) => api.notifications({ offset, limit }),
1696
+ markRead: (ids) => (ids ? Promise.all(ids.map(api.readNotification)) : api.readAll()),
1697
+ } });
1698
+ const tabs = createBottomPanelTabs(panelEl, { eventBus, panes: [
1699
+ { id: 'alerts', label: 'Alerts', eager: true,
1700
+ mount: (el, store, api) => mountAlertsPane(el, { log, onOpen: openRef,
1701
+ onBadge: ({ unread, level }) => api.setBadge('alerts', unread, level) }) },
1702
+ ] });
1703
+ new NotificationHistory({ notificationCenter: nc, eventBus, alertLog: log, mountPoint: statusBarEl }).mount();
1704
+ ```
1705
+
1706
+ An eager pane, and the first one shown, mount inside `createBottomPanelTabs`
1707
+ before it returns. Reach the host through `mount`'s `api` argument, not the
1708
+ variable being assigned. Without `alertLog` and `mountPoint`, `NotificationHistory`
1709
+ is the bell it was.
1710
+
1674
1711
  ## The host port
1675
1712
 
1676
1713
  FlexDesk never touches `pywebview`, `fetch`, `localStorage` or the filesystem. It
@@ -1688,6 +1725,22 @@ createHost({
1688
1725
 
1689
1726
  `NULL_HOST` is a valid host with nothing behind it.
1690
1727
 
1728
+ Two adapters ship ready-made. `createPywebviewHost()` speaks to a pywebview
1729
+ window, and `createPhotinoHost()` (0.14.0) to a Photino.NET window over
1730
+ `window.external.sendMessage` / `receiveMessage`:
1731
+
1732
+ ```js
1733
+ import { createPhotinoHost } from '@flexdesk/host';
1734
+
1735
+ // In a browser tab this is NULL_HOST. Keep a capability of your own by passing
1736
+ // it in, or leave one out with null.
1737
+ const host = createPhotinoHost({ state: myServerState, onEvent: (name, value) => {} });
1738
+ ```
1739
+
1740
+ Its `window.chrome()` answers `'native'`, `dialogs.saveFile` falls back to a
1741
+ Blob download when the window refuses, and the messages the C# side has to
1742
+ answer are in [`src/host/photino_host.md`](src/host/photino_host.md).
1743
+
1691
1744
  ## Peer dependencies
1692
1745
 
1693
1746
  `@flexdesk/charts` needs Plotly (4.4 MB). `@flexdesk/editor` needs Monaco (14 MB).
package/css/flexdesk.css CHANGED
@@ -394,6 +394,12 @@
394
394
  padding: 0 3px;
395
395
  line-height: 1;
396
396
  }
397
+ /* On an alert log (STORY-0020): the worst unread level's colour, and the bell
398
+ * itself brighter while anything is unread. */
399
+ .twm-notification-history__badge--info { background: var(--color-info); }
400
+ .twm-notification-history__badge--warning { background: var(--color-warning); color: #1e1e1e; }
401
+ .twm-notification-history__badge--alert { background: var(--color-danger); }
402
+ .twm-notification-history__trigger--unread { color: var(--text-bright); }
397
403
  .twm-notification-history__panel {
398
404
  position: fixed;
399
405
  z-index: 9999;
@@ -7737,6 +7743,28 @@ tr:hover .twm-row-action { opacity: 1; }
7737
7743
  color: var(--text-bright);
7738
7744
  border-bottom-color: #fff;
7739
7745
  }
7746
+ /* A tab's count (createBottomPanelTabs' setBadge): Alerts' unread in the
7747
+ * worst unread level's colour, a Changes count in the neutral one. */
7748
+ .twm-bp__badge {
7749
+ display: inline-flex;
7750
+ align-items: center;
7751
+ justify-content: center;
7752
+ min-width: 16px;
7753
+ height: 16px;
7754
+ margin-left: 6px;
7755
+ padding: 0 4px;
7756
+ box-sizing: border-box;
7757
+ border-radius: 8px;
7758
+ background: var(--accent);
7759
+ color: #fff;
7760
+ font-size: 10.5px;
7761
+ line-height: 16px;
7762
+ text-transform: none;
7763
+ }
7764
+ .twm-bp__badge[hidden] { display: none; }
7765
+ .twm-bp__badge--info { background: color-mix(in srgb, var(--color-info) 35%, var(--surface-0)); }
7766
+ .twm-bp__badge--warning { background: color-mix(in srgb, var(--color-warning) 35%, var(--surface-0)); }
7767
+ .twm-bp__badge--alert { background: color-mix(in srgb, var(--color-danger) 45%, var(--surface-0)); }
7740
7768
  .twm-bp__body {
7741
7769
  flex: 1 1 auto;
7742
7770
  min-height: 0;
@@ -7782,6 +7810,77 @@ tr:hover .twm-row-action { opacity: 1; }
7782
7810
  .twm-bp-log__row--error .twm-bp-log__level,
7783
7811
  .twm-bp-log__row--error .twm-bp-log__msg { color: var(--color-danger); }
7784
7812
  .twm-bp-log__msg { color: var(--text-body); white-space: pre-wrap; word-break: break-word; }
7813
+
7814
+ /* ── The Alerts pane (alerts_pane.js, STORY-0019) ─────────────────────────
7815
+ * bugdesk's Alerts tab, on tokens: rules on the left when the embedder has
7816
+ * them, the log on the right, newest first, grouped by day. */
7817
+ .twm-alerts { display: flex; height: 100%; min-height: 0; font-size: var(--font-size-xs); color: var(--text-body); }
7818
+ /* In the bottom panel the pane fills the body, so the log scrolls and the
7819
+ * status line sits at the foot. Only a pane holding the Alerts pane: other
7820
+ * panes keep their padding and their own height. */
7821
+ .twm-bp-pane:has(> .twm-alerts) { height: 100%; padding: 0; box-sizing: border-box; }
7822
+ .twm-alerts .material-symbols-outlined { font-size: 15px; }
7823
+ .twm-alerts__spacer { flex: 1 1 auto; }
7824
+ .twm-alerts__rules { flex: 0 0 280px; display: flex; flex-direction: column; min-width: 0; border-right: 1px solid var(--border-subtle); }
7825
+ .twm-alerts__head, .twm-alerts__bar { flex: none; display: flex; align-items: center; gap: 8px; min-height: 34px; padding: 0 12px; border-bottom: 1px solid var(--border-subtle); }
7826
+ .twm-alerts__bar { flex-wrap: wrap; }
7827
+ .twm-alerts__caps { color: var(--text-muted); font-size: 10.5px; letter-spacing: .08em; }
7828
+ .twm-alerts__link { padding: 0; border: 0; background: none; color: var(--accent-bright); font: inherit; cursor: pointer; }
7829
+ .twm-alerts__link:hover { text-decoration: underline; }
7830
+ .twm-alerts__rulelist { flex: 1 1 auto; overflow-y: auto; padding: 4px 0; }
7831
+ .twm-alerts__rulebtn { width: 100%; display: flex; align-items: center; gap: 10px; height: 30px; padding: 0 12px; border: 0; background: none; color: var(--text-body); font: inherit; cursor: pointer; }
7832
+ .twm-alerts__rulebtn:hover { background: var(--state-hover); }
7833
+ .twm-alerts__rulebtn--on { background: var(--state-selected); box-shadow: inset 2px 0 0 var(--accent-bright); }
7834
+ .twm-alerts__rulebtn--off { color: var(--text-dim); }
7835
+ .twm-alerts__dot { flex: none; width: 8px; height: 8px; border-radius: 50%; }
7836
+ .twm-alerts__dot--alert { background: var(--color-danger); }
7837
+ .twm-alerts__dot--warning { background: var(--color-warning); }
7838
+ .twm-alerts__dot--info { background: var(--color-info); }
7839
+ .twm-alerts__rulename { flex: 1; min-width: 0; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
7840
+ .twm-alerts__meta { flex: none; color: var(--text-muted); font-size: 11px; }
7841
+ .twm-alerts__foot { padding: 6px 12px; border-top: 1px solid var(--border-subtle); color: var(--text-muted); font-size: 11px; line-height: 1.4; }
7842
+ .twm-alerts__main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
7843
+ .twm-alerts__chips { display: flex; gap: 4px; }
7844
+ .twm-alerts__chip { height: 24px; padding: 0 10px; border: 1px solid var(--surface-3); border-radius: 12px; background: none; color: var(--text-secondary); font: inherit; cursor: pointer; }
7845
+ .twm-alerts__chip:hover { color: var(--text-bright); }
7846
+ .twm-alerts__count { color: var(--text-muted); }
7847
+ .twm-alerts__chip--on { border-color: var(--accent-bright); background: var(--state-selected); color: var(--text-bright); }
7848
+ .twm-alerts__q { width: 220px; height: 24px; box-sizing: border-box; padding: 0 8px; border: 1px solid var(--surface-3); border-radius: 3px; background: var(--surface-1); color: var(--text-body); font: inherit; }
7849
+ .twm-alerts__q:focus { outline: none; border-color: var(--accent-bright); }
7850
+ .twm-alerts__btn { height: 24px; }
7851
+ .twm-alerts__btn[hidden] { display: none; }
7852
+ .twm-alerts__log { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
7853
+ .twm-alerts__day { padding: 8px 12px 4px 34px; color: var(--text-muted); font-size: 10.5px; letter-spacing: .08em; }
7854
+ .twm-alerts__row { display: flex; align-items: center; gap: 10px; height: 30px; padding: 0 12px; }
7855
+ .twm-alerts__row:hover { background: var(--state-hover); }
7856
+ .twm-alerts__unread { flex: 0 0 7px; height: 7px; border-radius: 50%; }
7857
+ .twm-alerts__row--unread .twm-alerts__unread { background: var(--accent-bright); }
7858
+ .twm-alerts__icon { flex: 0 0 18px; }
7859
+ .twm-alerts__row--info .twm-alerts__icon { color: var(--color-info); }
7860
+ .twm-alerts__row--warning .twm-alerts__icon { color: var(--color-warning); }
7861
+ .twm-alerts__row--alert .twm-alerts__icon { color: var(--color-danger); }
7862
+ .twm-alerts__time { flex: 0 0 78px; color: var(--text-muted); white-space: nowrap; }
7863
+ .twm-alerts__rule { flex: 0 0 170px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-primary); }
7864
+ .twm-alerts__tag { flex: none; display: inline-flex; align-items: center; height: 18px; padding: 0 6px; border-radius: 3px; background: var(--surface-2); color: var(--text-secondary); font-size: 10.5px; }
7865
+ .twm-alerts__tag--none { display: none; }
7866
+ .twm-alerts__ref { flex: 0 0 auto; max-width: 140px; padding: 0; border: 0; background: none; font-family: var(--font-family-mono); font-size: 11.5px; color: var(--accent-bright); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }
7867
+ button.twm-alerts__ref { cursor: pointer; }
7868
+ button.twm-alerts__ref:hover { text-decoration: underline; }
7869
+ .twm-alerts__ref:empty { display: none; }
7870
+ .twm-alerts__text { flex: 1 1 auto; min-width: 0; display: flex; gap: 8px; overflow: hidden; white-space: nowrap; }
7871
+ .twm-alerts__title { overflow: hidden; text-overflow: ellipsis; color: var(--text-secondary); }
7872
+ .twm-alerts__row--unread .twm-alerts__title { color: var(--text-primary); font-weight: 500; }
7873
+ .twm-alerts__body { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--text-muted); }
7874
+ .twm-alerts__state { flex: 0 0 128px; font-size: 11px; color: var(--text-muted); }
7875
+ .twm-alerts__state--gone { color: var(--text-dim); font-style: italic; }
7876
+ .twm-alerts__acts { flex: 0 0 auto; display: flex; justify-content: flex-end; gap: 2px; }
7877
+ .twm-alerts__icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 3px; background: none; color: var(--text-muted); cursor: pointer; }
7878
+ .twm-alerts__icon-btn:hover { background: var(--surface-2); color: var(--text-bright); }
7879
+ .twm-alerts__icon-btn .material-symbols-outlined { font-size: 14px; }
7880
+ .twm-alerts__none { padding: 28px 16px; text-align: center; color: var(--text-muted); }
7881
+ .twm-alerts__more { display: block; margin: 8px auto; height: 24px; }
7882
+ .twm-alerts__status { flex: none; display: flex; align-items: center; height: 26px; padding: 0 12px; border-top: 1px solid var(--border-subtle); color: var(--text-muted); font-size: 11px; }
7883
+ .twm-alerts__status[hidden] { display: none; }
7785
7884
  /* Context menu — override main_new.css's .twm-context-menu to the tighter,
7786
7885
  * grid-style look (icon | label) with a flat surface-1 background and
7787
7886
  * square corners. All callers (sidebars, tile chrome, desktop bar, …)
package/dist/flexdesk.css CHANGED
@@ -394,6 +394,12 @@
394
394
  padding: 0 3px;
395
395
  line-height: 1;
396
396
  }
397
+ /* On an alert log (STORY-0020): the worst unread level's colour, and the bell
398
+ * itself brighter while anything is unread. */
399
+ .twm-notification-history__badge--info { background: var(--color-info); }
400
+ .twm-notification-history__badge--warning { background: var(--color-warning); color: #1e1e1e; }
401
+ .twm-notification-history__badge--alert { background: var(--color-danger); }
402
+ .twm-notification-history__trigger--unread { color: var(--text-bright); }
397
403
  .twm-notification-history__panel {
398
404
  position: fixed;
399
405
  z-index: 9999;
@@ -7737,6 +7743,28 @@ tr:hover .twm-row-action { opacity: 1; }
7737
7743
  color: var(--text-bright);
7738
7744
  border-bottom-color: #fff;
7739
7745
  }
7746
+ /* A tab's count (createBottomPanelTabs' setBadge): Alerts' unread in the
7747
+ * worst unread level's colour, a Changes count in the neutral one. */
7748
+ .twm-bp__badge {
7749
+ display: inline-flex;
7750
+ align-items: center;
7751
+ justify-content: center;
7752
+ min-width: 16px;
7753
+ height: 16px;
7754
+ margin-left: 6px;
7755
+ padding: 0 4px;
7756
+ box-sizing: border-box;
7757
+ border-radius: 8px;
7758
+ background: var(--accent);
7759
+ color: #fff;
7760
+ font-size: 10.5px;
7761
+ line-height: 16px;
7762
+ text-transform: none;
7763
+ }
7764
+ .twm-bp__badge[hidden] { display: none; }
7765
+ .twm-bp__badge--info { background: color-mix(in srgb, var(--color-info) 35%, var(--surface-0)); }
7766
+ .twm-bp__badge--warning { background: color-mix(in srgb, var(--color-warning) 35%, var(--surface-0)); }
7767
+ .twm-bp__badge--alert { background: color-mix(in srgb, var(--color-danger) 45%, var(--surface-0)); }
7740
7768
  .twm-bp__body {
7741
7769
  flex: 1 1 auto;
7742
7770
  min-height: 0;
@@ -7782,6 +7810,77 @@ tr:hover .twm-row-action { opacity: 1; }
7782
7810
  .twm-bp-log__row--error .twm-bp-log__level,
7783
7811
  .twm-bp-log__row--error .twm-bp-log__msg { color: var(--color-danger); }
7784
7812
  .twm-bp-log__msg { color: var(--text-body); white-space: pre-wrap; word-break: break-word; }
7813
+
7814
+ /* ── The Alerts pane (alerts_pane.js, STORY-0019) ─────────────────────────
7815
+ * bugdesk's Alerts tab, on tokens: rules on the left when the embedder has
7816
+ * them, the log on the right, newest first, grouped by day. */
7817
+ .twm-alerts { display: flex; height: 100%; min-height: 0; font-size: var(--font-size-xs); color: var(--text-body); }
7818
+ /* In the bottom panel the pane fills the body, so the log scrolls and the
7819
+ * status line sits at the foot. Only a pane holding the Alerts pane: other
7820
+ * panes keep their padding and their own height. */
7821
+ .twm-bp-pane:has(> .twm-alerts) { height: 100%; padding: 0; box-sizing: border-box; }
7822
+ .twm-alerts .material-symbols-outlined { font-size: 15px; }
7823
+ .twm-alerts__spacer { flex: 1 1 auto; }
7824
+ .twm-alerts__rules { flex: 0 0 280px; display: flex; flex-direction: column; min-width: 0; border-right: 1px solid var(--border-subtle); }
7825
+ .twm-alerts__head, .twm-alerts__bar { flex: none; display: flex; align-items: center; gap: 8px; min-height: 34px; padding: 0 12px; border-bottom: 1px solid var(--border-subtle); }
7826
+ .twm-alerts__bar { flex-wrap: wrap; }
7827
+ .twm-alerts__caps { color: var(--text-muted); font-size: 10.5px; letter-spacing: .08em; }
7828
+ .twm-alerts__link { padding: 0; border: 0; background: none; color: var(--accent-bright); font: inherit; cursor: pointer; }
7829
+ .twm-alerts__link:hover { text-decoration: underline; }
7830
+ .twm-alerts__rulelist { flex: 1 1 auto; overflow-y: auto; padding: 4px 0; }
7831
+ .twm-alerts__rulebtn { width: 100%; display: flex; align-items: center; gap: 10px; height: 30px; padding: 0 12px; border: 0; background: none; color: var(--text-body); font: inherit; cursor: pointer; }
7832
+ .twm-alerts__rulebtn:hover { background: var(--state-hover); }
7833
+ .twm-alerts__rulebtn--on { background: var(--state-selected); box-shadow: inset 2px 0 0 var(--accent-bright); }
7834
+ .twm-alerts__rulebtn--off { color: var(--text-dim); }
7835
+ .twm-alerts__dot { flex: none; width: 8px; height: 8px; border-radius: 50%; }
7836
+ .twm-alerts__dot--alert { background: var(--color-danger); }
7837
+ .twm-alerts__dot--warning { background: var(--color-warning); }
7838
+ .twm-alerts__dot--info { background: var(--color-info); }
7839
+ .twm-alerts__rulename { flex: 1; min-width: 0; text-align: left; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
7840
+ .twm-alerts__meta { flex: none; color: var(--text-muted); font-size: 11px; }
7841
+ .twm-alerts__foot { padding: 6px 12px; border-top: 1px solid var(--border-subtle); color: var(--text-muted); font-size: 11px; line-height: 1.4; }
7842
+ .twm-alerts__main { flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; }
7843
+ .twm-alerts__chips { display: flex; gap: 4px; }
7844
+ .twm-alerts__chip { height: 24px; padding: 0 10px; border: 1px solid var(--surface-3); border-radius: 12px; background: none; color: var(--text-secondary); font: inherit; cursor: pointer; }
7845
+ .twm-alerts__chip:hover { color: var(--text-bright); }
7846
+ .twm-alerts__count { color: var(--text-muted); }
7847
+ .twm-alerts__chip--on { border-color: var(--accent-bright); background: var(--state-selected); color: var(--text-bright); }
7848
+ .twm-alerts__q { width: 220px; height: 24px; box-sizing: border-box; padding: 0 8px; border: 1px solid var(--surface-3); border-radius: 3px; background: var(--surface-1); color: var(--text-body); font: inherit; }
7849
+ .twm-alerts__q:focus { outline: none; border-color: var(--accent-bright); }
7850
+ .twm-alerts__btn { height: 24px; }
7851
+ .twm-alerts__btn[hidden] { display: none; }
7852
+ .twm-alerts__log { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
7853
+ .twm-alerts__day { padding: 8px 12px 4px 34px; color: var(--text-muted); font-size: 10.5px; letter-spacing: .08em; }
7854
+ .twm-alerts__row { display: flex; align-items: center; gap: 10px; height: 30px; padding: 0 12px; }
7855
+ .twm-alerts__row:hover { background: var(--state-hover); }
7856
+ .twm-alerts__unread { flex: 0 0 7px; height: 7px; border-radius: 50%; }
7857
+ .twm-alerts__row--unread .twm-alerts__unread { background: var(--accent-bright); }
7858
+ .twm-alerts__icon { flex: 0 0 18px; }
7859
+ .twm-alerts__row--info .twm-alerts__icon { color: var(--color-info); }
7860
+ .twm-alerts__row--warning .twm-alerts__icon { color: var(--color-warning); }
7861
+ .twm-alerts__row--alert .twm-alerts__icon { color: var(--color-danger); }
7862
+ .twm-alerts__time { flex: 0 0 78px; color: var(--text-muted); white-space: nowrap; }
7863
+ .twm-alerts__rule { flex: 0 0 170px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-primary); }
7864
+ .twm-alerts__tag { flex: none; display: inline-flex; align-items: center; height: 18px; padding: 0 6px; border-radius: 3px; background: var(--surface-2); color: var(--text-secondary); font-size: 10.5px; }
7865
+ .twm-alerts__tag--none { display: none; }
7866
+ .twm-alerts__ref { flex: 0 0 auto; max-width: 140px; padding: 0; border: 0; background: none; font-family: var(--font-family-mono); font-size: 11.5px; color: var(--accent-bright); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; }
7867
+ button.twm-alerts__ref { cursor: pointer; }
7868
+ button.twm-alerts__ref:hover { text-decoration: underline; }
7869
+ .twm-alerts__ref:empty { display: none; }
7870
+ .twm-alerts__text { flex: 1 1 auto; min-width: 0; display: flex; gap: 8px; overflow: hidden; white-space: nowrap; }
7871
+ .twm-alerts__title { overflow: hidden; text-overflow: ellipsis; color: var(--text-secondary); }
7872
+ .twm-alerts__row--unread .twm-alerts__title { color: var(--text-primary); font-weight: 500; }
7873
+ .twm-alerts__body { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--text-muted); }
7874
+ .twm-alerts__state { flex: 0 0 128px; font-size: 11px; color: var(--text-muted); }
7875
+ .twm-alerts__state--gone { color: var(--text-dim); font-style: italic; }
7876
+ .twm-alerts__acts { flex: 0 0 auto; display: flex; justify-content: flex-end; gap: 2px; }
7877
+ .twm-alerts__icon-btn { display: inline-flex; align-items: center; justify-content: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: 3px; background: none; color: var(--text-muted); cursor: pointer; }
7878
+ .twm-alerts__icon-btn:hover { background: var(--surface-2); color: var(--text-bright); }
7879
+ .twm-alerts__icon-btn .material-symbols-outlined { font-size: 14px; }
7880
+ .twm-alerts__none { padding: 28px 16px; text-align: center; color: var(--text-muted); }
7881
+ .twm-alerts__more { display: block; margin: 8px auto; height: 24px; }
7882
+ .twm-alerts__status { flex: none; display: flex; align-items: center; height: 26px; padding: 0 12px; border-top: 1px solid var(--border-subtle); color: var(--text-muted); font-size: 11px; }
7883
+ .twm-alerts__status[hidden] { display: none; }
7785
7884
  /* Context menu — override main_new.css's .twm-context-menu to the tighter,
7786
7885
  * grid-style look (icon | label) with a flat surface-1 background and
7787
7886
  * square corners. All callers (sidebars, tile chrome, desktop bar, …)
package/dist/flow.js CHANGED
@@ -199,6 +199,7 @@ function openPopover({ anchor, content, label, className = "", onClose = null, f
199
199
  handle.close("escape");
200
200
  };
201
201
  const onResize = () => place();
202
+ const sized = typeof ResizeObserver === "function" ? new ResizeObserver(() => place()) : null;
202
203
  const handle = {
203
204
  el: pop,
204
205
  anchor,
@@ -212,6 +213,7 @@ function openPopover({ anchor, content, label, className = "", onClose = null, f
212
213
  document.removeEventListener("mousedown", onPress, true);
213
214
  pop.removeEventListener("keydown", onKey);
214
215
  window.removeEventListener("resize", onResize);
216
+ sized?.disconnect();
215
217
  pop.remove();
216
218
  if (ACTIVE === handle) ACTIVE = null;
217
219
  if (held && reason !== "blur" && anchor?.isConnected) anchor.focus?.({ preventScroll: true });
@@ -226,6 +228,7 @@ function openPopover({ anchor, content, label, className = "", onClose = null, f
226
228
  }, 0);
227
229
  pop.addEventListener("keydown", onKey);
228
230
  window.addEventListener("resize", onResize);
231
+ sized?.observe(pop);
229
232
  place();
230
233
  (focus || pop).focus?.({ preventScroll: true });
231
234
  return handle;