cursedbelt 5.70.0 → 5.72.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/components/MeterList.d.ts.map +1 -1
- package/dist/react/components/MeterList.js +1 -1
- package/dist/react/components/MeterList.js.map +1 -1
- 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/stats.css +12 -12
- package/dist/styles-utilities.css +8 -0
- package/package.json +1 -1
- package/scripts/fixtureAppCss.ts +6 -0
- package/src/fixtureAppCss.spec.ts +15 -0
- package/src/react/components/MeterList.spec.tsx +34 -0
- package/src/react/components/MeterList.tsx +15 -6
- 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/stats.css +12 -12
- package/src/styles-utilities.css +8 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MeterList.d.ts","sourceRoot":"","sources":["../../../src/react/components/MeterList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAC;AAGtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAkBlD,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,+DAA+D;IAC/D,KAAK,EAAE,SAAS,CAAC;IACjB,oEAAoE;IACpE,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,mCAAmC;IACnC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,uFAAuF;IACvF,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,MAAM,CAAC;AAOrD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,kFAAkF;IAClF,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,IAAY,EAAE,UAAsB,EAAE,SAAS,EAAE,EAAE,cAAc,+
|
|
1
|
+
{"version":3,"file":"MeterList.d.ts","sourceRoot":"","sources":["../../../src/react/components/MeterList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAiB,SAAS,EAAE,MAAM,OAAO,CAAC;AAGtD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAkBlD,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,+DAA+D;IAC/D,KAAK,EAAE,SAAS,CAAC;IACjB,oEAAoE;IACpE,QAAQ,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACjE,mCAAmC;IACnC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,uFAAuF;IACvF,UAAU,CAAC,EAAE,SAAS,CAAC;CACxB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,MAAM,CAAC;AAOrD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,kFAAkF;IAClF,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,IAAY,EAAE,UAAsB,EAAE,SAAS,EAAE,EAAE,cAAc,+BAqFnG"}
|
|
@@ -49,7 +49,7 @@ export function MeterList({ items, flat = false, labelWidth = 'default', classNa
|
|
|
49
49
|
return (_jsxs("div", { className: cn('group/meter grid min-h-11 grid-cols-[minmax(0,1fr)_auto] items-center gap-x-4 gap-y-1 py-2', LABEL_COLUMN[labelWidth], interactive &&
|
|
50
50
|
'cursor-pointer rounded-sm transition-colors hover:bg-accent/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'), ...(interactive
|
|
51
51
|
? { role: 'button', tabIndex: 0, onClick: item.onClick, onKeyDown, 'aria-label': item.label }
|
|
52
|
-
: {}), children: [_jsxs("div", { className: 'flex min-w-0 flex-col', children: [_jsxs("span", { className: 'flex items-center gap-1.5 text-sm font-medium text-foreground', children: [item.tone ? (_jsx("span", { "aria-hidden": true, className: cn('
|
|
52
|
+
: {}), children: [_jsxs("div", { className: 'flex min-w-0 flex-col', children: [_jsxs("span", { className: 'flex flex-wrap items-center gap-x-1.5 text-sm font-medium text-foreground sm:flex-nowrap', children: [_jsxs("span", { className: 'min-w-0 [overflow-wrap:anywhere] sm:truncate', children: [item.tone ? (_jsx("span", { "aria-hidden": true, className: cn('mr-1.5 inline-block size-1.5 rounded-full align-middle', TONE_DOT[item.tone]) })) : null, item.label] }), item.actionHint ? (_jsx("span", { className: `text-caption font-medium text-muted-foreground opacity-0 transition-opacity group-hover/meter:opacity-100 group-focus-visible/meter:opacity-100 ${TAP_TARGET_ALWAYS}`, children: item.actionHint })) : null] }), item.sub ? _jsx("span", { className: 'text-xs text-muted-foreground [overflow-wrap:anywhere] sm:truncate', children: item.sub }) : null] }), pct !== null ? (_jsx("div", { className: 'col-span-2 h-1.5 w-full overflow-hidden rounded-full bg-muted sm:col-span-1', "aria-hidden": true, children: _jsx("div", {
|
|
53
53
|
// The bar takes the row's warning/danger tone: a red dot beside a calm accent bar
|
|
54
54
|
// said "fine" and "full" at once (forge task 2593). `barClassName` still wins.
|
|
55
55
|
className: cn('h-full rounded-full', TONE_BAR[item.tone ?? 'neutral'], item.progress?.barClassName), style: { width: `${pct}%` } }) })) : (_jsx("span", { "aria-hidden": true, className: 'hidden sm:block' })), _jsxs("span", { className: cn('flex items-center justify-end gap-2 tabular-nums text-sm font-semibold text-foreground', pct !== null && 'row-start-1 col-start-2 sm:row-auto sm:col-auto'), children: [item.value, item.trailing] })] }, item.key ?? item.label));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MeterList.js","sourceRoot":"","sources":["../../../src/react/components/MeterList.tsx"],"names":[],"mappings":";AACA,OAAO,EAAE,EAAE,EAAE,MAAM,cAAc,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,QAAQ,GAAiC;IAC7C,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,gBAAgB;IACxB,OAAO,EAAE,kBAAkB;CAC5B,CAAC;AAEF,gGAAgG;AAChG,MAAM,QAAQ,GAAiC;IAC7C,OAAO,EAAE,iBAAiB;IAC1B,OAAO,EAAE,YAAY;IACrB,MAAM,EAAE,gBAAgB;IACxB,OAAO,EAAE,iBAAiB;CAC3B,CAAC;AAuCF,MAAM,YAAY,GAAwC;IACxD,OAAO,EAAE,yDAAyD;IAClE,IAAI,EAAE,yDAAyD;CAChE,CAAC;AAWF;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,SAAS,CAAC,EAAE,KAAK,EAAE,IAAI,GAAG,KAAK,EAAE,UAAU,GAAG,SAAS,EAAE,SAAS,EAAkB;IAClG,MAAM,IAAI,GAAG,CACX,cAAK,SAAS,EAAE,EAAE,CAAC,sCAAsC,EAAE,SAAS,CAAC,YAClE,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;YAClB,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC;YAC/C,MAAM,GAAG,GACP,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,GAAG,CAAC;gBACpC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;gBAC7E,CAAC,CAAC,IAAI,CAAC;YACX,MAAM,SAAS,GAAG,WAAW;gBAC3B,CAAC,CAAC,CAAC,CAAgC,EAAE,EAAE;oBACnC,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,IAAI,CAAC,CAAC,GAAG,KAAK,GAAG,EAAE,CAAC;wBACvC,CAAC,CAAC,cAAc,EAAE,CAAC;wBACnB,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;oBACnB,CAAC;gBACH,CAAC;gBACH,CAAC,CAAC,SAAS,CAAC;YACd,OAAO,CACL,eAEE,SAAS,EAAE,EAAE,CACX,4FAA4F,EAC5F,YAAY,CAAC,UAAU,CAAC,EACxB,WAAW;oBACT,wIAAwI,CAC3I,KACG,CAAC,WAAW;oBACd,CAAC,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,IAAI,CAAC,KAAK,EAAE;oBAC7F,CAAC,CAAC,EAAE,CAAC,aAEP,eAAK,SAAS,EAAC,uBAAuB,
|
|
1
|
+
{"version":3,"file":"MeterList.js","sourceRoot":"","sources":["../../../src/react/components/MeterList.tsx"],"names":[],"mappings":"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"}
|
|
@@ -39,8 +39,13 @@ export interface NotificationsBinding {
|
|
|
39
39
|
* transport feeding rows in, and the store's config hooks pushing
|
|
40
40
|
* read/dismiss back out.
|
|
41
41
|
*
|
|
42
|
-
* const
|
|
43
|
-
* <NotificationBell store={store}
|
|
42
|
+
* const binding = useNotificationsBinding(); // in a component — never during render
|
|
43
|
+
* if (binding) return <NotificationBell store={binding.store} />;
|
|
44
|
+
*
|
|
45
|
+
* 🔴 This function CONNECTS when called, so a component must not call it while
|
|
46
|
+
* rendering: React discards renders, and each discarded one would leak an open
|
|
47
|
+
* stream. {@link useNotificationsBinding} builds it in an effect and disposes it
|
|
48
|
+
* on unmount; call this directly only outside React, or as a page-long singleton.
|
|
44
49
|
*
|
|
45
50
|
* ── The one asymmetry worth knowing ─────────────────────────────────────────
|
|
46
51
|
* Rows flow IN through the transport; acknowledgments flow OUT through these
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../../src/react/notifications/binding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,8BAA8B,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAGpG,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,KAAK,uBAAuB,EAA8B,MAAM,oBAAoB,CAAC;AAE9F;;gFAEgF;AAChF,MAAM,MAAM,0BAA0B,GAAG,uBAAuB,GAC9D,4BAA4B,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,MAAM,WAAW,2BAA2B;IAC1C,gFAAgF;IAChF,MAAM,CAAC,EAAE,0BAA0B,CAAC;IACpC,6EAA6E;IAC7E,IAAI,CAAC,EAAE,8BAA8B,CAAC;IACtC,qFAAqF;IACrF,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,eAAe,KAAK,IAAI,CAAC;IACjD,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC1C,+EAA+E;IAC/E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,EAAE,0BAA0B,CAAC;IACnC;oEACgE;IAChE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED
|
|
1
|
+
{"version":3,"file":"binding.d.ts","sourceRoot":"","sources":["../../../src/react/notifications/binding.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,8BAA8B,EAAE,4BAA4B,EAAE,MAAM,iBAAiB,CAAC;AAGpG,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,KAAK,uBAAuB,EAA8B,MAAM,oBAAoB,CAAC;AAE9F;;gFAEgF;AAChF,MAAM,MAAM,0BAA0B,GAAG,uBAAuB,GAC9D,4BAA4B,GAAG;IAAE,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAExD,MAAM,WAAW,2BAA2B;IAC1C,gFAAgF;IAChF,MAAM,CAAC,EAAE,0BAA0B,CAAC;IACpC,6EAA6E;IAC7E,IAAI,CAAC,EAAE,8BAA8B,CAAC;IACtC,qFAAqF;IACrF,SAAS,CAAC,EAAE,qBAAqB,CAAC;IAClC,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,MAAM,CAAC,EAAE,CAAC,YAAY,EAAE,eAAe,KAAK,IAAI,CAAC;IACjD,yCAAyC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAC1C,+EAA+E;IAC/E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACpD;AAED,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,KAAK,EAAE,iBAAiB,CAAC;IACzB,MAAM,EAAE,0BAA0B,CAAC;IACnC;oEACgE;IAChE,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,GAAE,2BAAgC,GACxC,oBAAoB,CA4BtB"}
|
|
@@ -7,8 +7,13 @@ import { notifyNotificationsChanged } from './pollTransport.js';
|
|
|
7
7
|
* transport feeding rows in, and the store's config hooks pushing
|
|
8
8
|
* read/dismiss back out.
|
|
9
9
|
*
|
|
10
|
-
* const
|
|
11
|
-
* <NotificationBell store={store}
|
|
10
|
+
* const binding = useNotificationsBinding(); // in a component — never during render
|
|
11
|
+
* if (binding) return <NotificationBell store={binding.store} />;
|
|
12
|
+
*
|
|
13
|
+
* 🔴 This function CONNECTS when called, so a component must not call it while
|
|
14
|
+
* rendering: React discards renders, and each discarded one would leak an open
|
|
15
|
+
* stream. {@link useNotificationsBinding} builds it in an effect and disposes it
|
|
16
|
+
* on unmount; call this directly only outside React, or as a page-long singleton.
|
|
12
17
|
*
|
|
13
18
|
* ── The one asymmetry worth knowing ─────────────────────────────────────────
|
|
14
19
|
* Rows flow IN through the transport; acknowledgments flow OUT through these
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"binding.js","sourceRoot":"","sources":["../../../src/react/notifications/binding.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,kCAAkC,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EACL,uBAAuB,GAIxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAgC,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAoC9F
|
|
1
|
+
{"version":3,"file":"binding.js","sourceRoot":"","sources":["../../../src/react/notifications/binding.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,6BAA6B,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,kCAAkC,EAAE,MAAM,oBAAoB,CAAC;AACxE,OAAO,EACL,uBAAuB,GAIxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAgC,0BAA0B,EAAE,MAAM,oBAAoB,CAAC;AAoC9F;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,UAAU,0BAA0B,CACxC,UAAuC,EAAE;IAEzC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,6BAA6B,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,mBAAmB,MAAM,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC;IAElH,MAAM,GAAG,GAAG,CAAC,MAAc,EAAE,IAAsB,EAAQ,EAAE;QAC3D,KAAK,IAAI,CAAC,IAAI,CAAC,0BAA0B,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/F,CAAC,CAAC;IAEF,MAAM,KAAK,GAAG,uBAAuB,CAAC;QACpC,QAAQ,EAAE,OAAO,CAAC,QAAQ;QAC1B,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,UAAU,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;QAClF,aAAa,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,aAAa,EAAE,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;QACzE,SAAS,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACjE,UAAU,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,SAAS,EAAE,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;KAC/D,CAAC,CAAC;IAEH,MAAM,SAAS,GACb,OAAO,CAAC,SAAS;QACjB,kCAAkC,CAAC,MAAM,EAAE;YACzC,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,MAAM,CAAC;SAC3C,CAAC,CAAC;IAEL,MAAM,OAAO,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAE7D,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC;AACpC,CAAC"}
|
|
@@ -7,4 +7,5 @@ export { NotificationFeed, type NotificationFeedProps } from './NotificationFeed
|
|
|
7
7
|
export { NotificationInbox, type NotificationInboxProps } from './NotificationInbox.js';
|
|
8
8
|
export { createNotificationsPollTransport, NOTIFICATIONS_CHANGED_EVENT, type NotificationsListClient, type NotificationsPollTransportOptions, notifyNotificationsChanged, } from './pollTransport.js';
|
|
9
9
|
export { createNotificationStore, type NotificationAction, type NotificationItem, type NotificationPushInput, type NotificationState, type NotificationStore, type NotificationStoreConfig, type NotificationTone, type NotificationTransport, type NotificationTransportSync, useNotificationStore, } from './notificationStore.js';
|
|
10
|
+
export { useNotificationsBinding } from './useNotificationsBinding.js';
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EACL,0BAA0B,EAC1B,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,2BAA2B,GACjC,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,6BAA6B,EAC7B,sBAAsB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,GAClC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kCAAkC,EAClC,iBAAiB,EACjB,KAAK,mCAAmC,GACzC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACxF,OAAO,EACL,gCAAgC,EAChC,2BAA2B,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,iCAAiC,EACtC,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,EAC9B,oBAAoB,GACrB,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EACL,0BAA0B,EAC1B,KAAK,oBAAoB,EACzB,KAAK,0BAA0B,EAC/B,KAAK,2BAA2B,GACjC,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,6BAA6B,EAC7B,sBAAsB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,GAClC,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kCAAkC,EAClC,iBAAiB,EACjB,KAAK,mCAAmC,GACzC,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,KAAK,sBAAsB,EAAE,MAAM,wBAAwB,CAAC;AACxF,OAAO,EACL,gCAAgC,EAChC,2BAA2B,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,iCAAiC,EACtC,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uBAAuB,EACvB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,yBAAyB,EAC9B,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC"}
|
|
@@ -7,4 +7,5 @@ export { NotificationFeed } from './NotificationFeed.js';
|
|
|
7
7
|
export { NotificationInbox } from './NotificationInbox.js';
|
|
8
8
|
export { createNotificationsPollTransport, NOTIFICATIONS_CHANGED_EVENT, notifyNotificationsChanged, } from './pollTransport.js';
|
|
9
9
|
export { createNotificationStore, useNotificationStore, } from './notificationStore.js';
|
|
10
|
+
export { useNotificationsBinding } from './useNotificationsBinding.js';
|
|
10
11
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EACL,0BAA0B,GAI3B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,6BAA6B,EAC7B,sBAAsB,GAIvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kCAAkC,EAClC,iBAAiB,GAElB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAA8B,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAA8B,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAA+B,MAAM,wBAAwB,CAAC;AACxF,OAAO,EACL,gCAAgC,EAChC,2BAA2B,EAG3B,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uBAAuB,EAUvB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/notifications/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EACL,0BAA0B,GAI3B,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,6BAA6B,EAC7B,sBAAsB,GAIvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,kCAAkC,EAClC,iBAAiB,GAElB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,gBAAgB,EAA8B,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,gBAAgB,EAA8B,MAAM,uBAAuB,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAA+B,MAAM,wBAAwB,CAAC;AACxF,OAAO,EACL,gCAAgC,EAChC,2BAA2B,EAG3B,0BAA0B,GAC3B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,uBAAuB,EAUvB,oBAAoB,GACrB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type NotificationsBinding, type NotificationsBindingOptions } from './binding.js';
|
|
2
|
+
/**
|
|
3
|
+
* The React way to hold a {@link createNotificationsBinding}: built when the
|
|
4
|
+
* component COMMITS, disposed when it unmounts, `null` until then.
|
|
5
|
+
*
|
|
6
|
+
* const binding = useNotificationsBinding();
|
|
7
|
+
* if (!binding) return <span aria-hidden className={BELL_BOX} />;
|
|
8
|
+
* return <NotificationBell store={binding.store} />;
|
|
9
|
+
*
|
|
10
|
+
* 🔴 **Why this is a hook and not a `useMemo`.** `createNotificationsBinding`
|
|
11
|
+
* opens its stream (an EventSource plus a poll loop) the moment it is called,
|
|
12
|
+
* and React renders components it then throws away — a sibling suspending, an
|
|
13
|
+
* abandoned concurrent render, StrictMode's double render. Built during render,
|
|
14
|
+
* every discarded render left a stream open with nobody holding its `dispose`:
|
|
15
|
+
* panning family's tree leaked eight, six filled Chromium's per-host pool, and
|
|
16
|
+
* every later request on the page hung (forge task 2945). An effect only runs
|
|
17
|
+
* for a render React kept, and its cleanup is the dispose.
|
|
18
|
+
*
|
|
19
|
+
* `options` are read ONCE, when the binding is built — like `useState`'s
|
|
20
|
+
* initial value. A new object every render does not rebuild it; a callback in
|
|
21
|
+
* them should read changing state through a ref.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useNotificationsBinding(options?: NotificationsBindingOptions): NotificationsBinding | null;
|
|
24
|
+
//# sourceMappingURL=useNotificationsBinding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNotificationsBinding.d.ts","sourceRoot":"","sources":["../../../src/react/notifications/useNotificationsBinding.ts"],"names":[],"mappings":"AACA,OAAO,EAA8B,KAAK,oBAAoB,EAAE,KAAK,2BAA2B,EAAE,MAAM,cAAc,CAAC;AAEvH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,2BAA2B,GAAG,oBAAoB,GAAG,IAAI,CAU1G"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createNotificationsBinding } from './binding.js';
|
|
3
|
+
/**
|
|
4
|
+
* The React way to hold a {@link createNotificationsBinding}: built when the
|
|
5
|
+
* component COMMITS, disposed when it unmounts, `null` until then.
|
|
6
|
+
*
|
|
7
|
+
* const binding = useNotificationsBinding();
|
|
8
|
+
* if (!binding) return <span aria-hidden className={BELL_BOX} />;
|
|
9
|
+
* return <NotificationBell store={binding.store} />;
|
|
10
|
+
*
|
|
11
|
+
* 🔴 **Why this is a hook and not a `useMemo`.** `createNotificationsBinding`
|
|
12
|
+
* opens its stream (an EventSource plus a poll loop) the moment it is called,
|
|
13
|
+
* and React renders components it then throws away — a sibling suspending, an
|
|
14
|
+
* abandoned concurrent render, StrictMode's double render. Built during render,
|
|
15
|
+
* every discarded render left a stream open with nobody holding its `dispose`:
|
|
16
|
+
* panning family's tree leaked eight, six filled Chromium's per-host pool, and
|
|
17
|
+
* every later request on the page hung (forge task 2945). An effect only runs
|
|
18
|
+
* for a render React kept, and its cleanup is the dispose.
|
|
19
|
+
*
|
|
20
|
+
* `options` are read ONCE, when the binding is built — like `useState`'s
|
|
21
|
+
* initial value. A new object every render does not rebuild it; a callback in
|
|
22
|
+
* them should read changing state through a ref.
|
|
23
|
+
*/
|
|
24
|
+
export function useNotificationsBinding(options) {
|
|
25
|
+
const optionsRef = useRef(options);
|
|
26
|
+
optionsRef.current = options;
|
|
27
|
+
const [binding, setBinding] = useState(null);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const next = createNotificationsBinding(optionsRef.current);
|
|
30
|
+
setBinding(next);
|
|
31
|
+
return next.dispose;
|
|
32
|
+
}, []);
|
|
33
|
+
return binding;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=useNotificationsBinding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useNotificationsBinding.js","sourceRoot":"","sources":["../../../src/react/notifications/useNotificationsBinding.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAA+D,MAAM,cAAc,CAAC;AAEvH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,uBAAuB,CAAC,OAAqC;IAC3E,MAAM,UAAU,GAAG,MAAM,CAAC,OAAO,CAAC,CAAC;IACnC,UAAU,CAAC,OAAO,GAAG,OAAO,CAAC;IAC7B,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAA8B,IAAI,CAAC,CAAC;IAC1E,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,IAAI,GAAG,0BAA0B,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;QAC5D,UAAU,CAAC,IAAI,CAAC,CAAC;QACjB,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC,EAAE,EAAE,CAAC,CAAC;IACP,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -18,22 +18,22 @@
|
|
|
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
|
+
* 64 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
|
-
@source inline("[&>div]:sm:[grid-template-columns:…] [&_svg]:size-3.5
|
|
28
|
-
@source inline("bg-background/70 border-l-2 border-l-border-strong
|
|
29
|
-
@source inline("bottom-3 col-span-2 col-start-2 collapse cursor-help
|
|
30
|
-
@source inline("grid-cols-[minmax(0,1fr)_auto]
|
|
31
|
-
@source inline("group-focus-visible/
|
|
32
|
-
@source inline("group-
|
|
33
|
-
@source inline("group/meter group/stat group/tile hover:bg-muted/50 last:pr-1
|
|
34
|
-
@source inline("left-[var(--timeline-rail-x)] list-none min-h-9 min-w-34 min-w-full mr-0.5
|
|
35
|
-
@source inline("rounded-b-md rounded-t-md row-start-1 self-stretch shadow-md sm:col-auto
|
|
36
|
-
@source inline("sm:grid-cols-[minmax(7rem,18rem)_minmax(4rem,1fr)_auto]");
|
|
37
|
-
@source inline("sm:grid-cols-[minmax(7rem,34rem)_minmax(4rem,1fr)_auto] sm:row-auto text-3xl");
|
|
27
|
+
@source inline("[&>div]:sm:[grid-template-columns:…] [&_svg]:size-3.5 [overflow-wrap:anywhere] basis-40");
|
|
28
|
+
@source inline("bg-accent/15 bg-accent/40 bg-background/70 border-l-2 border-l-border-strong");
|
|
29
|
+
@source inline("border-l-destructive border-l-warning bottom-3 col-span-2 col-start-2 collapse cursor-help");
|
|
30
|
+
@source inline("divide-x first:pl-1 gap-x-1.5 gap-x-2 gap-y-0.5 grid-cols-[minmax(0,1fr)_auto]");
|
|
31
|
+
@source inline("group-focus-visible/meter:opacity-100 group-focus-visible/stat:opacity-100");
|
|
32
|
+
@source inline("group-focus-visible/tile:opacity-100 group-hover/meter:opacity-100 group-hover/stat:opacity-100");
|
|
33
|
+
@source inline("group-hover/tile:opacity-100 group/meter group/stat group/tile hover:bg-muted/50 last:pr-1");
|
|
34
|
+
@source inline("left-3 left-[var(--timeline-rail-x)] list-none min-h-9 min-w-34 min-w-full mr-0.5 mr-1.5");
|
|
35
|
+
@source inline("normal-case pt-1 rounded-b-md rounded-t-md row-start-1 self-stretch shadow-md sm:col-auto");
|
|
36
|
+
@source inline("sm:col-span-1 sm:flex-nowrap sm:grid-cols-[minmax(7rem,18rem)_minmax(4rem,1fr)_auto]");
|
|
37
|
+
@source inline("sm:grid-cols-[minmax(7rem,34rem)_minmax(4rem,1fr)_auto] sm:row-auto sm:truncate text-3xl");
|
|
38
38
|
@source inline("text-[length:var(--text-glyph)] text-foreground/80 text-success top-7 tracking-normal w-48");
|
|
39
39
|
@source inline("whitespace-pre z-[1]");
|
|
@@ -1474,6 +1474,9 @@
|
|
|
1474
1474
|
.gap-x-0 {
|
|
1475
1475
|
column-gap: 0px;
|
|
1476
1476
|
}
|
|
1477
|
+
.gap-x-1\.5 {
|
|
1478
|
+
column-gap: calc(var(--spacing) * 1.5);
|
|
1479
|
+
}
|
|
1477
1480
|
.gap-x-2 {
|
|
1478
1481
|
column-gap: calc(var(--spacing) * 2);
|
|
1479
1482
|
}
|
|
@@ -4688,6 +4691,11 @@
|
|
|
4688
4691
|
.sm\:gap-3 {
|
|
4689
4692
|
gap: calc(var(--spacing) * 3);
|
|
4690
4693
|
}
|
|
4694
|
+
.sm\:truncate {
|
|
4695
|
+
overflow: hidden;
|
|
4696
|
+
text-overflow: ellipsis;
|
|
4697
|
+
white-space: nowrap;
|
|
4698
|
+
}
|
|
4691
4699
|
.sm\:rounded-xl {
|
|
4692
4700
|
border-radius: 1rem;
|
|
4693
4701
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.72.0",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "The React design system of the cursedbelt split — components, styles, theme. cursedbelt-core below it; server tier in cursedbelt-server.",
|
package/scripts/fixtureAppCss.ts
CHANGED
|
@@ -268,6 +268,12 @@ export const parseBaseline = (text: string): Baseline => {
|
|
|
268
268
|
for (const line of text.split('\n')) {
|
|
269
269
|
if (line.startsWith('#') || line.trim() === '') {
|
|
270
270
|
if (counts.size === 0) header.push(line);
|
|
271
|
+
// 🔴 The header is everything ABOVE the first count, and `--prune` writes header + counts —
|
|
272
|
+
// so a note below a count is deleted by the next prune. Found 2026-10-09 (forge task 2966):
|
|
273
|
+
// a stale count pair at line 17 had 270 lines of raise notes below it, every one of them
|
|
274
|
+
// one `--prune` from gone, and a hand raise edited the stale pair the parser never read.
|
|
275
|
+
else if (line.startsWith('#'))
|
|
276
|
+
throw new Error(`${BASELINE}: a note follows a count line — \`--prune\` keeps only what is above the first count and would delete it. Keep the counts as the file's last lines, once:\n ${line}`);
|
|
271
277
|
continue;
|
|
272
278
|
}
|
|
273
279
|
const [bytes, route] = line.trim().split(/\s+/);
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
fixtureImports,
|
|
48
48
|
measure,
|
|
49
49
|
missingNeeds,
|
|
50
|
+
parseBaseline,
|
|
50
51
|
readBaseline,
|
|
51
52
|
requiredCss,
|
|
52
53
|
uncoveredModules,
|
|
@@ -104,6 +105,20 @@ describe('the fixture app measures a real consumer', () => {
|
|
|
104
105
|
});
|
|
105
106
|
});
|
|
106
107
|
|
|
108
|
+
describe('the baseline survives its own --prune', () => {
|
|
109
|
+
// `--prune` writes back only what is above the first count, so a note below one is a note the
|
|
110
|
+
// next prune deletes. The parser refuses it; this is the refusal, and the file passing it.
|
|
111
|
+
it('refuses a note that follows a count line', () => {
|
|
112
|
+
expect(() => parseBaseline('# header\n 10 union\n# a raise, written below\n 20 areas\n')).toThrow(/a note follows a count line/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('accepts counts as the last lines, and keeps every note above them', () => {
|
|
116
|
+
const parsed = parseBaseline('# header\n\n# a raise\n\n 10 union\n 20 areas\n');
|
|
117
|
+
expect(parsed.header).toContain('# a raise');
|
|
118
|
+
expect([...parsed.counts]).toEqual([['union', 10], ['areas', 20]]);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
107
122
|
describe('the fixture app is materially smaller on the area route', () => {
|
|
108
123
|
it('has a baseline recording both routes', () => {
|
|
109
124
|
// A missing baseline must fail, not seed itself: a check that writes its own
|
|
@@ -42,3 +42,37 @@ describe('MeterList label width', () => {
|
|
|
42
42
|
expect(rowOf(container).className).not.toContain('mt-2');
|
|
43
43
|
});
|
|
44
44
|
});
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A label that is a sentence (station's costliest slowdowns, forge task 2966) was cut to two words on
|
|
48
|
+
* a phone: the label truncated beside the value AND beside an invisible hover hint. Below `sm` the
|
|
49
|
+
* label and sub wrap and the hint may drop to its own line; from `sm` up they truncate as before.
|
|
50
|
+
*/
|
|
51
|
+
describe('MeterList on a phone', () => {
|
|
52
|
+
const items = [
|
|
53
|
+
{
|
|
54
|
+
label: 'Phone check fails on a lossy network and is rerun',
|
|
55
|
+
sub: '↑ from 4m 50s · 9 batches hit',
|
|
56
|
+
value: '5m 10s a batch',
|
|
57
|
+
tone: 'warning' as const,
|
|
58
|
+
onClick: () => {},
|
|
59
|
+
actionHint: 'Open its notes and chart',
|
|
60
|
+
},
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
test('the label and sub wrap below sm and truncate from sm up', () => {
|
|
64
|
+
const { container, getByText } = render(<MeterList items={items} flat />);
|
|
65
|
+
const label = getByText(/Phone check fails/);
|
|
66
|
+
for (const el of [label, getByText(/from 4m 50s/)]) {
|
|
67
|
+
expect(el.className.split(' ')).not.toContain('truncate');
|
|
68
|
+
expect(el.className).toContain('sm:truncate');
|
|
69
|
+
expect(el.className).toContain('[overflow-wrap:anywhere]');
|
|
70
|
+
}
|
|
71
|
+
const line = label.parentElement as HTMLElement;
|
|
72
|
+
expect(line.className).toContain('flex-wrap');
|
|
73
|
+
expect(line.className).toContain('sm:flex-nowrap');
|
|
74
|
+
// The tone dot rides inside the label, so a wrapped label keeps it on its first line.
|
|
75
|
+
expect(label.querySelector('.bg-warning')).not.toBeNull();
|
|
76
|
+
expect(rowOf(container).getAttribute('aria-label')).toBe(items[0]?.label ?? '');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -114,18 +114,27 @@ export function MeterList({ items, flat = false, labelWidth = 'default', classNa
|
|
|
114
114
|
: {})}
|
|
115
115
|
>
|
|
116
116
|
<div className='flex min-w-0 flex-col'>
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
117
|
+
{/*
|
|
118
|
+
* Below `sm` the label and sub WRAP: a phone gives the label column what is left
|
|
119
|
+
* beside the value, and a label that is a sentence was cut to two words (forge
|
|
120
|
+
* task 2966). The dot rides inside the label so a wrapped label keeps it, and the
|
|
121
|
+
* line wraps so the hint — in the layout even while invisible — drops under a long
|
|
122
|
+
* label instead of squeezing it. From `sm` up, one line each, truncated, as before.
|
|
123
|
+
*/}
|
|
124
|
+
<span className='flex flex-wrap items-center gap-x-1.5 text-sm font-medium text-foreground sm:flex-nowrap'>
|
|
125
|
+
<span className='min-w-0 [overflow-wrap:anywhere] sm:truncate'>
|
|
126
|
+
{item.tone ? (
|
|
127
|
+
<span aria-hidden className={cn('mr-1.5 inline-block size-1.5 rounded-full align-middle', TONE_DOT[item.tone])} />
|
|
128
|
+
) : null}
|
|
129
|
+
{item.label}
|
|
130
|
+
</span>
|
|
122
131
|
{item.actionHint ? (
|
|
123
132
|
<span className={`text-caption font-medium text-muted-foreground opacity-0 transition-opacity group-hover/meter:opacity-100 group-focus-visible/meter:opacity-100 ${TAP_TARGET_ALWAYS}`}>
|
|
124
133
|
{item.actionHint}
|
|
125
134
|
</span>
|
|
126
135
|
) : null}
|
|
127
136
|
</span>
|
|
128
|
-
{item.sub ? <span className='
|
|
137
|
+
{item.sub ? <span className='text-xs text-muted-foreground [overflow-wrap:anywhere] sm:truncate'>{item.sub}</span> : null}
|
|
129
138
|
</div>
|
|
130
139
|
{pct !== null ? (
|
|
131
140
|
<div className='col-span-2 h-1.5 w-full overflow-hidden rounded-full bg-muted sm:col-span-1' aria-hidden>
|
|
@@ -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,22 +18,22 @@
|
|
|
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
|
+
* 64 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
|
-
@source inline("[&>div]:sm:[grid-template-columns:…] [&_svg]:size-3.5
|
|
28
|
-
@source inline("bg-background/70 border-l-2 border-l-border-strong
|
|
29
|
-
@source inline("bottom-3 col-span-2 col-start-2 collapse cursor-help
|
|
30
|
-
@source inline("grid-cols-[minmax(0,1fr)_auto]
|
|
31
|
-
@source inline("group-focus-visible/
|
|
32
|
-
@source inline("group-
|
|
33
|
-
@source inline("group/meter group/stat group/tile hover:bg-muted/50 last:pr-1
|
|
34
|
-
@source inline("left-[var(--timeline-rail-x)] list-none min-h-9 min-w-34 min-w-full mr-0.5
|
|
35
|
-
@source inline("rounded-b-md rounded-t-md row-start-1 self-stretch shadow-md sm:col-auto
|
|
36
|
-
@source inline("sm:grid-cols-[minmax(7rem,18rem)_minmax(4rem,1fr)_auto]");
|
|
37
|
-
@source inline("sm:grid-cols-[minmax(7rem,34rem)_minmax(4rem,1fr)_auto] sm:row-auto text-3xl");
|
|
27
|
+
@source inline("[&>div]:sm:[grid-template-columns:…] [&_svg]:size-3.5 [overflow-wrap:anywhere] basis-40");
|
|
28
|
+
@source inline("bg-accent/15 bg-accent/40 bg-background/70 border-l-2 border-l-border-strong");
|
|
29
|
+
@source inline("border-l-destructive border-l-warning bottom-3 col-span-2 col-start-2 collapse cursor-help");
|
|
30
|
+
@source inline("divide-x first:pl-1 gap-x-1.5 gap-x-2 gap-y-0.5 grid-cols-[minmax(0,1fr)_auto]");
|
|
31
|
+
@source inline("group-focus-visible/meter:opacity-100 group-focus-visible/stat:opacity-100");
|
|
32
|
+
@source inline("group-focus-visible/tile:opacity-100 group-hover/meter:opacity-100 group-hover/stat:opacity-100");
|
|
33
|
+
@source inline("group-hover/tile:opacity-100 group/meter group/stat group/tile hover:bg-muted/50 last:pr-1");
|
|
34
|
+
@source inline("left-3 left-[var(--timeline-rail-x)] list-none min-h-9 min-w-34 min-w-full mr-0.5 mr-1.5");
|
|
35
|
+
@source inline("normal-case pt-1 rounded-b-md rounded-t-md row-start-1 self-stretch shadow-md sm:col-auto");
|
|
36
|
+
@source inline("sm:col-span-1 sm:flex-nowrap sm:grid-cols-[minmax(7rem,18rem)_minmax(4rem,1fr)_auto]");
|
|
37
|
+
@source inline("sm:grid-cols-[minmax(7rem,34rem)_minmax(4rem,1fr)_auto] sm:row-auto sm:truncate text-3xl");
|
|
38
38
|
@source inline("text-[length:var(--text-glyph)] text-foreground/80 text-success top-7 tracking-normal w-48");
|
|
39
39
|
@source inline("whitespace-pre z-[1]");
|
package/src/styles-utilities.css
CHANGED
|
@@ -1474,6 +1474,9 @@
|
|
|
1474
1474
|
.gap-x-0 {
|
|
1475
1475
|
column-gap: 0px;
|
|
1476
1476
|
}
|
|
1477
|
+
.gap-x-1\.5 {
|
|
1478
|
+
column-gap: calc(var(--spacing) * 1.5);
|
|
1479
|
+
}
|
|
1477
1480
|
.gap-x-2 {
|
|
1478
1481
|
column-gap: calc(var(--spacing) * 2);
|
|
1479
1482
|
}
|
|
@@ -4688,6 +4691,11 @@
|
|
|
4688
4691
|
.sm\:gap-3 {
|
|
4689
4692
|
gap: calc(var(--spacing) * 3);
|
|
4690
4693
|
}
|
|
4694
|
+
.sm\:truncate {
|
|
4695
|
+
overflow: hidden;
|
|
4696
|
+
text-overflow: ellipsis;
|
|
4697
|
+
white-space: nowrap;
|
|
4698
|
+
}
|
|
4691
4699
|
.sm\:rounded-xl {
|
|
4692
4700
|
border-radius: 1rem;
|
|
4693
4701
|
}
|