@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.
@@ -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);
@@ -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
- export declare const invoke: <T, A extends unknown[]>(fn: T | ((...args: A) => T), ...args: A) => T;
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
- export const invoke = (fn, ...args) => (typeof fn === 'function' ? fn(...args) : fn);
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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neovici/cosmoz-utils",
3
- "version": "6.25.0",
3
+ "version": "6.26.0",
4
4
  "description": "Date, money and template management functions commonly needed in Cosmoz views.",
5
5
  "keywords": [
6
6
  "polymer",