@neovici/cosmoz-utils 6.25.0 → 6.26.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/directives/breakpoints.d.ts +47 -0
- package/dist/directives/breakpoints.js +213 -0
- package/dist/function.d.ts +7 -1
- package/dist/function.js +8 -1
- package/dist/promise.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface Breakpoint {
|
|
2
|
+
/** Reflected as data-breakpoint when this breakpoint is the narrowest match. */
|
|
3
|
+
name?: string;
|
|
4
|
+
/** Matches when container width <= maxWidth. Omit for the base breakpoint (maxWidth ∞, always matches). */
|
|
5
|
+
maxWidth?: number;
|
|
6
|
+
/** Element property writes. */
|
|
7
|
+
props?: Record<string, unknown>;
|
|
8
|
+
/** Attribute writes; boolean true → empty string, false → removed. */
|
|
9
|
+
attrs?: Record<string, string | boolean>;
|
|
10
|
+
}
|
|
11
|
+
interface ManagedKey {
|
|
12
|
+
name: string;
|
|
13
|
+
/** How to write the key on the host. */
|
|
14
|
+
channel: 'prop' | 'attr';
|
|
15
|
+
}
|
|
16
|
+
interface Snapshot {
|
|
17
|
+
breakpoints: Breakpoint[];
|
|
18
|
+
keys: ManagedKey[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Normalize breakpoints into a frozen snapshot.
|
|
22
|
+
* - exactly one breakpoint without maxWidth (the base, required)
|
|
23
|
+
* - keys declared in bounded breakpoints must also be declared in the base
|
|
24
|
+
* breakpoint (every managed key gets a resting value, so exit is well-defined)
|
|
25
|
+
* - keys are the union across breakpoints, prop channel wins on conflicts
|
|
26
|
+
*/
|
|
27
|
+
export declare const normalize: (breakpoints: Breakpoint[]) => Snapshot;
|
|
28
|
+
/**
|
|
29
|
+
* Container-width breakpoint directive for element parts: applies the
|
|
30
|
+
* narrowest matching breakpoint's `props`/`attrs` onto the host and
|
|
31
|
+
* reflects its `name` as `data-breakpoint`, driven by a ResizeObserver on
|
|
32
|
+
* the element.
|
|
33
|
+
*
|
|
34
|
+
* Exactly one base breakpoint (no `maxWidth`) is required and must declare
|
|
35
|
+
* every key used by bounded breakpoints — base values are the resting
|
|
36
|
+
* (exit) state. Snapshots are rebuilt only when `deps` change, so
|
|
37
|
+
* unrelated re-renders don't churn writes.
|
|
38
|
+
*
|
|
39
|
+
* ```js
|
|
40
|
+
* html`<my-list ${breakpoints(() => [
|
|
41
|
+
* { name: 'mini', maxWidth: 480, props: { renderItem: renderMini } },
|
|
42
|
+
* { props: { renderItem: null } },
|
|
43
|
+
* ], [renderMini])}></my-list>`
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export declare const breakpoints: (factory?: () => Breakpoint[], deps?: unknown[]) => unknown;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { nothing } from 'lit-html';
|
|
2
|
+
import { AsyncDirective, directive, PartType, } from 'lit-html/async-directive.js';
|
|
3
|
+
const depsEqual = (a, b) => (a ?? []).length === (b ?? []).length &&
|
|
4
|
+
(a ?? []).every((v, i) => (b ?? [])[i] === v);
|
|
5
|
+
const matches = (breakpoint, width) => breakpoint.maxWidth == null || width <= breakpoint.maxWidth;
|
|
6
|
+
/**
|
|
7
|
+
* Normalize breakpoints into a frozen snapshot.
|
|
8
|
+
* - exactly one breakpoint without maxWidth (the base, required)
|
|
9
|
+
* - keys declared in bounded breakpoints must also be declared in the base
|
|
10
|
+
* breakpoint (every managed key gets a resting value, so exit is well-defined)
|
|
11
|
+
* - keys are the union across breakpoints, prop channel wins on conflicts
|
|
12
|
+
*/
|
|
13
|
+
export const normalize = (breakpoints) => {
|
|
14
|
+
const base = breakpoints.filter((b) => b.maxWidth == null);
|
|
15
|
+
if (base.length !== 1) {
|
|
16
|
+
throw new Error(`breakpoints: exactly one breakpoint without maxWidth is required (the base), got ${base.length}`);
|
|
17
|
+
}
|
|
18
|
+
const sorted = [...breakpoints].sort((a, b) => (a.maxWidth ?? Infinity) - (b.maxWidth ?? Infinity));
|
|
19
|
+
const collect = (channel) => new Set(sorted.flatMap((b) => Object.keys(b[channel] ?? {})));
|
|
20
|
+
const propKeys = collect('props');
|
|
21
|
+
const attrKeys = collect('attrs');
|
|
22
|
+
const keys = [
|
|
23
|
+
...[...propKeys].map((name) => ({ name, channel: 'prop' })),
|
|
24
|
+
...[...attrKeys]
|
|
25
|
+
.filter((name) => !propKeys.has(name))
|
|
26
|
+
.map((name) => ({ name, channel: 'attr' })),
|
|
27
|
+
];
|
|
28
|
+
const baseKeys = new Set([
|
|
29
|
+
...Object.keys(base[0].props ?? {}),
|
|
30
|
+
...Object.keys(base[0].attrs ?? {}),
|
|
31
|
+
]);
|
|
32
|
+
const orphans = [...propKeys, ...attrKeys].filter((k) => !baseKeys.has(k));
|
|
33
|
+
if (orphans.length > 0) {
|
|
34
|
+
throw new Error(`breakpoints: key(s) [${orphans.join(', ')}] must also be declared in the base breakpoint (no maxWidth)`);
|
|
35
|
+
}
|
|
36
|
+
return { breakpoints: sorted, keys };
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Pure: snapshot × width → resolved values. No host reads.
|
|
40
|
+
* Returns undefined when the width is not resolvable (no layout box, <= 0):
|
|
41
|
+
* the caller must hold last state instead of resolving — 0 would false-match
|
|
42
|
+
* the narrowest breakpoint.
|
|
43
|
+
*/
|
|
44
|
+
const compute = (snapshot, width) => {
|
|
45
|
+
if (width <= 0) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const values = new Map();
|
|
49
|
+
for (const { name } of snapshot.keys) {
|
|
50
|
+
const breakpoint = snapshot.breakpoints.find((b) => matches(b, width) &&
|
|
51
|
+
(b.props?.[name] !== undefined || b.attrs?.[name] !== undefined));
|
|
52
|
+
if (!breakpoint) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Strict presence: null is a legitimate value (the exit path), so ??
|
|
56
|
+
// is wrong here — it would reduce a base `null` to "no opinion" and
|
|
57
|
+
// skip the write, stranding a previous breakpoint's override.
|
|
58
|
+
const value = breakpoint.props?.[name] !== undefined
|
|
59
|
+
? breakpoint.props?.[name]
|
|
60
|
+
: breakpoint.attrs?.[name];
|
|
61
|
+
values.set(name, value);
|
|
62
|
+
}
|
|
63
|
+
const named = snapshot.breakpoints.find((b) => b.name && matches(b, width));
|
|
64
|
+
return { breakpointName: named?.name, values };
|
|
65
|
+
};
|
|
66
|
+
const applyValue = (el, key, value) => {
|
|
67
|
+
if (key.channel === 'prop') {
|
|
68
|
+
el[key.name] = value;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (typeof value === 'boolean') {
|
|
72
|
+
if (value) {
|
|
73
|
+
el.setAttribute(key.name, '');
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
el.removeAttribute(key.name);
|
|
77
|
+
}
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
el.setAttribute(key.name, String(value));
|
|
81
|
+
};
|
|
82
|
+
const sameResolved = (a, b) => {
|
|
83
|
+
if (!a || !b || a.breakpointName !== b.breakpointName) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
if (a.values.size !== b.values.size) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
for (const [k, v] of a.values) {
|
|
90
|
+
if (b.values.get(k) !== v) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return true;
|
|
95
|
+
};
|
|
96
|
+
class BreakpointsDirective extends AsyncDirective {
|
|
97
|
+
_el;
|
|
98
|
+
_ro;
|
|
99
|
+
_snapshot;
|
|
100
|
+
_deps;
|
|
101
|
+
_resolved;
|
|
102
|
+
constructor(partInfo) {
|
|
103
|
+
super(partInfo);
|
|
104
|
+
if (partInfo.type !== PartType.ELEMENT) {
|
|
105
|
+
throw new Error('breakpoints directive must be used on an element');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
109
|
+
render(_factory, _deps) {
|
|
110
|
+
return nothing;
|
|
111
|
+
}
|
|
112
|
+
update(part, [factory, deps]) {
|
|
113
|
+
if (this._el !== undefined && this._el !== part.element) {
|
|
114
|
+
this._teardown();
|
|
115
|
+
this._el = undefined;
|
|
116
|
+
this._resolved = undefined;
|
|
117
|
+
}
|
|
118
|
+
this._el ??= part.element;
|
|
119
|
+
if (this._snapshot === undefined || !depsEqual(deps, this._deps)) {
|
|
120
|
+
this._deps = deps;
|
|
121
|
+
this._snapshot = normalize(factory());
|
|
122
|
+
// Snapshot change invalidates the memo; re-resolve at current width.
|
|
123
|
+
this._resolved = undefined;
|
|
124
|
+
this._evaluate();
|
|
125
|
+
}
|
|
126
|
+
this._observe();
|
|
127
|
+
if (!this._ro) {
|
|
128
|
+
// update() can run before the element is connected (initial fixture
|
|
129
|
+
// render) or before layout gives it a box; retry on the next
|
|
130
|
+
// microtask so observation + first evaluation happen post-connect.
|
|
131
|
+
queueMicrotask(() => {
|
|
132
|
+
if (!this.isConnected) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this._observe();
|
|
136
|
+
this._evaluate();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return nothing;
|
|
140
|
+
}
|
|
141
|
+
_observe() {
|
|
142
|
+
if (!this._el?.isConnected || this._ro) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
this._ro = new ResizeObserver((entries) => {
|
|
146
|
+
this._evaluate(entries[0]?.contentRect.width ?? 0);
|
|
147
|
+
});
|
|
148
|
+
this._ro.observe(this._el);
|
|
149
|
+
}
|
|
150
|
+
_evaluate(measured) {
|
|
151
|
+
if (!this._el || !this._snapshot) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const width = measured === undefined
|
|
155
|
+
? // No RO measurement yet (snapshot change outside an RO tick):
|
|
156
|
+
// measure synchronously to avoid a pre-breakpoint flash.
|
|
157
|
+
this._el.getBoundingClientRect().width
|
|
158
|
+
: measured;
|
|
159
|
+
const resolved = compute(this._snapshot, width);
|
|
160
|
+
if (resolved) {
|
|
161
|
+
this._apply(resolved);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
_apply(resolved) {
|
|
165
|
+
const el = this._el;
|
|
166
|
+
if (sameResolved(this._resolved, resolved)) {
|
|
167
|
+
return; // RO churn / idempotent re-render — write nothing
|
|
168
|
+
}
|
|
169
|
+
for (const key of this._snapshot.keys) {
|
|
170
|
+
const value = resolved.values.get(key.name);
|
|
171
|
+
if (value === undefined) {
|
|
172
|
+
continue; // no breakpoint has an opinion — never write
|
|
173
|
+
}
|
|
174
|
+
applyValue(el, key, value);
|
|
175
|
+
}
|
|
176
|
+
if (resolved.breakpointName) {
|
|
177
|
+
el.setAttribute('data-breakpoint', resolved.breakpointName);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
el.removeAttribute('data-breakpoint');
|
|
181
|
+
}
|
|
182
|
+
this._resolved = resolved;
|
|
183
|
+
}
|
|
184
|
+
_teardown() {
|
|
185
|
+
this._ro?.disconnect();
|
|
186
|
+
this._ro = undefined;
|
|
187
|
+
}
|
|
188
|
+
disconnected() {
|
|
189
|
+
this._teardown();
|
|
190
|
+
}
|
|
191
|
+
reconnected() {
|
|
192
|
+
this._observe();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Container-width breakpoint directive for element parts: applies the
|
|
197
|
+
* narrowest matching breakpoint's `props`/`attrs` onto the host and
|
|
198
|
+
* reflects its `name` as `data-breakpoint`, driven by a ResizeObserver on
|
|
199
|
+
* the element.
|
|
200
|
+
*
|
|
201
|
+
* Exactly one base breakpoint (no `maxWidth`) is required and must declare
|
|
202
|
+
* every key used by bounded breakpoints — base values are the resting
|
|
203
|
+
* (exit) state. Snapshots are rebuilt only when `deps` change, so
|
|
204
|
+
* unrelated re-renders don't churn writes.
|
|
205
|
+
*
|
|
206
|
+
* ```js
|
|
207
|
+
* html`<my-list ${breakpoints(() => [
|
|
208
|
+
* { name: 'mini', maxWidth: 480, props: { renderItem: renderMini } },
|
|
209
|
+
* { props: { renderItem: null } },
|
|
210
|
+
* ], [renderMini])}></my-list>`
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export const breakpoints = directive(BreakpointsDirective);
|
package/dist/function.d.ts
CHANGED
|
@@ -6,5 +6,11 @@ export declare function constant(): () => undefined;
|
|
|
6
6
|
export declare function constant<T>(v: T): () => T;
|
|
7
7
|
export declare const constTrue: () => true, constUndefined: () => undefined, noop: () => undefined, identity: <T>(obj: T) => T, or: <A, F extends OrFn<A>>(...fns: F[]) => (...args: A[]) => boolean;
|
|
8
8
|
export declare const once: <A extends Arr, R, F extends OnceFn<A, R> = OnceFn<A, R>>(fn: F, check?: OnceCheckFn<A>) => (...args: A) => any;
|
|
9
|
-
|
|
9
|
+
type Invoked<F> = F extends (...args: any[]) => infer R ? R : F;
|
|
10
|
+
/**
|
|
11
|
+
* The args of a known function are checked against its tuple; for a
|
|
12
|
+
* value-or-function union the runtime dispatch decides the member, so
|
|
13
|
+
* the args cannot be typed.
|
|
14
|
+
*/
|
|
15
|
+
export declare const invoke: <F>(fn: F, ...args: F extends (...args: infer P) => unknown ? P : unknown[]) => Invoked<F>;
|
|
10
16
|
export {};
|
package/dist/function.js
CHANGED
|
@@ -6,4 +6,11 @@ export const once = (fn, check = constTrue) => {
|
|
|
6
6
|
let result;
|
|
7
7
|
return (...args) => (result ??= check(args) ? fn(...args) : undefined);
|
|
8
8
|
};
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The args of a known function are checked against its tuple; for a
|
|
11
|
+
* value-or-function union the runtime dispatch decides the member, so
|
|
12
|
+
* the args cannot be typed.
|
|
13
|
+
*/
|
|
14
|
+
export const invoke = (fn, ...args) => typeof fn === 'function'
|
|
15
|
+
? fn(...args)
|
|
16
|
+
: fn;
|
package/dist/promise.d.ts
CHANGED
|
@@ -29,5 +29,5 @@ export declare const debounce$: <T extends unknown[], P>(fn: (...args: T) => P |
|
|
|
29
29
|
export declare const share$: <T extends unknown[], P>(fn: (...args: T) => P | PromiseLike<P>) => (...args: T) => Promise<P>;
|
|
30
30
|
export declare const log$: <T extends unknown[], P>(fn: (...args: T) => PromiseLike<P>) => (...args: T) => PromiseLike<P>;
|
|
31
31
|
export declare const retry$: <T extends unknown[], P>(fn: (...args: T) => P | PromiseLike<P>, n: number) => (...args: T) => Promise<P>;
|
|
32
|
-
export declare const invoke$: <T, A extends unknown[]>(fn: T | ((...args: A) => T | PromiseLike<T>), ...args: A) => Promise<Awaited<T> | Awaited<T>>;
|
|
32
|
+
export declare const invoke$: <T, A extends unknown[]>(fn: T | ((...args: A) => T | PromiseLike<T>), ...args: A) => Promise<Awaited<T> | Awaited<T> | Awaited<T extends (...args: any[]) => infer R ? R : T>>;
|
|
33
33
|
export {};
|