@arcforgelabs/flow 2026.10.26 → 2026.10.29

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.
Files changed (63) hide show
  1. package/README.md +6 -0
  2. package/npm-shrinkwrap.json +627 -2
  3. package/package.json +3 -2
  4. package/server/api/invoicing.js +313 -46
  5. package/server/app.js +30 -2
  6. package/server/core/automations.js +154 -0
  7. package/server/core/invoice-rules.js +226 -0
  8. package/server/core/invoices.js +8 -3
  9. package/server/core/invoicing-rules.js +255 -0
  10. package/server/core/job-board.js +130 -0
  11. package/server/core/ledger.js +57 -17
  12. package/server/core/line-mapping.js +337 -0
  13. package/server/core/monitoring.js +34 -0
  14. package/server/core/pipeline.js +16 -3
  15. package/server/core/sending.js +283 -0
  16. package/server/db/migrate-all.js +26 -0
  17. package/server/db/migrations/0010_invoicing_rules.sql +72 -0
  18. package/server/db/migrations/0011_invoice_rules.sql +27 -0
  19. package/server/db/migrations/0012_invoice_sending.sql +18 -0
  20. package/server/db/migrations/0013_line_mappings.sql +35 -0
  21. package/server/db/migrations/meta/0010_snapshot.json +3158 -0
  22. package/server/db/migrations/meta/0011_snapshot.json +3314 -0
  23. package/server/db/migrations/meta/0012_snapshot.json +3380 -0
  24. package/server/db/migrations/meta/0013_snapshot.json +3568 -0
  25. package/server/db/migrations/meta/_journal.json +28 -0
  26. package/server/db/schema.js +158 -0
  27. package/server/import/at-start.js +64 -2
  28. package/server/index.js +17 -5
  29. package/server/integrations/adapters.js +2 -1
  30. package/server/integrations/port.js +6 -0
  31. package/server/integrations/seazona/feed.js +62 -4
  32. package/server/integrations/seazona/index.js +7 -1
  33. package/server/integrations/seazona/lanes.js +4 -1
  34. package/server/integrations/seazona/ledger.js +2 -0
  35. package/server/integrations/seazona/migrations/0000_api_governor.sql +3 -3
  36. package/server/integrations/seazona/migrations/0001_mirror.sql +9 -9
  37. package/server/integrations/sentry/index.js +105 -0
  38. package/server/integrations/xero/client.js +5 -0
  39. package/server/integrations/xero/drafts.js +127 -17
  40. package/server/integrations/xero/index.js +11 -1
  41. package/server/integrations/xero/lookup.js +6 -2
  42. package/server/integrations/xero/migrations/0000_xero_connections.sql +1 -1
  43. package/server/integrations/xero/migrations/0001_xero_catalogue.sql +5 -5
  44. package/server/integrations/xero/migrations/0002_xero_invoice_pushes.sql +2 -2
  45. package/server/integrations/xero/migrations/0003_xero_payment_cursors.sql +1 -1
  46. package/server/integrations/xero/migrations/0004_xero_mirror.sql +7 -7
  47. package/server/integrations/xero/migrations/0005_xero_send_modes.sql +16 -0
  48. package/server/integrations/xero/migrations/meta/_journal.json +7 -0
  49. package/server/ui/app-page.js +7 -4
  50. package/server/ui/app.js +122 -6
  51. package/server/ui/automations.js +96 -0
  52. package/server/ui/clerk-appearance.js +21 -0
  53. package/server/ui/flow.css +243 -30
  54. package/server/ui/invoice-rules.js +237 -0
  55. package/server/ui/invoicing-settings.js +110 -0
  56. package/server/ui/invoicing.js +166 -20
  57. package/server/ui/job-board.js +155 -0
  58. package/server/ui/jobs.js +232 -84
  59. package/server/ui/line-mappings.js +60 -0
  60. package/server/ui/logos/sentry.svg +1 -0
  61. package/server/ui/monitoring.js +61 -0
  62. package/server/ui/routes.js +2 -2
  63. package/server/ui/search.js +145 -0
package/server/ui/app.js CHANGED
@@ -10,7 +10,11 @@ import { getAppearance, onAppearanceChange, setAppearance, loadFonts } from '/as
10
10
  import { flowClerkAppearance } from '/assets/ui/clerk-appearance.js';
11
11
  import { ACCENT_PRESETS, normalizeAccent, THEME_FAMILIES, tokensFor } from '/assets/theme/themes.js';
12
12
  import { renderInvoice, renderInvoiceList } from '/assets/ui/invoicing.js';
13
+ import { automationSection } from '/assets/ui/automations.js';
14
+ import { renderInvoicingSettings, sendModeSection } from '/assets/ui/invoicing-settings.js';
13
15
  import { renderJobs } from '/assets/ui/jobs.js';
16
+ import { startMonitoring } from '/assets/ui/monitoring.js';
17
+ import { openSearch } from '/assets/ui/search.js';
14
18
 
15
19
  const $ = (id) => document.getElementById(id);
16
20
 
@@ -190,6 +194,7 @@ let me = null;
190
194
  const VIEWS = {
191
195
  jobs: { label: 'Jobs', icon: 'jobs', group: 'Work', render: (main) => renderJobs(main, ui()) },
192
196
  invoices: { label: 'Invoices', icon: 'invoices', group: 'Work', render: (main, id) => (id ? renderInvoice(main, ui(), id) : renderInvoiceList(main, ui())) },
197
+ 'invoicing-settings': { label: 'Invoicing', icon: 'invoices', group: 'Settings', render: (main) => renderInvoicingSettings(main, ui()) },
193
198
  people: { label: 'People', icon: 'people', group: 'Settings', ownerOnly: true, render: renderPeople },
194
199
  integrations: { label: 'Integrations', icon: 'plug', group: 'Settings', ownerOnly: true, render: (main, id) => (id ? renderIntegration(main, id) : renderIntegrations(main)) },
195
200
  account: { label: 'Account', icon: 'person', group: 'Settings', render: renderAccount },
@@ -203,6 +208,9 @@ const ICON_PATHS = {
203
208
  people: '<circle cx="9" cy="8" r="3"/><path d="M3 20c0-3.5 2.7-6 6-6s6 2.5 6 6"/><path d="M16 5a3 3 0 010 6M21 20c0-3-1.6-5-4-5.7"/>',
204
209
  plug: '<path d="M9 3v5M15 3v5M6 8h12v3a6 6 0 01-12 0zM12 17v4"/>',
205
210
  person: '<circle cx="12" cy="8" r="4"/><path d="M4 21c0-4 3.6-7 8-7s8 3 8 7"/>',
211
+ flag: '<path d="M5 21V4M5 4h11l-2 4 2 4H5"/>',
212
+ search: '<circle cx="11" cy="11" r="6.5"/><path d="M16 16l4.5 4.5"/>',
213
+ menu: '<path d="M4 6h16M4 12h16M4 18h16"/>',
206
214
  palette: '<path d="M12 3a9 9 0 100 18c1.1 0 1.5-.8 1.5-1.6 0-1-.8-1.4-.8-2.4 0-.9.7-1.5 1.6-1.5H17a4 4 0 004-4c0-4.7-4-8.5-9-8.5z"/><circle cx="7.5" cy="11" r="1"/><circle cx="10" cy="7" r="1"/><circle cx="15" cy="7" r="1"/>',
207
215
  };
208
216
 
@@ -212,6 +220,87 @@ function icon(name) {
212
220
  return node;
213
221
  }
214
222
 
223
+ // The menu button at the top of the sidebar. On a wide screen it folds the
224
+ // sidebar to its icons and back, and the choice stays in this browser. On a
225
+ // narrow screen the sidebar is a bar across the top, and the button opens
226
+ // the menu below it; choosing a page, Escape or a click outside closes it.
227
+ const NARROW = window.matchMedia('(max-width: 820px)');
228
+ const FOLDED_KEY = 'flow_sidebar_folded';
229
+
230
+ function readFolded() {
231
+ try { return localStorage.getItem(FOLDED_KEY) === '1'; } catch { return false; }
232
+ }
233
+
234
+ function drawMenu() {
235
+ const shell = $('shell');
236
+ const folded = readFolded();
237
+ shell.toggleAttribute('data-folded', folded);
238
+ const open = NARROW.matches ? shell.hasAttribute('data-menu-open') : !folded;
239
+ const toggle = $('menu-toggle');
240
+ const label = NARROW.matches ? (open ? 'Close the menu' : 'Open the menu') : (folded ? 'Show the menu' : 'Fold the menu');
241
+ toggle.setAttribute('aria-expanded', String(open));
242
+ toggle.setAttribute('aria-label', label);
243
+ toggle.title = label;
244
+ // Folded, a page's name shows when the pointer rests on its icon.
245
+ for (const link of document.querySelectorAll('[data-nav]')) {
246
+ if (folded && !NARROW.matches) link.title = link.dataset.label;
247
+ else link.removeAttribute('title');
248
+ }
249
+ }
250
+
251
+ function closeMenu() {
252
+ if (!$('shell').hasAttribute('data-menu-open')) return;
253
+ $('shell').removeAttribute('data-menu-open');
254
+ drawMenu();
255
+ }
256
+
257
+ function toggleMenu() {
258
+ if (NARROW.matches) {
259
+ $('shell').toggleAttribute('data-menu-open');
260
+ } else {
261
+ try { localStorage.setItem(FOLDED_KEY, readFolded() ? '0' : '1'); } catch { /* the choice is only a convenience */ }
262
+ }
263
+ drawMenu();
264
+ }
265
+
266
+ NARROW.addEventListener('change', () => { $('shell').removeAttribute('data-menu-open'); drawMenu(); });
267
+ document.addEventListener('keydown', (event) => {
268
+ if (event.key === 'Escape' && $('shell')?.hasAttribute('data-menu-open')) {
269
+ closeMenu();
270
+ $('menu-toggle').focus();
271
+ }
272
+ });
273
+ document.addEventListener('click', (event) => {
274
+ if (!event.target.closest?.('.fx-side') || event.target.closest?.('[data-nav]')) closeMenu();
275
+ });
276
+
277
+ // Search, from the sidebar foot or Ctrl+K (⌘K): the pages this person may
278
+ // open, invoices and jobs (search.js).
279
+ function search() {
280
+ const views = Object.entries(VIEWS)
281
+ .filter(([, view]) => !view.ownerOnly || me?.person.role === 'owner')
282
+ .map(([name, view]) => ({ name, label: view.label, icon: view.icon }));
283
+ openSearch({ h, flow, go, icon, views });
284
+ }
285
+
286
+ document.addEventListener('keydown', (event) => {
287
+ if (event.key.toLowerCase() !== 'k' || !(event.metaKey || event.ctrlKey) || event.altKey || event.shiftKey) return;
288
+ if (document.body.dataset.state !== 'signed-in') return;
289
+ event.preventDefault();
290
+ search();
291
+ });
292
+
293
+ // "Report a problem" in Clerk's account menu, between "Manage account" and
294
+ // "Sign out". Clerk mounts and unmounts the icon itself.
295
+ function reportMenuItem(openReport) {
296
+ return {
297
+ label: 'Report a problem',
298
+ onClick: () => { openReport().catch((error) => toast(error.message, 'danger')); },
299
+ mountIcon: (element) => element.replaceChildren(icon('flag')),
300
+ unmountIcon: (element) => element.replaceChildren(),
301
+ };
302
+ }
303
+
215
304
  // The view and, for one record, its id: /invoices/<id> is ['invoices', '<id>'].
216
305
  function currentView() {
217
306
  const [name, id = null] = location.pathname.replace(/^\/+|\/+$/g, '').split('/');
@@ -225,7 +314,7 @@ async function route() {
225
314
  const [view, id] = currentView();
226
315
  const path = id ? `/${view}/${id}` : `/${view}`;
227
316
  if (location.pathname !== path) history.replaceState(null, '', path);
228
- for (const link of document.querySelectorAll('[data-nav]')) {
317
+ for (const link of document.querySelectorAll('a[data-nav]')) {
229
318
  link.setAttribute('aria-current', link.dataset.nav === view ? 'page' : 'false');
230
319
  }
231
320
  main.replaceChildren();
@@ -237,7 +326,7 @@ async function route() {
237
326
 
238
327
  // Opens a path in place; the address changes, the page stays.
239
328
  function go(path, { replace = false } = {}) {
240
- if (path !== location.pathname) history[replace ? 'replaceState' : 'pushState'](null, '', path);
329
+ if (path !== location.pathname + location.search) history[replace ? 'replaceState' : 'pushState'](null, '', path);
241
330
  route();
242
331
  }
243
332
 
@@ -281,10 +370,34 @@ function renderShell() {
281
370
  if (view.ownerOnly && me?.person.role !== 'owner') continue;
282
371
  if (view.group && view.group !== group) items.push(h('span', { class: 'fx-nav__group' }, view.group));
283
372
  group = view.group;
284
- items.push(h('a', { href: `/${name}`, 'data-nav': name, onclick: navigate }, icon(view.icon), h('span', {}, view.label)));
373
+ items.push(h('a', { href: `/${name}`, 'data-nav': name, 'data-label': view.label, onclick: navigate }, icon(view.icon), h('span', { class: 'fx-nav__label' }, view.label)));
285
374
  }
286
- nav.replaceChildren(...items);
287
- mountClerk('userButton', $('user-button'), { afterSignOutUrl: '/sign-in', showName: false });
375
+ // "Report a problem" sits at the foot of the menu, above the account chip,
376
+ // while Sentry and its report form are on (monitoring.js).
377
+ const report = h('button', { type: 'button', class: 'fx-nav__report', 'data-nav': 'report', 'data-label': 'Report a problem', hidden: true },
378
+ icon('flag'), h('span', { class: 'fx-nav__label' }, 'Report a problem'));
379
+ nav.replaceChildren(...items, report);
380
+ $('menu-toggle').replaceChildren(icon('menu'));
381
+ $('menu-toggle').onclick = toggleMenu;
382
+ drawMenu();
383
+ // The sidebar foot shows who is signed in: photo or initial, then the name
384
+ // (the email when the account has no name). It opens the account menu.
385
+ const userButton = { afterSignOutUrl: '/sign-in', showName: true };
386
+ $('search').replaceChildren(icon('search'));
387
+ $('search').onclick = search;
388
+ mountClerk('userButton', $('user-button'), userButton);
389
+ // Errors and problem reports go to the installation's own Sentry, when the
390
+ // Sentry integration is on (monitoring.js). Then "Report a problem" shows
391
+ // in the menu and in the account menu. Flow works the same without it.
392
+ startMonitoring({ flow, person: me.person })
393
+ .then((openReport) => {
394
+ if (!openReport) return;
395
+ report.hidden = false;
396
+ report.onclick = () => { openReport().catch((error) => toast(error.message, 'danger')); };
397
+ unmountClerk($('user-button'));
398
+ mountClerk('userButton', $('user-button'), { ...userButton, customMenuItems: [reportMenuItem(openReport)] });
399
+ })
400
+ .catch((error) => console.warn(`[monitoring] ${error.message}`));
288
401
  window.addEventListener('popstate', route);
289
402
  return route();
290
403
  }
@@ -405,8 +518,9 @@ const STATE_TONE = { ready: 'success', disabled: 'mute', failed: 'danger', resta
405
518
  const CATALOGUE = {
406
519
  xero: { maker: 'Xero', category: 'accounting', blurb: 'Send invoices to Xero as drafts and bring payments back.', logo: 'xero.svg' },
407
520
  seazona: { maker: 'Seazona', category: 'lab', blurb: 'Bring lab cases and orders into Flow as jobs.', logo: 'seazona.svg' },
521
+ sentry: { maker: 'Sentry', category: 'monitoring', blurb: 'Send errors and problem reports to your own Sentry project.', logo: 'sentry.svg' },
408
522
  };
409
- const CATEGORIES = [['all', 'All'], ['accounting', 'Accounting'], ['lab', 'Lab software']];
523
+ const CATEGORIES = [['all', 'All'], ['accounting', 'Accounting'], ['lab', 'Lab software'], ['monitoring', 'Monitoring']];
410
524
  const CATEGORY_LABEL = Object.fromEntries(CATEGORIES);
411
525
 
412
526
  function integrationLogo(integration, size = '') {
@@ -626,6 +740,8 @@ async function renderIntegration(main, id) {
626
740
  !body.secretsKey ? notice('Flow has no secrets key, so it cannot store a client secret. Ask whoever hosts Flow to set FLOW_SECRETS_KEY (64 hex characters), then restart Flow.', 'warn', 'No secrets key.') : null,
627
741
  current.state === 'failed' && current.error ? notice(current.error, 'danger', 'It could not start.') : null,
628
742
  current.id === 'xero' && current.state === 'ready' ? h('section', { class: 'fx-int-section' }, xeroConnection(body.organisations, canChange)) : null,
743
+ current.id === 'seazona' ? h('section', { class: 'fx-int-section fx-card', style: { padding: '8px 20px 16px' } }, automationSection(ui(), 'seazona', 'Ready to invoice')) : null,
744
+ current.id === 'xero' ? h('section', { class: 'fx-int-section fx-card', style: { padding: '8px 20px 16px' } }, sendModeSection(ui())) : null,
629
745
  guide ? h('section', { class: 'fx-int-section' },
630
746
  h('h2', { class: 'fx-int-section__title' }, 'Set up'),
631
747
  h('ol', { class: 'fx-steps' }, guide.steps.map((step) => h('li', {}, step))),
@@ -0,0 +1,96 @@
1
+ // Automation rows (server/core/automations.js, /api/invoicing/automations).
2
+ // Each automation shows where its trigger lives: the work system's ready rule
3
+ // on that integration's page, the draft rule on Settings, Invoicing. One row
4
+ // for each: its name, one short line, and its switch, plus a row for each of
5
+ // its settings. A change saves at once. Everyone can read them; an owner or
6
+ // admin changes them. The caller passes in app.js's shared pieces as `ui`.
7
+
8
+ const DOCS = 'https://github.com/arcforgelabs/flow/blob/main/docs/AUTOMATIONS.md';
9
+
10
+ // A section of the automations kept at `place`, filled when they load.
11
+ export function automationSection(ui, place, title) {
12
+ const { h, flow, toast, notice, me } = ui;
13
+ const section = h('section', { class: 'fx-set' }, h('h2', { class: 'fx-set__title' }, title), h('div', { class: 'fx-skeleton' }));
14
+ const disabled = !['owner', 'admin'].includes(me?.person.role);
15
+
16
+ async function save(key, changes, control) {
17
+ control.disabled = true;
18
+ try {
19
+ const { automation } = await flow(`/api/invoicing/automations/${key}`, { method: 'PUT', body: JSON.stringify(changes) });
20
+ toast('Saved.', 'success');
21
+ return automation;
22
+ } catch (error) {
23
+ toast(error.message, 'danger');
24
+ return null;
25
+ } finally {
26
+ control.disabled = disabled;
27
+ }
28
+ }
29
+
30
+ const row = (name, hint, control) => h('div', { class: 'fx-set__row' },
31
+ h('div', { class: 'fx-set__text' }, h('span', { class: 'fx-set__name' }, name), h('span', { class: 'fx-set__hint' }, hint)),
32
+ h('div', { class: 'fx-set__control' }, control));
33
+
34
+ // A menu of the statuses the work system reported, each with its job count.
35
+ // A status saved earlier stays in the menu even when no job has it now.
36
+ const statusPicker = (automation) => {
37
+ const summary = h('summary', { class: 'fx-select fx-multi__summary' });
38
+ const list = h('div', { class: 'fx-multi__list', role: 'group', 'aria-label': 'Ready statuses' }, h('div', { class: 'fx-skeleton' }));
39
+ const picker = h('details', { class: 'fx-multi' }, summary, list);
40
+ let options = [];
41
+ const show = () => {
42
+ const chosen = new Set(automation.config.statuses);
43
+ const names = options.filter((o) => chosen.has(o.value)).map((o) => o.label);
44
+ summary.textContent = names.length ? names.join(', ') : 'None';
45
+ list.replaceChildren(...(options.length ? options.map((o) => {
46
+ const box = h('input', { type: 'checkbox', checked: chosen.has(o.value), disabled });
47
+ box.addEventListener('change', async () => {
48
+ const next = options.map((x) => x.value).filter((v) => (v === o.value ? box.checked : chosen.has(v)));
49
+ if (!next.length) {
50
+ box.checked = true;
51
+ toast('Pick at least one status. To stop it, turn the automation off.', 'danger');
52
+ return;
53
+ }
54
+ const saved = await save(automation.key, { config: { statuses: next } }, box);
55
+ if (saved) automation.config = saved.config;
56
+ show();
57
+ });
58
+ return h('label', { class: 'fx-multi__option' }, box, h('span', {}, o.label), h('span', { class: 'fx-dim' }, String(o.jobs)));
59
+ }) : [h('p', { class: 'fx-dim', style: { margin: 0 } }, 'Seazona has reported no statuses yet.')]));
60
+ };
61
+ (async () => {
62
+ try {
63
+ ({ statuses: options } = await flow('/api/invoicing/work-statuses'));
64
+ } catch (error) {
65
+ options = [];
66
+ toast(error.message, 'danger');
67
+ }
68
+ const known = new Set(options.map((o) => o.value));
69
+ for (const value of automation.config.statuses) if (!known.has(value)) options.push({ value, label: value, jobs: 0 });
70
+ show();
71
+ })();
72
+ return picker;
73
+ };
74
+
75
+ const rowsOf = (automation) => {
76
+ const button = h('button', { class: 'fx-switch', type: 'button', role: 'switch', 'aria-label': automation.name, 'aria-checked': String(automation.enabled), disabled });
77
+ button.addEventListener('click', async () => {
78
+ const saved = await save(automation.key, { enabled: button.getAttribute('aria-checked') !== 'true' }, button);
79
+ if (saved) button.setAttribute('aria-checked', String(saved.enabled));
80
+ });
81
+ const out = [row(automation.name, automation.hint, button)];
82
+ if (automation.config.statuses) out.push(row('Ready statuses', 'Picked from the statuses Seazona reports.', statusPicker(automation)));
83
+ return out;
84
+ };
85
+
86
+ (async () => {
87
+ try {
88
+ const { automations } = await flow(`/api/invoicing/automations?place=${encodeURIComponent(place)}`);
89
+ section.replaceChildren(h('h2', { class: 'fx-set__title' }, title), ...automations.flatMap(rowsOf),
90
+ h('p', { class: 'fx-dim', style: { margin: '8px 0 0' } }, h('a', { href: DOCS, target: '_blank', rel: 'noopener' }, 'How automations work')));
91
+ } catch (error) {
92
+ section.replaceChildren(h('h2', { class: 'fx-set__title' }, title), notice(error.message, 'danger'));
93
+ }
94
+ })();
95
+ return section;
96
+ }
@@ -11,6 +11,9 @@ const gearIcon = {
11
11
  '&& > *': { visibility: 'hidden' },
12
12
  };
13
13
 
14
+ // Matches the narrow-screen rule in flow.css, where the sidebar becomes a bar.
15
+ const NARROW = '@media (max-width: 820px)';
16
+
14
17
  function token(name, fallback) {
15
18
  const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
16
19
  return value || fallback;
@@ -42,6 +45,7 @@ export function flowClerkAppearance({ embedded = false, publicSignUp = false } =
42
45
  const inputEdge = { ...controlEdge, '&&[data-feedback="error"]': { boxShadow: edge(danger) } };
43
46
  const display = token('--t-display', 'ui-serif, Georgia, serif');
44
47
  const dim = token('--t-dim', '#5b6773');
48
+ const paper = token('--t-paper', '#ffffff');
45
49
  return {
46
50
  variables: {
47
51
  colorPrimary: token('--t-primary', '#1f6f8f'),
@@ -84,6 +88,23 @@ export function flowClerkAppearance({ embedded = false, publicSignUp = false } =
84
88
  badge: { '&&': { color: dim, background: `color-mix(in srgb, ${text} 5%, transparent)`, borderColor: line } },
85
89
  formButtonPrimary: { boxShadow: 'none', textTransform: 'none', fontWeight: 650 },
86
90
  socialButtonsBlockButton: controlEdge,
91
+ // The account button in the sidebar foot is a chip: photo or initial,
92
+ // then the name (Clerk shows the email when there is no name), clipped
93
+ // with an ellipsis to fit. The top bar on narrow screens has no room for
94
+ // the name, so there it is the photo or initial alone.
95
+ userButtonTrigger: {
96
+ '&&': {
97
+ width: '100%', minWidth: 0, justifyContent: 'flex-start', padding: '4px 10px 4px 4px', borderRadius: token('--t-radius-lg', '10px'), background: paper, boxShadow: `inset 0 0 0 1px ${line}`,
98
+ '&:hover': { background: `color-mix(in srgb, ${text} 6%, ${paper})` },
99
+ [NARROW]: { width: 'auto', padding: 0, borderRadius: '50%', background: 'transparent', boxShadow: 'none', '&:hover': { background: 'transparent' } },
100
+ },
101
+ },
102
+ userButtonBox: { flexDirection: 'row-reverse', justifyContent: 'flex-end', minWidth: 0, gap: '8px' },
103
+ userButtonOuterIdentifier: {
104
+ minWidth: 0, paddingInlineStart: 0, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
105
+ color: token('--t-strong', text), fontSize: '14px', fontWeight: 600,
106
+ [NARROW]: { display: 'none' },
107
+ },
87
108
  userButtonPopoverCard: { border: `1px solid ${line}`, boxShadow: shadow },
88
109
  userButtonPopoverActionButtonIcon__manageAccount: gearIcon,
89
110
  footer: embedded ? { display: 'none' } : undefined,