cursedbelt 5.69.0 → 5.71.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/dist/react/index.d.ts +1 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +1 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/look/AppearanceFields.d.ts +10 -0
- package/dist/react/look/AppearanceFields.d.ts.map +1 -0
- package/dist/react/look/AppearanceFields.js +18 -0
- package/dist/react/look/AppearanceFields.js.map +1 -0
- package/dist/react/look/AppearancePanel.d.ts.map +1 -1
- package/dist/react/look/AppearancePanel.js +3 -10
- package/dist/react/look/AppearancePanel.js.map +1 -1
- package/dist/react/look/AppearanceSheet.d.ts +26 -0
- package/dist/react/look/AppearanceSheet.d.ts.map +1 -0
- package/dist/react/look/AppearanceSheet.js +32 -0
- package/dist/react/look/AppearanceSheet.js.map +1 -0
- package/dist/react/look/AppearanceSheetPanel.d.ts +14 -0
- package/dist/react/look/AppearanceSheetPanel.d.ts.map +1 -0
- package/dist/react/look/AppearanceSheetPanel.js +12 -0
- package/dist/react/look/AppearanceSheetPanel.js.map +1 -0
- package/dist/react/notifications/binding.d.ts +7 -2
- package/dist/react/notifications/binding.d.ts.map +1 -1
- package/dist/react/notifications/binding.js +7 -2
- package/dist/react/notifications/binding.js.map +1 -1
- package/dist/react/notifications/index.d.ts +1 -0
- package/dist/react/notifications/index.d.ts.map +1 -1
- package/dist/react/notifications/index.js +1 -0
- package/dist/react/notifications/index.js.map +1 -1
- package/dist/react/notifications/useNotificationsBinding.d.ts +24 -0
- package/dist/react/notifications/useNotificationsBinding.d.ts.map +1 -0
- package/dist/react/notifications/useNotificationsBinding.js +35 -0
- package/dist/react/notifications/useNotificationsBinding.js.map +1 -0
- package/dist/styles-areas/analytics.css +13 -19
- package/dist/styles-areas/auth.css +3 -3
- package/dist/styles-areas/calendar.css +12 -12
- package/dist/styles-areas/core.css +43 -39
- package/dist/styles-areas/data-table.css +13 -18
- package/dist/styles-areas/fields.css +16 -21
- package/dist/styles-areas/filter-rail.css +2 -2
- package/dist/styles-areas/nav.css +10 -14
- package/dist/styles-areas/overlays.css +3 -8
- package/dist/styles-areas/place-bar.css +3 -8
- package/dist/styles-areas/problem-detail.css +3 -3
- package/dist/styles-areas/sharing.css +3 -7
- package/dist/styles-areas/test-report.css +2 -7
- package/dist/styles-areas/wizard.css +6 -10
- package/dist/styles-areas/workbench.css +10 -14
- package/package.json +1 -1
- package/src/react/index.ts +1 -0
- package/src/react/look/AppearanceFields.tsx +33 -0
- package/src/react/look/AppearanceMenu.spec.tsx +45 -1
- package/src/react/look/AppearancePanel.tsx +2 -22
- package/src/react/look/AppearanceSheet.tsx +45 -0
- package/src/react/look/AppearanceSheetPanel.tsx +23 -0
- package/src/react/notifications/binding.ts +7 -2
- package/src/react/notifications/index.ts +1 -0
- package/src/react/notifications/useNotificationsBinding.spec.tsx +107 -0
- package/src/react/notifications/useNotificationsBinding.ts +35 -0
- package/src/styles-areas/analytics.css +13 -19
- package/src/styles-areas/auth.css +3 -3
- package/src/styles-areas/calendar.css +12 -12
- package/src/styles-areas/core.css +43 -39
- package/src/styles-areas/data-table.css +13 -18
- package/src/styles-areas/fields.css +16 -21
- package/src/styles-areas/filter-rail.css +2 -2
- package/src/styles-areas/nav.css +10 -14
- package/src/styles-areas/overlays.css +3 -8
- package/src/styles-areas/place-bar.css +3 -8
- package/src/styles-areas/problem-detail.css +3 -3
- package/src/styles-areas/sharing.css +3 -7
- package/src/styles-areas/test-report.css +2 -7
- package/src/styles-areas/wizard.css +6 -10
- package/src/styles-areas/workbench.css +10 -14
|
@@ -3,6 +3,7 @@ import { cleanup, fireEvent, render } from '@testing-library/react';
|
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
4
|
import { colorSchemeStore, initColorScheme } from 'cursedbelt-core/color-scheme';
|
|
5
5
|
import { AppearanceMenu } from './AppearanceMenu.js';
|
|
6
|
+
import { AppearanceSheet } from './AppearanceSheet.js';
|
|
6
7
|
import { initLook, LOOK_STORAGE_KEY, LOOKS, lookClass, resetLook } from './lookStore.js';
|
|
7
8
|
|
|
8
9
|
afterEach(() => {
|
|
@@ -65,6 +66,34 @@ describe('AppearanceMenu', () => {
|
|
|
65
66
|
});
|
|
66
67
|
});
|
|
67
68
|
|
|
69
|
+
describe('AppearanceSheet', () => {
|
|
70
|
+
test('draws nothing while closed; opened, the same looks and scheme in a sheet named Appearance', async () => {
|
|
71
|
+
initLook({ fallback: 'obsidian' });
|
|
72
|
+
initColorScheme();
|
|
73
|
+
const closed = render(<AppearanceSheet open={false} onClose={() => {}} />);
|
|
74
|
+
expect(closed.container.innerHTML).toBe('');
|
|
75
|
+
expect(closed.queryByRole('dialog')).toBeNull();
|
|
76
|
+
closed.unmount();
|
|
77
|
+
|
|
78
|
+
const { findByRole, getByRole, getAllByRole } = render(<AppearanceSheet open onClose={() => {}} />);
|
|
79
|
+
expect(await findByRole('dialog', { name: 'Appearance' })).not.toBeNull();
|
|
80
|
+
const radios = getAllByRole('radio').map((r) => r.textContent ?? '');
|
|
81
|
+
for (const look of LOOKS) expect(radios.some((t) => t.includes(look.label))).toBe(true);
|
|
82
|
+
expect(getByRole('tablist', { name: 'Light or dark' })).not.toBeNull();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('a look picked in the sheet is worn and kept; Close hands back to the caller', async () => {
|
|
86
|
+
initLook({ fallback: 'obsidian' });
|
|
87
|
+
let closes = 0;
|
|
88
|
+
const { findByRole, getByRole } = render(<AppearanceSheet open onClose={() => closes++} />);
|
|
89
|
+
fireEvent.click(await findByRole('radio', { name: /Dracula/ }));
|
|
90
|
+
expect(document.documentElement.classList.contains('cb-dracula')).toBe(true);
|
|
91
|
+
expect(localStorage.getItem(LOOK_STORAGE_KEY)).toBe('dracula');
|
|
92
|
+
fireEvent.click(getByRole('button', { name: 'Close' }));
|
|
93
|
+
expect(closes).toBe(1);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
68
97
|
// 🔴 The panel's Popover, RadioGroup and Tabs cost every app ~21 kB gzip of ENTRY chunk when this
|
|
69
98
|
// file imported them (arch, 2026-10-07: 157,950 B with the menu vs 136,443 B with ThemeToggle).
|
|
70
99
|
// They may reach the button only through the dynamic import of ./AppearancePanel.
|
|
@@ -72,7 +101,15 @@ const STATIC_IMPORT = /^import\s[^;]*?from\s+'([^']+)'/gms;
|
|
|
72
101
|
function staticImportsOf(source: string): string[] {
|
|
73
102
|
return [...source.matchAll(STATIC_IMPORT)].filter((m) => !m[0].startsWith('import type')).map((m) => m[1] ?? '');
|
|
74
103
|
}
|
|
75
|
-
const PANEL_ONLY = [
|
|
104
|
+
const PANEL_ONLY = [
|
|
105
|
+
'./AppearancePanel.js',
|
|
106
|
+
'./AppearanceSheetPanel.js',
|
|
107
|
+
'./AppearanceFields.js',
|
|
108
|
+
'../components/Popover.js',
|
|
109
|
+
'../components/Drawer.js',
|
|
110
|
+
'../components/SegmentedControl.js',
|
|
111
|
+
'./LookPicker.js',
|
|
112
|
+
];
|
|
76
113
|
|
|
77
114
|
describe('AppearanceMenu stays out of the entry chunk', () => {
|
|
78
115
|
test('its module imports none of the panel statically', () => {
|
|
@@ -83,6 +120,13 @@ describe('AppearanceMenu stays out of the entry chunk', () => {
|
|
|
83
120
|
expect(source).toContain("import('./AppearancePanel.js')");
|
|
84
121
|
});
|
|
85
122
|
|
|
123
|
+
test('nor does the sheet: its Drawer and fields arrive through the dynamic import', () => {
|
|
124
|
+
const source = readFileSync(new URL('./AppearanceSheet.tsx', import.meta.url), 'utf8');
|
|
125
|
+
const imported = staticImportsOf(source);
|
|
126
|
+
for (const heavy of PANEL_ONLY) expect(imported).not.toContain(heavy);
|
|
127
|
+
expect(source).toContain("import('./AppearanceSheetPanel.js')");
|
|
128
|
+
});
|
|
129
|
+
|
|
86
130
|
test('🔴 the failure path: a static import of the panel is caught', () => {
|
|
87
131
|
const regressed = "import { Popover } from '../components/Popover.js';\nimport { IconButton } from '../components/IconButton.js';";
|
|
88
132
|
expect(PANEL_ONLY.some((heavy) => staticImportsOf(regressed).includes(heavy))).toBe(true);
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import { useColorScheme } from '../colorScheme/useColorScheme.js';
|
|
3
2
|
import { Popover } from '../components/Popover.js';
|
|
4
|
-
import {
|
|
5
|
-
import { LookPicker } from './LookPicker.js';
|
|
3
|
+
import { AppearanceFields } from './AppearanceFields.js';
|
|
6
4
|
import type { LookId } from './lookStore.js';
|
|
7
5
|
|
|
8
6
|
export interface AppearancePanelProps {
|
|
@@ -21,7 +19,6 @@ export interface AppearancePanelProps {
|
|
|
21
19
|
* arch, 2026-10-07, forge task 2916). Not exported: `AppearanceMenu` is the API.
|
|
22
20
|
*/
|
|
23
21
|
export default function AppearancePanel({ trigger, open, onOpenChange, label, looks, align }: AppearancePanelProps) {
|
|
24
|
-
const { mode, setMode } = useColorScheme();
|
|
25
22
|
return (
|
|
26
23
|
<Popover
|
|
27
24
|
trigger={trigger}
|
|
@@ -31,24 +28,7 @@ export default function AppearancePanel({ trigger, open, onOpenChange, label, lo
|
|
|
31
28
|
label={label}
|
|
32
29
|
width='min(20rem, calc(100vw - 1rem))'
|
|
33
30
|
>
|
|
34
|
-
<
|
|
35
|
-
<LookPicker looks={looks} label='Look' columns={2} />
|
|
36
|
-
<div className='flex flex-col gap-1.5'>
|
|
37
|
-
<span className='text-sm font-medium text-foreground'>Light or dark</span>
|
|
38
|
-
<SegmentedControl
|
|
39
|
-
size='sm'
|
|
40
|
-
aria-label='Light or dark'
|
|
41
|
-
value={mode}
|
|
42
|
-
onChange={setMode}
|
|
43
|
-
options={[
|
|
44
|
-
{ value: 'light', label: 'Light' },
|
|
45
|
-
{ value: 'dark', label: 'Dark' },
|
|
46
|
-
{ value: 'system', label: 'System' },
|
|
47
|
-
]}
|
|
48
|
-
/>
|
|
49
|
-
</div>
|
|
50
|
-
<p className='text-xs text-muted-foreground'>Kept on this device.</p>
|
|
51
|
-
</div>
|
|
31
|
+
<AppearanceFields looks={looks} />
|
|
52
32
|
</Popover>
|
|
53
33
|
);
|
|
54
34
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Suspense, useState } from 'react';
|
|
2
|
+
import { lazyWithReload } from '../lazy/staleChunk.js';
|
|
3
|
+
import type { LookId } from './lookStore.js';
|
|
4
|
+
|
|
5
|
+
/** The sheet, fetched the first time it is opened (or preloaded) — never in the entry chunk. */
|
|
6
|
+
const loadSheet = () => import('./AppearanceSheetPanel.js');
|
|
7
|
+
const AppearanceSheetPanel = lazyWithReload(loadSheet);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fetch the sheet's chunk ahead of the press — call it when the row that opens the sheet is drawn
|
|
11
|
+
* (an account menu opening), so the sheet slides up on the tap instead of after a round trip.
|
|
12
|
+
*/
|
|
13
|
+
export function preloadAppearanceSheet(): void {
|
|
14
|
+
void loadSheet();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AppearanceSheetProps {
|
|
18
|
+
open: boolean;
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
/** The sheet's title and accessible name. Default 'Appearance'. */
|
|
21
|
+
label?: string;
|
|
22
|
+
/** Which looks to offer, in order. Default: every look the stylesheets ship. */
|
|
23
|
+
looks?: readonly LookId[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* {@link AppearanceMenu}'s choice — the LOOK cards and Light / Dark / System — as a controlled
|
|
28
|
+
* bottom sheet, for a phone bar with no room for the menu's icon button. Open it from a row in
|
|
29
|
+
* the account menu ("Appearance…"), as Linear's and Obsidian's phone apps reach their theme
|
|
30
|
+
* settings from the account / settings list rather than the toolbar (forge task 2932).
|
|
31
|
+
*
|
|
32
|
+
* Renders nothing until first opened; from then on the lazy panel stays mounted so the close
|
|
33
|
+
* animates. Reads and writes the same stores as the menu, so a look picked here is the one the
|
|
34
|
+
* desktop bar shows.
|
|
35
|
+
*/
|
|
36
|
+
export function AppearanceSheet({ open, onClose, label = 'Appearance', looks }: AppearanceSheetProps) {
|
|
37
|
+
const [wanted, setWanted] = useState(open);
|
|
38
|
+
if (open && !wanted) setWanted(true);
|
|
39
|
+
if (!wanted) return null;
|
|
40
|
+
return (
|
|
41
|
+
<Suspense fallback={null}>
|
|
42
|
+
<AppearanceSheetPanel open={open} onClose={onClose} label={label} looks={looks} />
|
|
43
|
+
</Suspense>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Drawer } from '../components/Drawer.js';
|
|
2
|
+
import { AppearanceFields } from './AppearanceFields.js';
|
|
3
|
+
import type { LookId } from './lookStore.js';
|
|
4
|
+
|
|
5
|
+
export interface AppearanceSheetPanelProps {
|
|
6
|
+
open: boolean;
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
label: string;
|
|
9
|
+
looks?: readonly LookId[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* `AppearanceSheet`'s body — the same fields as the TopBar's popover, in a bottom sheet. Its own
|
|
14
|
+
* module for the reason `AppearancePanel` is: the Drawer, radio group and tabs load when the
|
|
15
|
+
* sheet is first asked for, never with the entry. Not exported: `AppearanceSheet` is the API.
|
|
16
|
+
*/
|
|
17
|
+
export default function AppearanceSheetPanel({ open, onClose, label, looks }: AppearanceSheetPanelProps) {
|
|
18
|
+
return (
|
|
19
|
+
<Drawer open={open} onClose={onClose} side='bottom' size='md' title={label}>
|
|
20
|
+
<AppearanceFields looks={looks} />
|
|
21
|
+
</Drawer>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -49,8 +49,13 @@ export interface NotificationsBinding {
|
|
|
49
49
|
* transport feeding rows in, and the store's config hooks pushing
|
|
50
50
|
* read/dismiss back out.
|
|
51
51
|
*
|
|
52
|
-
* const
|
|
53
|
-
* <NotificationBell store={store}
|
|
52
|
+
* const binding = useNotificationsBinding(); // in a component — never during render
|
|
53
|
+
* if (binding) return <NotificationBell store={binding.store} />;
|
|
54
|
+
*
|
|
55
|
+
* 🔴 This function CONNECTS when called, so a component must not call it while
|
|
56
|
+
* rendering: React discards renders, and each discarded one would leak an open
|
|
57
|
+
* stream. {@link useNotificationsBinding} builds it in an effect and disposes it
|
|
58
|
+
* on unmount; call this directly only outside React, or as a page-long singleton.
|
|
54
59
|
*
|
|
55
60
|
* ── The one asymmetry worth knowing ─────────────────────────────────────────
|
|
56
61
|
* Rows flow IN through the transport; acknowledgments flow OUT through these
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test';
|
|
2
|
+
import { act, cleanup, render } from '@testing-library/react';
|
|
3
|
+
import { StrictMode, Suspense } from 'react';
|
|
4
|
+
import type { NotificationsBindingClient } from './binding.js';
|
|
5
|
+
import type { NotificationTransport } from './notificationStore.js';
|
|
6
|
+
import { useNotificationsBinding } from './useNotificationsBinding.js';
|
|
7
|
+
|
|
8
|
+
afterEach(cleanup);
|
|
9
|
+
|
|
10
|
+
const client = {
|
|
11
|
+
queries: { list: async () => ({ items: [], hasMore: false }) },
|
|
12
|
+
mutations: {
|
|
13
|
+
markRead: async () => ({ changed: true }),
|
|
14
|
+
markAllRead: async () => ({ changed: 1 }),
|
|
15
|
+
setDone: async () => ({ changed: true }),
|
|
16
|
+
doneAll: async () => ({ changed: 1 }),
|
|
17
|
+
remove: async () => ({ changed: true }),
|
|
18
|
+
},
|
|
19
|
+
} as unknown as NotificationsBindingClient;
|
|
20
|
+
|
|
21
|
+
/** An inert transport that counts what it opened and what was closed — the
|
|
22
|
+
* stand-in for the EventSource a leaked binding leaves holding a connection. */
|
|
23
|
+
function countingTransport() {
|
|
24
|
+
const counts = { connects: 0, disposes: 0 };
|
|
25
|
+
const transport: NotificationTransport = {
|
|
26
|
+
connect: () => {
|
|
27
|
+
counts.connects += 1;
|
|
28
|
+
return () => {
|
|
29
|
+
counts.disposes += 1;
|
|
30
|
+
};
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
return { counts, transport, open: () => counts.connects - counts.disposes };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function Bell({ transport }: { transport: NotificationTransport }) {
|
|
37
|
+
const binding = useNotificationsBinding({ client, transport });
|
|
38
|
+
return <output>{binding ? 'bound' : 'pending'}</output>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Suspends its first `times` renders, then renders nothing — the lazy
|
|
42
|
+
* switcher next to family's bell that made React discard the bell's render. */
|
|
43
|
+
function suspender(times: number) {
|
|
44
|
+
let left = times;
|
|
45
|
+
let release: () => void = () => {};
|
|
46
|
+
function Sibling(): null {
|
|
47
|
+
if (left > 0) {
|
|
48
|
+
throw new Promise<void>((resolve) => {
|
|
49
|
+
release = () => {
|
|
50
|
+
left -= 1;
|
|
51
|
+
resolve();
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return { Sibling, release: () => release() };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('useNotificationsBinding', () => {
|
|
61
|
+
test('🔴 a render React throws away opens no stream — one binding, and it closes on unmount', async () => {
|
|
62
|
+
const { counts, transport, open } = countingTransport();
|
|
63
|
+
const { Sibling, release } = suspender(3);
|
|
64
|
+
const view = render(
|
|
65
|
+
<Suspense fallback={<span>loading</span>}>
|
|
66
|
+
<Bell transport={transport} />
|
|
67
|
+
<Sibling />
|
|
68
|
+
</Suspense>,
|
|
69
|
+
);
|
|
70
|
+
for (let retry = 0; retry < 3; retry += 1) {
|
|
71
|
+
await act(async () => {
|
|
72
|
+
release();
|
|
73
|
+
await Promise.resolve();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
expect(view.getByText('bound')).toBeTruthy();
|
|
78
|
+
expect(counts.connects).toBe(1);
|
|
79
|
+
expect(open()).toBe(1);
|
|
80
|
+
|
|
81
|
+
view.unmount();
|
|
82
|
+
expect(open()).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('StrictMode mounts twice and still leaves exactly one stream open', async () => {
|
|
86
|
+
const { transport, open } = countingTransport();
|
|
87
|
+
const view = render(
|
|
88
|
+
<StrictMode>
|
|
89
|
+
<Bell transport={transport} />
|
|
90
|
+
</StrictMode>,
|
|
91
|
+
);
|
|
92
|
+
await act(async () => {});
|
|
93
|
+
expect(view.getByText('bound')).toBeTruthy();
|
|
94
|
+
expect(open()).toBe(1);
|
|
95
|
+
view.unmount();
|
|
96
|
+
expect(open()).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('a re-render with new options does not rebuild the binding', async () => {
|
|
100
|
+
const { counts, transport } = countingTransport();
|
|
101
|
+
const view = render(<Bell transport={transport} />);
|
|
102
|
+
await act(async () => {});
|
|
103
|
+
view.rerender(<Bell transport={transport} />);
|
|
104
|
+
await act(async () => {});
|
|
105
|
+
expect(counts.connects).toBe(1);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createNotificationsBinding, type NotificationsBinding, type NotificationsBindingOptions } from './binding.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The React way to hold a {@link createNotificationsBinding}: built when the
|
|
6
|
+
* component COMMITS, disposed when it unmounts, `null` until then.
|
|
7
|
+
*
|
|
8
|
+
* const binding = useNotificationsBinding();
|
|
9
|
+
* if (!binding) return <span aria-hidden className={BELL_BOX} />;
|
|
10
|
+
* return <NotificationBell store={binding.store} />;
|
|
11
|
+
*
|
|
12
|
+
* 🔴 **Why this is a hook and not a `useMemo`.** `createNotificationsBinding`
|
|
13
|
+
* opens its stream (an EventSource plus a poll loop) the moment it is called,
|
|
14
|
+
* and React renders components it then throws away — a sibling suspending, an
|
|
15
|
+
* abandoned concurrent render, StrictMode's double render. Built during render,
|
|
16
|
+
* every discarded render left a stream open with nobody holding its `dispose`:
|
|
17
|
+
* panning family's tree leaked eight, six filled Chromium's per-host pool, and
|
|
18
|
+
* every later request on the page hung (forge task 2945). An effect only runs
|
|
19
|
+
* for a render React kept, and its cleanup is the dispose.
|
|
20
|
+
*
|
|
21
|
+
* `options` are read ONCE, when the binding is built — like `useState`'s
|
|
22
|
+
* initial value. A new object every render does not rebuild it; a callback in
|
|
23
|
+
* them should read changing state through a ref.
|
|
24
|
+
*/
|
|
25
|
+
export function useNotificationsBinding(options?: NotificationsBindingOptions): NotificationsBinding | null {
|
|
26
|
+
const optionsRef = useRef(options);
|
|
27
|
+
optionsRef.current = options;
|
|
28
|
+
const [binding, setBinding] = useState<NotificationsBinding | null>(null);
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const next = createNotificationsBinding(optionsRef.current);
|
|
31
|
+
setBinding(next);
|
|
32
|
+
return next.dispose;
|
|
33
|
+
}, []);
|
|
34
|
+
return binding;
|
|
35
|
+
}
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 151 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -32,25 +32,19 @@
|
|
|
32
32
|
@source inline("@max-6xl:p-0! @max-6xl:w-0! @max-xl:contents @xl:basis-auto @xl:block @xl:hidden [&>*]:hidden");
|
|
33
33
|
@source inline("[&>*]:max-w-none [&>svg]:size-3.5 [&_svg]:size-3 [overflow-wrap:anywhere] active:cursor-grabbing");
|
|
34
34
|
@source inline("appearance-none basis basis-full bg-[var(--cb-frozen-bg)] bg-card/95 bg-gradient-to-t");
|
|
35
|
-
@source inline("bg-muted/30 bg-muted/50 border-accent-solid border-accent/60 border-field-error border-
|
|
36
|
-
@source inline("border-
|
|
37
|
-
@source inline("data-[
|
|
38
|
-
@source inline("data-[
|
|
39
|
-
@source inline("
|
|
40
|
-
@source inline("data-[state=closed]:slide-out-to-left data-[state=closed]:slide-out-to-right");
|
|
41
|
-
@source inline("data-[state=open]:slide-in-from-bottom data-[state=open]:slide-in-from-left");
|
|
42
|
-
@source inline("data-[state=open]:slide-in-from-right disabled:opacity-40 duration-100 fade-in");
|
|
43
|
-
@source inline("focus-visible:ring-field-ring/40 focus-within:border-field-ring focus-within:ring-2");
|
|
35
|
+
@source inline("bg-muted/30 bg-muted/50 border-accent-solid border-accent/60 border-field-error border-solid");
|
|
36
|
+
@source inline("border-t-2 col collapse cursor-grab data-[ending-style]:opacity-0 data-[ending-style]:scale-95");
|
|
37
|
+
@source inline("data-[highlighted]:bg-accent/10 data-[highlighted]:text-accent-strong");
|
|
38
|
+
@source inline("data-[starting-style]:opacity-0 data-[starting-style]:scale-95 disabled:opacity-40 duration-100");
|
|
39
|
+
@source inline("fade-in focus-visible:ring-field-ring/40 focus-within:border-field-ring focus-within:ring-2");
|
|
44
40
|
@source inline("focus-within:ring-field-ring/30 from-card gap-x-6 h-105");
|
|
45
41
|
@source inline("has-[[aria-invalid=true]]:border-field-error hover:bg-background hover:bg-muted/40");
|
|
46
42
|
@source inline("hover:border-field-ring hover:text-warning italic justify-self-end last:border-0 left-11");
|
|
47
|
-
@source inline("left-2.5 list-none max-h-64 max-h-88 max-
|
|
48
|
-
@source inline("max-
|
|
49
|
-
@source inline("max-
|
|
50
|
-
@source inline("max-w-sm min-h-20 min-h-dvh min-w-0! min-w-20 min-w-24 min-w-52 min-w-[var(--anchor-width)]");
|
|
43
|
+
@source inline("left-2.5 list-none max-h-64 max-h-88 max-sm:flex-1 max-sm:flex-wrap max-sm:min-w-0");
|
|
44
|
+
@source inline("max-sm:min-w-24 max-sm:min-w-32 max-sm:ml-13.5 max-sm:w-auto max-w-32 max-w-36 max-w-45 max-w-48");
|
|
45
|
+
@source inline("max-w-60 min-h-20 min-h-dvh min-w-0! min-w-20 min-w-24 min-w-52 min-w-[var(--anchor-width)]");
|
|
51
46
|
@source inline("min-w-max ml-1.5 mr-2 mt-1.5 mt-3 mt-4 not-italic ordinal p-0! pb-1.5 pl-1.5 pl-2 pr-0.5 pr-1");
|
|
52
|
-
@source inline("pr-10 pr-16 pr-3 pr-9 pt-1 resize-none resize-y right-3 ring-field-error/30
|
|
53
|
-
@source inline("
|
|
54
|
-
@source inline("
|
|
55
|
-
@source inline("
|
|
56
|
-
@source inline("w-32 w-36 w-40 w-88");
|
|
47
|
+
@source inline("pr-10 pr-16 pr-3 pr-9 pt-1 resize-none resize-y right-3 ring-field-error/30 self self-stretch");
|
|
48
|
+
@source inline("shadow-[1px_0_0_0_var(--color-border)] shrink-[2] size-7 slide-in-from-top-1 table-fixed");
|
|
49
|
+
@source inline("text-accent-solid-foreground text-field-placeholder text-success text-xl to-transparent top-3");
|
|
50
|
+
@source inline("transition-[transform,opacity] w-0 w-0! w-11 w-12 w-20 w-32 w-36 w-40 w-88");
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 9 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
25
25
|
* import closures rather than a hand-written list, is scripts/styleAreas.ts.
|
|
26
26
|
*/
|
|
27
27
|
@source inline("caret-transparent focus-within:border-field-ring focus-within:ring-2");
|
|
28
|
-
@source inline("focus-within:ring-field-ring/30 h-12 has-[[aria-invalid=true]]:border-field-error
|
|
29
|
-
@source inline("
|
|
28
|
+
@source inline("focus-within:ring-field-ring/30 h-12 has-[[aria-invalid=true]]:border-field-error min-h-dvh");
|
|
29
|
+
@source inline("self-stretch sm:w-11");
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
19
19
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
20
20
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
21
|
-
*
|
|
21
|
+
* 76 candidate(s).
|
|
22
22
|
*
|
|
23
23
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
24
24
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -27,14 +27,14 @@
|
|
|
27
27
|
@source inline("bg-amber-100 bg-amber-500 bg-blue-100 bg-blue-500 bg-emerald-500 bg-green-100 bg-green-500");
|
|
28
28
|
@source inline("bg-muted/60 bg-purple-100 bg-purple-500 bg-rose-100 bg-rose-500 bg-sky-100 bg-sky-500");
|
|
29
29
|
@source inline("bg-slate-200 bg-slate-500 bg-teal-100 bg-teal-500 bg-violet-100 bg-violet-500 border-border/40");
|
|
30
|
-
@source inline("
|
|
31
|
-
@source inline("dark:bg-
|
|
32
|
-
@source inline("dark:bg-
|
|
33
|
-
@source inline("dark:text-
|
|
34
|
-
@source inline("dark:text-
|
|
35
|
-
@source inline("
|
|
36
|
-
@source inline("hover:bg-muted/
|
|
37
|
-
@source inline("
|
|
38
|
-
@source inline("sm:
|
|
39
|
-
@source inline("text-
|
|
40
|
-
@source inline("text-
|
|
30
|
+
@source inline("collapse dark:bg-amber-900/40 dark:bg-blue-900/40 dark:bg-emerald-900/40 dark:bg-green-900/40");
|
|
31
|
+
@source inline("dark:bg-purple-900/40 dark:bg-rose-900/40 dark:bg-sky-900/40 dark:bg-slate-800");
|
|
32
|
+
@source inline("dark:bg-teal-900/40 dark:bg-violet-900/40 dark:text-amber-200 dark:text-blue-200");
|
|
33
|
+
@source inline("dark:text-emerald-200 dark:text-green-200 dark:text-purple-200 dark:text-rose-200");
|
|
34
|
+
@source inline("dark:text-sky-200 dark:text-slate-200 dark:text-teal-200 dark:text-violet-200 focus:opacity-100");
|
|
35
|
+
@source inline("grid-cols-[repeat(7,minmax(0,1fr))] group-focus-within:opacity-100 h-3.5 hover:bg-muted/30");
|
|
36
|
+
@source inline("hover:bg-muted/40 hover:border-border/80 last:border-r-0 line-through min-h-28 min-h-40");
|
|
37
|
+
@source inline("opacity-85 outline-1 outline-dashed outline-tone-amber-ink sm:inline sm:min-h-0 sm:min-h-24");
|
|
38
|
+
@source inline("sm:px-1.5 sm:text-xs text-accent-solid-foreground text-amber-900 text-blue-800 text-emerald-800");
|
|
39
|
+
@source inline("text-green-800 text-purple-800 text-rose-800 text-sky-800 text-slate-800 text-teal-800");
|
|
40
|
+
@source inline("text-tone-amber-ink text-violet-800 w-20 w-3.5");
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* after an app's responsive variant (task 2124). An app that imports every area gets exactly
|
|
16
16
|
* what `cursedbelt/styles-utilities.css` emits — proved rule by rule in
|
|
17
17
|
* src/stylesAreas.spec.ts, which is also what fails when this file is stale.
|
|
18
|
-
*
|
|
18
|
+
* 920 candidate(s).
|
|
19
19
|
*
|
|
20
20
|
* Written by scripts/generateAreaStyles.ts (`bun run styles:areas`), which
|
|
21
21
|
* `bun run build` runs. The map of what is in which area, and why the areas are
|
|
@@ -62,11 +62,11 @@
|
|
|
62
62
|
@source inline("border-accent/50 border-b border-b-0 border-b-2 border-b-[length:var(--status-glyph-h)]");
|
|
63
63
|
@source inline("border-b-current border-border border-border! border-border-strong border-border/60");
|
|
64
64
|
@source inline("border-collapse border-current border-destructive/30 border-destructive/40 border-destructive/50");
|
|
65
|
-
@source inline("border-field-border border-
|
|
66
|
-
@source inline("border-tone-sky-fg/30 border-tone-slate-fg/30 border-transparent
|
|
67
|
-
@source inline("border-warning/40 border-warning/50 border-x-4 border-x-transparent bot bottom
|
|
68
|
-
@source inline("break-all break-words c ca cap capitalize co col-span-full com contain container
|
|
69
|
-
@source inline("contents cursor cursor-col-resize cursor-default cursor-ew-resize cursor-not-allowed");
|
|
65
|
+
@source inline("border-field-border border-l border-r border-rule border-success/30 border-success/40");
|
|
66
|
+
@source inline("border-success/50 border-t border-tone-sky-fg/30 border-tone-slate-fg/30 border-transparent");
|
|
67
|
+
@source inline("border-warning/30 border-warning/40 border-warning/50 border-x-4 border-x-transparent bot bottom");
|
|
68
|
+
@source inline("bottom-0 break break-all break-words c ca cap capitalize co col-span-full com contain container");
|
|
69
|
+
@source inline("content contents cursor cursor-col-resize cursor-default cursor-ew-resize cursor-not-allowed");
|
|
70
70
|
@source inline("cursor-pointer cursor-row-resize d dark dark:bg-card-overlay dark:bg-card-raised");
|
|
71
71
|
@source inline("dark:bg-gray-800 data data-[disabled]:hover:bg-border data-[disabled]:opacity-50");
|
|
72
72
|
@source inline("data-[disabled]:pointer-events-none data-[interactive=true]:duration-[var(--duration-base)]");
|
|
@@ -86,12 +86,15 @@
|
|
|
86
86
|
@source inline("data-[state=checked]:ring-accent data-[state=checked]:text-white");
|
|
87
87
|
@source inline("data-[state=checked]:translate-x-4 data-[state=closed]:animate-collapsible-up");
|
|
88
88
|
@source inline("data-[state=closed]:animate-out data-[state=closed]:fade-out-0");
|
|
89
|
-
@source inline("data-[state=closed]:
|
|
90
|
-
@source inline("data-[state=
|
|
91
|
-
@source inline("data-[state=
|
|
92
|
-
@source inline("data-[state=
|
|
93
|
-
@source inline("data-[state=
|
|
94
|
-
@source inline("data-[state=open]:
|
|
89
|
+
@source inline("data-[state=closed]:slide-out-to-bottom data-[state=closed]:slide-out-to-left");
|
|
90
|
+
@source inline("data-[state=closed]:slide-out-to-right data-[state=closed]:zoom-out-[0.985]");
|
|
91
|
+
@source inline("data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0");
|
|
92
|
+
@source inline("data-[state=delayed-open]:zoom-in-[0.985] data-[state=indeterminate]:bg-accent-solid");
|
|
93
|
+
@source inline("data-[state=indeterminate]:border-accent-solid data-[state=indeterminate]:text-white");
|
|
94
|
+
@source inline("data-[state=open]:animate-collapsible-down data-[state=open]:animate-in");
|
|
95
|
+
@source inline("data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-bottom");
|
|
96
|
+
@source inline("data-[state=open]:slide-in-from-bottom-2 data-[state=open]:slide-in-from-left");
|
|
97
|
+
@source inline("data-[state=open]:slide-in-from-right data-[state=open]:zoom-in-[0.985]");
|
|
95
98
|
@source inline("data-[state=unchecked]:bg-muted data-[state=unchecked]:translate-x-0 disable disabled");
|
|
96
99
|
@source inline("disabled:active:scale-100 disabled:bg-muted disabled:border-transparent");
|
|
97
100
|
@source inline("disabled:cursor-not-allowed disabled:hover:bg-transparent disabled:hover:border-border");
|
|
@@ -135,33 +138,34 @@
|
|
|
135
138
|
@source inline("leading-tight left left-0 left-1/2 left-2 lg lg:px-8 line line-clamp-2 low lower lowercase m m-0");
|
|
136
139
|
@source inline("max max-h max-h-[80vh] max-h-[calc(var(--radix-popper-available-height,100dvh)-1rem)]");
|
|
137
140
|
@source inline("max-h-[var(--guide-popover-max-h)] max-h-[var(--radix-select-content-available-height)]");
|
|
138
|
-
@source inline("max-
|
|
139
|
-
@source inline("max-sm:
|
|
140
|
-
@source inline("max-sm:
|
|
141
|
-
@source inline("max-sm:max-
|
|
142
|
-
@source inline("max-sm:
|
|
143
|
-
@source inline("max-w-
|
|
144
|
-
@source inline("max-w-[var(--page-w-
|
|
145
|
-
@source inline("max-w-
|
|
146
|
-
@source inline("md:
|
|
147
|
-
@source inline("min-h-
|
|
148
|
-
@source inline("min-
|
|
149
|
-
@source inline("
|
|
150
|
-
@source inline("motion-reduce:transition-none mt-0.5 mt-1
|
|
151
|
-
@source inline("
|
|
152
|
-
@source inline("
|
|
153
|
-
@source inline("overscroll-contain p p-0 p-0.5 p-1 p-1.5 p-2 p-3
|
|
154
|
-
@source inline("paused pb-0 pb-1 pb-2 pb-3 pb-4
|
|
155
|
-
@source inline("pb-[var(--empty-lift)] pl-4 pl-8 place
|
|
156
|
-
@source inline("pointer pointer-coarse pointer-coarse:-m-2
|
|
157
|
-
@source inline("pointer-coarse:
|
|
158
|
-
@source inline("pointer-coarse:
|
|
159
|
-
@source inline("pointer-
|
|
160
|
-
@source inline("px-2.5 px-3 px-4 px-5 px-6 px-row-x py-0 py-0.5 py-1
|
|
161
|
-
@source inline("py-[var(--panel-pad)] py-px py-row-y r re relative res
|
|
162
|
-
@source inline("ring-background ro rotate-90 round rounded
|
|
163
|
-
@source inline("rounded-field rounded-full rounded-l-full rounded-
|
|
164
|
-
@source inline("rounded-
|
|
141
|
+
@source inline("max-h-[var(--sheet-h-lg)] max-h-[var(--sheet-h-md)] max-h-[var(--sheet-h-sm)] max-sm:border-b-0");
|
|
142
|
+
@source inline("max-sm:border-x-0 max-sm:bottom-0 max-sm:data-[state=closed]:slide-out-to-bottom");
|
|
143
|
+
@source inline("max-sm:data-[state=open]:slide-in-from-bottom max-sm:h-dvh max-sm:hidden max-sm:inset-x-0");
|
|
144
|
+
@source inline("max-sm:max-h-[var(--sheet-h-phone)] max-sm:max-h-dvh max-sm:max-w-none max-sm:pt-6");
|
|
145
|
+
@source inline("max-sm:rounded-b-none max-sm:rounded-none max-sm:rounded-t-2xl max-sm:touch-none max-sm:w-full");
|
|
146
|
+
@source inline("max-w-(--radix-popover-content-available-width) max-w-2xl max-w-40 max-w-[var(--modal-w-sm)]");
|
|
147
|
+
@source inline("max-w-[var(--page-w-dashboard)] max-w-[var(--page-w-form)] max-w-[var(--page-w-wide)]");
|
|
148
|
+
@source inline("max-w-[var(--reading-width)] max-w-full max-w-lg max-w-md max-w-sm max-w-xl max-w-xs mb-0.5 mb-1");
|
|
149
|
+
@source inline("mb-3 md md:divide-border md:divide-x md:flex-none md:flex-row md:grid-cols-4 md:inline md:w-1/3");
|
|
150
|
+
@source inline("md:w-2/5 min min-h-0 min-h-11 min-h-14 min-h-[1lh] min-h-[var(--control-h-lg)]");
|
|
151
|
+
@source inline("min-h-[var(--control-h-md)] min-h-[var(--control-h-sm)] min-h-row min-w-0 min-w-11 min-w-32");
|
|
152
|
+
@source inline("min-w-[var(--radix-select-trigger-width)] ml-1 ml-auto motion motion-reduce:active:scale-100");
|
|
153
|
+
@source inline("motion-reduce:animate-none motion-reduce:duration-0 motion-reduce:transition-none mt-0.5 mt-1");
|
|
154
|
+
@source inline("mt-2 mx-auto my my-1 object object-cover opacity opacity-0 opacity-30 opacity-40 opacity-50");
|
|
155
|
+
@source inline("opacity-60 opacity-70 out outline outline-none over overflow overflow-auto overflow-hidden");
|
|
156
|
+
@source inline("overflow-x-auto overflow-x-clip overflow-y-auto overscroll-contain p p-0 p-0.5 p-1 p-1.5 p-2 p-3");
|
|
157
|
+
@source inline("p-4 p-6 p-[7px] p-[var(--panel-pad)] pause paused pb-0 pb-1 pb-2 pb-3 pb-4");
|
|
158
|
+
@source inline("pb-[max(env(safe-area-inset-bottom,0px),0.75rem)] pb-[var(--empty-lift)] pl-4 pl-8 place");
|
|
159
|
+
@source inline("placeholder placeholder:text-field-placeholder point pointer pointer-coarse pointer-coarse:-m-2");
|
|
160
|
+
@source inline("pointer-coarse:data-[state=checked]:translate-x-8 pointer-coarse:h-11 pointer-coarse:min-h-11");
|
|
161
|
+
@source inline("pointer-coarse:min-w-11 pointer-coarse:opacity-100 pointer-coarse:size-5 pointer-coarse:size-9");
|
|
162
|
+
@source inline("pointer-coarse:w-11 pointer-coarse:w-18 pointer-events pointer-events-auto pointer-events-none");
|
|
163
|
+
@source inline("pr-8 pt-2 pt-3 px px-0 px-1 px-1.5 px-2 px-2.5 px-3 px-4 px-5 px-6 px-row-x py-0 py-0.5 py-1");
|
|
164
|
+
@source inline("py-1.5 py-12 py-2 py-2.5 py-3 py-4 py-6 py-[var(--panel-pad)] py-px py-row-y r re relative res");
|
|
165
|
+
@source inline("resize right right-0 right-2 ring ring-2 ring-background ro rotate-90 round rounded");
|
|
166
|
+
@source inline("rounded-[calc(var(--radius-md)-0.25rem)] rounded-field rounded-full rounded-l-full rounded-l-xl");
|
|
167
|
+
@source inline("rounded-lg rounded-md rounded-none rounded-r-full rounded-r-xl rounded-sm rounded-t-xl");
|
|
168
|
+
@source inline("rounded-xl ru run running s select select-all select-none self-start shadow");
|
|
165
169
|
@source inline("shadow-[var(--shadow-overlay)] shadow-[var(--shadow-popover)] shadow-none shadow-overlay");
|
|
166
170
|
@source inline("shadow-raised shadow-sm shrink shrink-0 size size-0 size-1.5 size-10 size-12 size-14 size-2");
|
|
167
171
|
@source inline("size-2.5 size-20 size-3 size-3.5 size-4 size-5 size-6 size-8 slid slide slide-in-from-left sm");
|