foldkit 0.18.0 → 0.19.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/README.md +11 -11
- package/dist/effectExtensions/optionExtensions.d.ts +4 -0
- package/dist/effectExtensions/optionExtensions.d.ts.map +1 -1
- package/dist/effectExtensions/optionExtensions.js +2 -1
- package/dist/fieldValidation/index.d.ts.map +1 -1
- package/dist/fieldValidation/index.js +4 -4
- package/dist/html/index.d.ts +31 -3
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/index.js +49 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/message/index.d.ts +2 -0
- package/dist/message/index.d.ts.map +1 -0
- package/dist/message/index.js +1 -0
- package/dist/message/public.d.ts +2 -0
- package/dist/message/public.d.ts.map +1 -0
- package/dist/message/public.js +1 -0
- package/dist/route/index.d.ts +1 -0
- package/dist/route/index.d.ts.map +1 -1
- package/dist/route/index.js +1 -0
- package/dist/route/parser.js +17 -17
- package/dist/route/public.d.ts +1 -1
- package/dist/route/public.d.ts.map +1 -1
- package/dist/route/public.js +1 -1
- package/dist/runtime/runtime.js +8 -8
- package/dist/schema/index.d.ts +36 -8
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +6 -0
- package/dist/task/dom.d.ts +59 -0
- package/dist/task/dom.d.ts.map +1 -0
- package/dist/task/dom.js +113 -0
- package/dist/task/index.d.ts +6 -108
- package/dist/task/index.d.ts.map +1 -1
- package/dist/task/index.js +6 -168
- package/dist/task/inert.d.ts +25 -0
- package/dist/task/inert.d.ts.map +1 -0
- package/dist/task/inert.js +88 -0
- package/dist/task/public.d.ts +1 -1
- package/dist/task/public.d.ts.map +1 -1
- package/dist/task/public.js +1 -1
- package/dist/task/random.d.ts +12 -0
- package/dist/task/random.d.ts.map +1 -0
- package/dist/task/random.js +11 -0
- package/dist/task/scrollLock.d.ts +24 -0
- package/dist/task/scrollLock.d.ts.map +1 -0
- package/dist/task/scrollLock.js +48 -0
- package/dist/task/time.d.ts +42 -0
- package/dist/task/time.d.ts.map +1 -0
- package/dist/task/time.js +54 -0
- package/dist/task/timing.d.ts +36 -0
- package/dist/task/timing.d.ts.map +1 -0
- package/dist/task/timing.js +55 -0
- package/dist/ui/dialog/index.d.ts.map +1 -1
- package/dist/ui/dialog/index.js +4 -4
- package/dist/ui/disclosure/index.d.ts.map +1 -1
- package/dist/ui/disclosure/index.js +4 -4
- package/dist/ui/keyboard.d.ts.map +1 -1
- package/dist/ui/keyboard.js +1 -1
- package/dist/ui/menu/index.d.ts +70 -17
- package/dist/ui/menu/index.d.ts.map +1 -1
- package/dist/ui/menu/index.js +317 -112
- package/dist/ui/menu/public.d.ts +2 -2
- package/dist/ui/menu/public.d.ts.map +1 -1
- package/dist/ui/menu/public.js +1 -1
- package/dist/ui/tabs/index.d.ts.map +1 -1
- package/dist/ui/tabs/index.js +6 -6
- package/dist/url/index.js +4 -4
- package/package.json +5 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Array, Effect, Number, Option, Predicate, pipe } from 'effect';
|
|
2
|
+
const inertState = {
|
|
3
|
+
originals: new Map(),
|
|
4
|
+
counts: new Map(),
|
|
5
|
+
cleanups: new Map(),
|
|
6
|
+
};
|
|
7
|
+
const markInert = (element) => {
|
|
8
|
+
const count = inertState.counts.get(element) ?? 0;
|
|
9
|
+
inertState.counts.set(element, Number.increment(count));
|
|
10
|
+
if (count === 0) {
|
|
11
|
+
inertState.originals.set(element, {
|
|
12
|
+
ariaHidden: element.getAttribute('aria-hidden'),
|
|
13
|
+
inert: element.inert,
|
|
14
|
+
});
|
|
15
|
+
element.setAttribute('aria-hidden', 'true');
|
|
16
|
+
element.inert = true;
|
|
17
|
+
}
|
|
18
|
+
return () => markNotInert(element);
|
|
19
|
+
};
|
|
20
|
+
const markNotInert = (element) => {
|
|
21
|
+
const count = inertState.counts.get(element) ?? 1;
|
|
22
|
+
if (count === 1) {
|
|
23
|
+
const original = inertState.originals.get(element);
|
|
24
|
+
if (original) {
|
|
25
|
+
if (Predicate.isNull(original.ariaHidden)) {
|
|
26
|
+
element.removeAttribute('aria-hidden');
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
element.setAttribute('aria-hidden', original.ariaHidden);
|
|
30
|
+
}
|
|
31
|
+
element.inert = original.inert;
|
|
32
|
+
inertState.originals.delete(element);
|
|
33
|
+
}
|
|
34
|
+
inertState.counts.delete(element);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
inertState.counts.set(element, Number.decrement(count));
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
const resolveElements = (selectors) => Array.filterMap(selectors, selector => {
|
|
41
|
+
const element = document.querySelector(selector);
|
|
42
|
+
return element instanceof HTMLElement ? Option.some(element) : Option.none();
|
|
43
|
+
});
|
|
44
|
+
const ancestorsUpToBody = (element) => Array.unfold(element.parentElement, current => Predicate.isNotNull(current)
|
|
45
|
+
? Option.some([
|
|
46
|
+
current,
|
|
47
|
+
current === document.body ? null : current.parentElement,
|
|
48
|
+
])
|
|
49
|
+
: Option.none());
|
|
50
|
+
const inertableSiblings = (parent, allowedElements) => pipe(parent.children, Array.fromIterable, Array.filterMap(child => child instanceof HTMLElement &&
|
|
51
|
+
!Array.some(allowedElements, allowed => child.contains(allowed))
|
|
52
|
+
? Option.some(child)
|
|
53
|
+
: Option.none()));
|
|
54
|
+
/**
|
|
55
|
+
* Creates a command that marks all DOM elements outside the given selectors as
|
|
56
|
+
* `inert` and `aria-hidden="true"`. Walks each allowed element up to
|
|
57
|
+
* `document.body`, marking siblings that don't contain an allowed element.
|
|
58
|
+
* Uses reference counting so nested calls are safe.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* ```typescript
|
|
62
|
+
* Task.inertOthers('my-menu', ['#menu-button', '#menu-items'], () => NoOp())
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
export const inertOthers = (id, allowedSelectors, f) => Effect.sync(() => {
|
|
66
|
+
const allowedElements = resolveElements(allowedSelectors);
|
|
67
|
+
const cleanupFunctions = pipe(allowedElements, Array.flatMap(ancestorsUpToBody), Array.flatMap(ancestor => Array.map(inertableSiblings(ancestor, allowedElements), markInert)));
|
|
68
|
+
inertState.cleanups.set(id, cleanupFunctions);
|
|
69
|
+
return f();
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* Creates a command that restores all elements previously marked inert by
|
|
73
|
+
* `inertOthers` for the given ID. Safe to call without a preceding
|
|
74
|
+
* `inertOthers` — acts as a no-op in that case.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* Task.restoreInert('my-menu', () => NoOp())
|
|
79
|
+
* ```
|
|
80
|
+
*/
|
|
81
|
+
export const restoreInert = (id, f) => Effect.sync(() => {
|
|
82
|
+
const cleanupFunctions = inertState.cleanups.get(id);
|
|
83
|
+
if (cleanupFunctions) {
|
|
84
|
+
Array.forEach(cleanupFunctions, cleanup => cleanup());
|
|
85
|
+
inertState.cleanups.delete(id);
|
|
86
|
+
}
|
|
87
|
+
return f();
|
|
88
|
+
});
|
package/dist/task/public.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { getTime, getTimeZone, getZonedTime, getZonedTimeIn, focus, showModal, closeModal, delay, scrollIntoView, randomInt, } from './index';
|
|
1
|
+
export { getTime, getTimeZone, getZonedTime, getZonedTimeIn, focus, showModal, closeModal, clickElement, delay, scrollIntoView, randomInt, nextFrame, waitForTransitions, } from './index';
|
|
2
2
|
//# sourceMappingURL=public.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/task/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,WAAW,EACX,YAAY,EACZ,cAAc,EACd,KAAK,EACL,SAAS,EACT,UAAU,EACV,KAAK,EACL,cAAc,EACd,SAAS,
|
|
1
|
+
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/task/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,OAAO,EACP,WAAW,EACX,YAAY,EACZ,cAAc,EACd,KAAK,EACL,SAAS,EACT,UAAU,EACV,YAAY,EACZ,KAAK,EACL,cAAc,EACd,SAAS,EACT,SAAS,EACT,kBAAkB,GACnB,MAAM,SAAS,CAAA"}
|
package/dist/task/public.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { getTime, getTimeZone, getZonedTime, getZonedTimeIn, focus, showModal, closeModal, delay, scrollIntoView, randomInt, } from './index';
|
|
1
|
+
export { getTime, getTimeZone, getZonedTime, getZonedTimeIn, focus, showModal, closeModal, clickElement, delay, scrollIntoView, randomInt, nextFrame, waitForTransitions, } from './index';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a command that generates a random integer between min (inclusive) and max (exclusive)
|
|
4
|
+
* and passes it to a message constructor.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* Task.randomInt(0, 100, value => GotRandom({ value }))
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare const randomInt: <Message>(min: number, max: number, f: (value: number) => Message) => Effect.Effect<Message>;
|
|
12
|
+
//# sourceMappingURL=random.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"random.d.ts","sourceRoot":"","sources":["../../src/task/random.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAE/B;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,EAC/B,KAAK,MAAM,EACX,KAAK,MAAM,EACX,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,KAC5B,MAAM,CAAC,MAAM,CAAC,OAAO,CAC6C,CAAA"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a command that generates a random integer between min (inclusive) and max (exclusive)
|
|
4
|
+
* and passes it to a message constructor.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* Task.randomInt(0, 100, value => GotRandom({ value }))
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export const randomInt = (min, max, f) => Effect.sync(() => f(Math.floor(Math.random() * (max - min)) + min));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a command that locks page scroll by setting `overflow: hidden` on the
|
|
4
|
+
* document element. Compensates for scrollbar width with padding to prevent layout
|
|
5
|
+
* shift. Uses reference counting so nested locks are safe — the page only unlocks
|
|
6
|
+
* when every lock has been released.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* Task.lockScroll(() => NoOp())
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
export declare const lockScroll: <Message>(f: () => Message) => Effect.Effect<Message>;
|
|
14
|
+
/**
|
|
15
|
+
* Creates a command that releases one scroll lock. When the last lock is released,
|
|
16
|
+
* restores the original `overflow` and `padding-right` on the document element.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* Task.unlockScroll(() => NoOp())
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare const unlockScroll: <Message>(f: () => Message) => Effect.Effect<Message>;
|
|
24
|
+
//# sourceMappingURL=scrollLock.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scrollLock.d.ts","sourceRoot":"","sources":["../../src/task/scrollLock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAA;AAQvC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,UAAU,GAAI,OAAO,EAAE,GAAG,MAAM,OAAO,KAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAqBxE,CAAA;AAEJ;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,EAClC,GAAG,MAAM,OAAO,KACf,MAAM,CAAC,MAAM,CAAC,OAAO,CAapB,CAAA"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Effect, Number } from 'effect';
|
|
2
|
+
const scrollLockState = {
|
|
3
|
+
count: 0,
|
|
4
|
+
overflow: '',
|
|
5
|
+
paddingRight: '',
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Creates a command that locks page scroll by setting `overflow: hidden` on the
|
|
9
|
+
* document element. Compensates for scrollbar width with padding to prevent layout
|
|
10
|
+
* shift. Uses reference counting so nested locks are safe — the page only unlocks
|
|
11
|
+
* when every lock has been released.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* Task.lockScroll(() => NoOp())
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const lockScroll = (f) => Effect.sync(() => {
|
|
19
|
+
const { documentElement, documentElement: { style }, } = document;
|
|
20
|
+
if (scrollLockState.count === 0) {
|
|
21
|
+
scrollLockState.overflow = style.overflow;
|
|
22
|
+
scrollLockState.paddingRight = style.paddingRight;
|
|
23
|
+
const scrollbarWidth = window.innerWidth - documentElement.clientWidth;
|
|
24
|
+
style.overflow = 'hidden';
|
|
25
|
+
style.paddingRight =
|
|
26
|
+
scrollbarWidth > 0 ? `${scrollbarWidth}px` : style.paddingRight;
|
|
27
|
+
}
|
|
28
|
+
scrollLockState.count++;
|
|
29
|
+
return f();
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Creates a command that releases one scroll lock. When the last lock is released,
|
|
33
|
+
* restores the original `overflow` and `padding-right` on the document element.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* Task.unlockScroll(() => NoOp())
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export const unlockScroll = (f) => Effect.sync(() => {
|
|
41
|
+
scrollLockState.count = Math.max(0, Number.decrement(scrollLockState.count));
|
|
42
|
+
if (scrollLockState.count === 0) {
|
|
43
|
+
const { documentElement: { style }, } = document;
|
|
44
|
+
style.overflow = scrollLockState.overflow;
|
|
45
|
+
style.paddingRight = scrollLockState.paddingRight;
|
|
46
|
+
}
|
|
47
|
+
return f();
|
|
48
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { DateTime, Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a command that gets the current UTC time and passes it to a message constructor.
|
|
4
|
+
* This is similar to Elm's `Task.perform` with `Time.now`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* Task.getTime(utc => GotTime({ utc }))
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export declare const getTime: <Message>(f: (utc: DateTime.Utc) => Message) => Effect.Effect<Message>;
|
|
12
|
+
/**
|
|
13
|
+
* Creates a command that gets the system timezone and passes it to a message constructor.
|
|
14
|
+
* This is similar to Elm's `Task.perform` with `Time.here`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* Task.getTimeZone(zone => GotTimeZone({ zone }))
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export declare const getTimeZone: <Message>(f: (zone: DateTime.TimeZone) => Message) => Effect.Effect<Message>;
|
|
22
|
+
/**
|
|
23
|
+
* Creates a command that gets the current time in the system timezone and passes it to a message constructor.
|
|
24
|
+
* This combines both time and timezone in a single task.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* Task.getZonedTime(zoned => GotTime({ zoned }))
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare const getZonedTime: <Message>(f: (zoned: DateTime.Zoned) => Message) => Effect.Effect<Message>;
|
|
32
|
+
/**
|
|
33
|
+
* Creates a command that gets the current time in a specific timezone and passes it to a message constructor.
|
|
34
|
+
* If the timezone is invalid, the effect will fail with an error string.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* Task.getZonedTimeIn('America/New_York', zoned => GotNYTime({ zoned }))
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare const getZonedTimeIn: <Message>(zoneId: string, f: (zoned: DateTime.Zoned) => Message) => Effect.Effect<Message, string>;
|
|
42
|
+
//# sourceMappingURL=time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time.d.ts","sourceRoot":"","sources":["../../src/task/time.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAA;AAEjD;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,OAAO,EAC7B,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,GAAG,KAAK,OAAO,KAChC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAgC,CAAA;AAExD;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,GAAI,OAAO,EACjC,GAAG,CAAC,IAAI,EAAE,QAAQ,CAAC,QAAQ,KAAK,OAAO,KACtC,MAAM,CAAC,MAAM,CAAC,OAAO,CAAmD,CAAA;AAE3E;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,GAAI,OAAO,EAClC,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,KAAK,OAAO,KACpC,MAAM,CAAC,MAAM,CAAC,OAAO,CAMpB,CAAA;AAEJ;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,EACpC,QAAQ,MAAM,EACd,GAAG,CAAC,KAAK,EAAE,QAAQ,CAAC,KAAK,KAAK,OAAO,KACpC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAS5B,CAAA"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { DateTime, Effect, Option } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a command that gets the current UTC time and passes it to a message constructor.
|
|
4
|
+
* This is similar to Elm's `Task.perform` with `Time.now`.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* Task.getTime(utc => GotTime({ utc }))
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
export const getTime = (f) => Effect.map(DateTime.now, f);
|
|
12
|
+
/**
|
|
13
|
+
* Creates a command that gets the system timezone and passes it to a message constructor.
|
|
14
|
+
* This is similar to Elm's `Task.perform` with `Time.here`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* Task.getTimeZone(zone => GotTimeZone({ zone }))
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export const getTimeZone = (f) => Effect.sync(() => f(DateTime.zoneMakeLocal()));
|
|
22
|
+
/**
|
|
23
|
+
* Creates a command that gets the current time in the system timezone and passes it to a message constructor.
|
|
24
|
+
* This combines both time and timezone in a single task.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* Task.getZonedTime(zoned => GotTime({ zoned }))
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export const getZonedTime = (f) => Effect.gen(function* () {
|
|
32
|
+
const utc = yield* DateTime.now;
|
|
33
|
+
const zone = DateTime.zoneMakeLocal();
|
|
34
|
+
const zoned = DateTime.setZone(utc, zone);
|
|
35
|
+
return f(zoned);
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Creates a command that gets the current time in a specific timezone and passes it to a message constructor.
|
|
39
|
+
* If the timezone is invalid, the effect will fail with an error string.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* Task.getZonedTimeIn('America/New_York', zoned => GotNYTime({ zoned }))
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export const getZonedTimeIn = (zoneId, f) => Effect.gen(function* () {
|
|
47
|
+
const utc = yield* DateTime.now;
|
|
48
|
+
const maybeZone = DateTime.zoneMakeNamed(zoneId);
|
|
49
|
+
if (Option.isNone(maybeZone)) {
|
|
50
|
+
return yield* Effect.fail(`Invalid timezone: ${zoneId}`);
|
|
51
|
+
}
|
|
52
|
+
const zoned = DateTime.setZone(utc, maybeZone.value);
|
|
53
|
+
return f(zoned);
|
|
54
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Duration, Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a command that resolves to a message after a delay.
|
|
4
|
+
* Useful for debouncing, such as clearing a typeahead search query.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* Task.delay(350, () => ClearedSearch({ version: model.searchVersion }))
|
|
9
|
+
* Task.delay(Duration.seconds(1), () => TimedOut())
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export declare const delay: <Message>(duration: Duration.DurationInput, f: () => Message) => Effect.Effect<Message>;
|
|
13
|
+
/**
|
|
14
|
+
* Creates a command that resolves to a message after two animation frames,
|
|
15
|
+
* ensuring the browser has painted the current state before proceeding.
|
|
16
|
+
* Used for CSS transition orchestration — the double-rAF guarantees the "from"
|
|
17
|
+
* state is visible before transitioning to the "to" state.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* Task.nextFrame(() => TransitionFrameAdvanced())
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare const nextFrame: <Message>(f: () => Message) => Effect.Effect<Message>;
|
|
25
|
+
/**
|
|
26
|
+
* Creates a command that waits for all CSS transitions on the element matching the selector
|
|
27
|
+
* to complete, then resolves to a message. Uses the Web Animations API for reliable detection.
|
|
28
|
+
* Falls back to resolving immediately if the element is missing or has no active transitions.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* Task.waitForTransitions('#menu-items', () => TransitionEnded())
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare const waitForTransitions: <Message>(selector: string, f: () => Message) => Effect.Effect<Message>;
|
|
36
|
+
//# sourceMappingURL=timing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timing.d.ts","sourceRoot":"","sources":["../../src/task/timing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEzC;;;;;;;;;GASG;AACH,eAAO,MAAM,KAAK,GAAI,OAAO,EAC3B,UAAU,QAAQ,CAAC,aAAa,EAChC,GAAG,MAAM,OAAO,KACf,MAAM,CAAC,MAAM,CAAC,OAAO,CAIpB,CAAA;AAEJ;;;;;;;;;;GAUG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,EAAE,GAAG,MAAM,OAAO,KAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAOvE,CAAA;AAEJ;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,EACxC,UAAU,MAAM,EAChB,GAAG,MAAM,OAAO,KACf,MAAM,CAAC,MAAM,CAAC,OAAO,CAgBpB,CAAA"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Effect } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a command that resolves to a message after a delay.
|
|
4
|
+
* Useful for debouncing, such as clearing a typeahead search query.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* Task.delay(350, () => ClearedSearch({ version: model.searchVersion }))
|
|
9
|
+
* Task.delay(Duration.seconds(1), () => TimedOut())
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
export const delay = (duration, f) => Effect.gen(function* () {
|
|
13
|
+
yield* Effect.sleep(duration);
|
|
14
|
+
return f();
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Creates a command that resolves to a message after two animation frames,
|
|
18
|
+
* ensuring the browser has painted the current state before proceeding.
|
|
19
|
+
* Used for CSS transition orchestration — the double-rAF guarantees the "from"
|
|
20
|
+
* state is visible before transitioning to the "to" state.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* Task.nextFrame(() => TransitionFrameAdvanced())
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export const nextFrame = (f) => Effect.async(resume => {
|
|
28
|
+
requestAnimationFrame(() => {
|
|
29
|
+
requestAnimationFrame(() => {
|
|
30
|
+
resume(Effect.succeed(f()));
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
/**
|
|
35
|
+
* Creates a command that waits for all CSS transitions on the element matching the selector
|
|
36
|
+
* to complete, then resolves to a message. Uses the Web Animations API for reliable detection.
|
|
37
|
+
* Falls back to resolving immediately if the element is missing or has no active transitions.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* Task.waitForTransitions('#menu-items', () => TransitionEnded())
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export const waitForTransitions = (selector, f) => Effect.async(resume => {
|
|
45
|
+
requestAnimationFrame(async () => {
|
|
46
|
+
const element = document.querySelector(selector);
|
|
47
|
+
const cssTransitions = element instanceof HTMLElement
|
|
48
|
+
? element
|
|
49
|
+
.getAnimations()
|
|
50
|
+
.filter(animation => 'transitionProperty' in animation)
|
|
51
|
+
: [];
|
|
52
|
+
await Promise.allSettled(cssTransitions.map(({ finished }) => finished));
|
|
53
|
+
resume(Effect.succeed(f()));
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/dialog/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAGxD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/dialog/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAGxD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAEtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAMpD,8FAA8F;AAC9F,eAAO,MAAM,KAAK;;;EAGhB,CAAA;AAEF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,wEAAwE;AACxE,eAAO,MAAM,MAAM,2DAAc,CAAA;AACjC,wHAAwH;AACxH,eAAO,MAAM,MAAM,2DAAc,CAAA;AACjC,oHAAoH;AACpH,eAAO,MAAM,IAAI,yDAAY,CAAA;AAE7B,8DAA8D;AAC9D,eAAO,MAAM,OAAO,0LAAgC,CAAA;AAEpD,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAA;AACvC,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAA;AACvC,MAAM,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,CAAA;AAEnC,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,6DAA6D;AAC7D,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAC,CAAA;AAEF,yEAAyE;AACzE,eAAO,MAAM,IAAI,GAAI,QAAQ,UAAU,KAAG,KAGxC,CAAA;AAMF,0EAA0E;AAC1E,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CA4BvC,CAAA;AAIH,iGAAiG;AACjG,eAAO,MAAM,OAAO,GAAI,OAAO,KAAK,KAAG,MAA6B,CAAA;AAEpE,wGAAwG;AACxG,eAAO,MAAM,aAAa,GAAI,OAAO,KAAK,KAAG,MAAmC,CAAA;AAEhF,wDAAwD;AACxD,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,QAAQ,CAAC;IACzC,KAAK,EAAE,KAAK,CAAA;IACZ,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,OAAO,CAAA;IAC9C,YAAY,EAAE,IAAI,CAAA;IAClB,cAAc,EAAE,MAAM,CAAA;IACtB,iBAAiB,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAC,CAAA;AAEF,sGAAsG;AACtG,eAAO,MAAM,IAAI,GAAI,OAAO,EAAE,QAAQ,UAAU,CAAC,OAAO,CAAC,KAAG,IA6C3D,CAAA"}
|
package/dist/ui/dialog/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Match as M, Option, Schema as S } from 'effect';
|
|
2
2
|
import { html } from '../../html';
|
|
3
|
-
import {
|
|
3
|
+
import { m } from '../../message';
|
|
4
4
|
import { evo } from '../../struct';
|
|
5
5
|
import * as Task from '../../task';
|
|
6
6
|
// MODEL
|
|
@@ -11,11 +11,11 @@ export const Model = S.Struct({
|
|
|
11
11
|
});
|
|
12
12
|
// MESSAGE
|
|
13
13
|
/** Sent when the dialog should open. Triggers the showModal command. */
|
|
14
|
-
export const Opened =
|
|
14
|
+
export const Opened = m('Opened');
|
|
15
15
|
/** Sent when the dialog should close (Escape key, backdrop click, or programmatic). Triggers the closeModal command. */
|
|
16
|
-
export const Closed =
|
|
16
|
+
export const Closed = m('Closed');
|
|
17
17
|
/** Placeholder message used when no action is needed, such as after a showModal or closeModal command completes. */
|
|
18
|
-
export const NoOp =
|
|
18
|
+
export const NoOp = m('NoOp');
|
|
19
19
|
/** Union of all messages the dialog component can produce. */
|
|
20
20
|
export const Message = S.Union(Opened, Closed, NoOp);
|
|
21
21
|
/** Creates an initial dialog model from a config. Defaults to closed. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/disclosure/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAGxD,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/disclosure/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsB,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAGxD,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AAE/C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAMpD,kGAAkG;AAClG,eAAO,MAAM,KAAK;;;EAGhB,CAAA;AAEF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,iFAAiF;AACjF,eAAO,MAAM,OAAO,4DAAe,CAAA;AACnC,gFAAgF;AAChF,eAAO,MAAM,MAAM,2DAAc,CAAA;AACjC,kGAAkG;AAClG,eAAO,MAAM,IAAI,yDAAY,CAAA;AAE7B,kEAAkE;AAClE,eAAO,MAAM,OAAO,2LAAiC,CAAA;AAErD,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AACzC,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAA;AACvC,MAAM,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,CAAA;AAEnC,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,iEAAiE;AACjE,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB,CAAC,CAAA;AAEF,6EAA6E;AAC7E,eAAO,MAAM,IAAI,GAAI,QAAQ,UAAU,KAAG,KAGxC,CAAA;AAQF,8EAA8E;AAC9E,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CA4BvC,CAAA;AAIH,4DAA4D;AAC5D,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,QAAQ,CAAC;IACzC,KAAK,EAAE,KAAK,CAAA;IACZ,SAAS,EAAE,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,KAAK,OAAO,CAAA;IACxD,eAAe,EAAE,MAAM,CAAA;IACvB,aAAa,EAAE,IAAI,CAAA;IACnB,cAAc,EAAE,MAAM,CAAA;IACtB,YAAY,EAAE,IAAI,CAAA;IAClB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAC,CAAA;AAEF,oGAAoG;AACpG,eAAO,MAAM,IAAI,GAAI,OAAO,EAAE,QAAQ,UAAU,CAAC,OAAO,CAAC,KAAG,IAyF3D,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Match as M, Option, Schema as S } from 'effect';
|
|
2
2
|
import { html } from '../../html';
|
|
3
|
-
import {
|
|
3
|
+
import { m } from '../../message';
|
|
4
4
|
import { evo } from '../../struct';
|
|
5
5
|
import * as Task from '../../task';
|
|
6
6
|
// MODEL
|
|
@@ -11,11 +11,11 @@ export const Model = S.Struct({
|
|
|
11
11
|
});
|
|
12
12
|
// MESSAGE
|
|
13
13
|
/** Sent when the disclosure button is clicked. Toggles the open/closed state. */
|
|
14
|
-
export const Toggled =
|
|
14
|
+
export const Toggled = m('Toggled');
|
|
15
15
|
/** Sent to explicitly close the disclosure, regardless of its current state. */
|
|
16
|
-
export const Closed =
|
|
16
|
+
export const Closed = m('Closed');
|
|
17
17
|
/** Placeholder message used when no action is needed, such as after a focus command completes. */
|
|
18
|
-
export const NoOp =
|
|
18
|
+
export const NoOp = m('NoOp');
|
|
19
19
|
/** Union of all messages the disclosure component can produce. */
|
|
20
20
|
export const Message = S.Union(Toggled, Closed, NoOp);
|
|
21
21
|
/** Creates an initial disclosure model from a config. Defaults to closed. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../../src/ui/keyboard.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAG,MACpB,CAAA;AAEtC,eAAO,MAAM,qBAAqB,GAE9B,WAAW,MAAM,EACjB,cAAc,MAAM,EACpB,YAAY,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,MAEvC,YAAY,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,KAAG,
|
|
1
|
+
{"version":3,"file":"keyboard.d.ts","sourceRoot":"","sources":["../../src/ui/keyboard.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,SAAS,GAAI,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAG,MACpB,CAAA;AAEtC,eAAO,MAAM,qBAAqB,GAE9B,WAAW,MAAM,EACjB,cAAc,MAAM,EACpB,YAAY,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,MAEvC,YAAY,MAAM,EAAE,WAAW,CAAC,GAAG,CAAC,CAAC,KAAG,MAMtC,CAAA;AAEL,eAAO,MAAM,UAAU,GACrB,SAAS,MAAM,EACf,aAAa,MAAM,EACnB,WAAW,MAAM,EACjB,cAAc,MAAM,EACpB,YAAY,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,KACrC,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAW1B,CAAA"}
|
package/dist/ui/keyboard.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Array, Match as M, Option, Predicate, pipe } from 'effect';
|
|
2
2
|
export const wrapIndex = (index, length) => ((index % length) + length) % length;
|
|
3
|
-
export const findFirstEnabledIndex = (itemCount, focusedIndex, isDisabled) => (startIndex, direction) => pipe(itemCount, Array.makeBy(
|
|
3
|
+
export const findFirstEnabledIndex = (itemCount, focusedIndex, isDisabled) => (startIndex, direction) => pipe(itemCount, Array.makeBy(step => wrapIndex(startIndex + step * direction, itemCount)), Array.findFirst(Predicate.not(isDisabled)), Option.getOrElse(() => focusedIndex));
|
|
4
4
|
export const keyToIndex = (nextKey, previousKey, itemCount, focusedIndex, isDisabled) => {
|
|
5
5
|
const find = findFirstEnabledIndex(itemCount, focusedIndex, isDisabled);
|
|
6
6
|
return (key) => M.value(key).pipe(M.when(nextKey, () => find(focusedIndex + 1, 1)), M.when(previousKey, () => find(focusedIndex - 1, -1)), M.whenOr('Home', 'PageUp', () => find(0, 1)), M.whenOr('End', 'PageDown', () => find(itemCount - 1, -1)), M.orElse(() => focusedIndex));
|