foldkit 0.120.0 → 0.122.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.
@@ -1,9 +1,19 @@
1
- import { Effect, Queue, Stream } from 'effect';
1
+ import { Effect, Option, Queue, Stream } from 'effect';
2
2
  const resolveTarget = (target) => (typeof target === 'function' ? target() : target);
3
3
  /**
4
- * Build a Stream that emits a Message for every dispatch of a DOM event,
5
- * registering the listener when the Stream's scope opens and removing it when
6
- * the scope closes.
4
+ * Build a Stream that emits a Message for the dispatches of a DOM event the
5
+ * mapper chooses to keep, registering the listener when the Stream's scope
6
+ * opens and removing it when the scope closes.
7
+ *
8
+ * This is the filtered variant of `fromEvent`. Its `toMessage` returns
9
+ * `Option.some(message)` to emit and `Option.none()` to ignore the event, so a
10
+ * single listener can react to some dispatches while passing on the rest.
11
+ *
12
+ * Reach for this over a downstream `Stream.filterMap` whenever the decision to
13
+ * keep an event is paired with `event.preventDefault()`. The mapper runs
14
+ * synchronously inside the browser's event dispatch, so `preventDefault()`
15
+ * takes effect, while a downstream filter would run on a later turn after the
16
+ * default action has already happened.
7
17
  *
8
18
  * The listener lifecycle uses `Effect.acquireRelease`. The `addEventListener`
9
19
  * call happens inside the acquire Effect, and the matching
@@ -19,16 +29,22 @@ const resolveTarget = (target) => (typeof target === 'function' ? target() : tar
19
29
  * @example
20
30
  * ```typescript
21
31
  * const subscriptions = Subscription.make<Model, Message>()(entry => ({
22
- * shortcut: entry(
32
+ * searchShortcut: entry(
23
33
  * { isListening: S.Boolean },
24
34
  * {
25
35
  * modelToDependencies: model => ({ isListening: model.isListening }),
26
36
  * dependenciesToStream: ({ isListening }) =>
27
37
  * Stream.when(
28
- * Subscription.fromEvent<KeyboardEvent, Message>({
38
+ * Subscription.fromEventFilterMap<KeyboardEvent, Message>({
29
39
  * target: window,
30
40
  * type: 'keydown',
31
- * toMessage: event => PressedKey({ key: event.key }),
41
+ * toMessage: event => {
42
+ * if ((event.metaKey || event.ctrlKey) && event.key === 'k') {
43
+ * event.preventDefault()
44
+ * return Option.some(OpenedSearch())
45
+ * }
46
+ * return Option.none()
47
+ * },
32
48
  * }),
33
49
  * Effect.sync(() => isListening),
34
50
  * ),
@@ -37,14 +53,61 @@ const resolveTarget = (target) => (typeof target === 'function' ? target() : tar
37
53
  * }))
38
54
  * ```
39
55
  */
40
- export const fromEvent = (config) => Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => {
56
+ export const fromEventFilterMap = (config) => Stream.callback(queue => Effect.acquireRelease(Effect.sync(() => {
41
57
  const target = resolveTarget(config.target);
42
58
  const handleEvent = (event) => {
43
59
  /* eslint-disable-next-line @typescript-eslint/consistent-type-assertions */
44
- Queue.offerUnsafe(queue, config.toMessage(event));
60
+ const maybeMessage = config.toMessage(event);
61
+ if (Option.isSome(maybeMessage)) {
62
+ Queue.offerUnsafe(queue, maybeMessage.value);
63
+ }
45
64
  };
46
65
  target.addEventListener(config.type, handleEvent, config.options);
47
66
  return { target, handleEvent };
48
67
  }), ({ target, handleEvent }) => Effect.sync(() => {
49
68
  target.removeEventListener(config.type, handleEvent, config.options);
50
69
  })).pipe(Effect.flatMap(() => Effect.never)));
70
+ /**
71
+ * Build a Stream that emits a Message for every dispatch of a DOM event,
72
+ * registering the listener when the Stream's scope opens and removing it when
73
+ * the scope closes.
74
+ *
75
+ * The listener lifecycle uses `Effect.acquireRelease`. The `addEventListener`
76
+ * call happens inside the acquire Effect, and the matching
77
+ * `removeEventListener` is registered only after acquire completes, so the
78
+ * listener never leaks on interruption.
79
+ *
80
+ * This is a Stream, not a Subscription entry. Wrap it with
81
+ * `Subscription.persistent` for a listener whose lifetime spans the whole
82
+ * Subscriptions record, or plug it into a `Subscription.make` entry's
83
+ * `dependenciesToStream` (typically behind `Stream.when`) to gate it on a
84
+ * Model condition.
85
+ *
86
+ * For a listener that reacts to only some events, reach for
87
+ * `fromEventFilterMap`, whose mapper returns `Option<Message>`.
88
+ *
89
+ * @example
90
+ * ```typescript
91
+ * const subscriptions = Subscription.make<Model, Message>()(entry => ({
92
+ * shortcut: entry(
93
+ * { isListening: S.Boolean },
94
+ * {
95
+ * modelToDependencies: model => ({ isListening: model.isListening }),
96
+ * dependenciesToStream: ({ isListening }) =>
97
+ * Stream.when(
98
+ * Subscription.fromEvent<KeyboardEvent, Message>({
99
+ * target: window,
100
+ * type: 'keydown',
101
+ * toMessage: event => PressedKey({ key: event.key }),
102
+ * }),
103
+ * Effect.sync(() => isListening),
104
+ * ),
105
+ * },
106
+ * ),
107
+ * }))
108
+ * ```
109
+ */
110
+ export const fromEvent = (config) => fromEventFilterMap({
111
+ ...config,
112
+ toMessage: event => Option.some(config.toMessage(event)),
113
+ });
@@ -2,6 +2,6 @@ export { aggregate, lift, make, persistent } from '../runtime/subscription.js';
2
2
  export type { EntryWithoutKeepAlive, Subscription, Subscriptions, } from '../runtime/subscription.js';
3
3
  export { animationFrame } from './animationFrame.js';
4
4
  export type { AnimationFrameConfig } from './animationFrame.js';
5
- export { fromEvent } from './fromEvent.js';
6
- export type { FromEventConfig } from './fromEvent.js';
5
+ export { fromEvent, fromEventFilterMap } from './fromEvent.js';
6
+ export type { FromEventConfig, FromEventFilterMapConfig } from './fromEvent.js';
7
7
  //# sourceMappingURL=public.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/subscription/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAE9E,YAAY,EACV,qBAAqB,EACrB,YAAY,EACZ,aAAa,GACd,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAEpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAE/D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAE1C,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAA"}
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/subscription/public.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAA;AAE9E,YAAY,EACV,qBAAqB,EACrB,YAAY,EACZ,aAAa,GACd,MAAM,4BAA4B,CAAA;AAEnC,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AAEpD,YAAY,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAE/D,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAE9D,YAAY,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,gBAAgB,CAAA"}
@@ -1,3 +1,3 @@
1
1
  export { aggregate, lift, make, persistent } from '../runtime/subscription.js';
2
2
  export { animationFrame } from './animationFrame.js';
3
- export { fromEvent } from './fromEvent.js';
3
+ export { fromEvent, fromEventFilterMap } from './fromEvent.js';
@@ -1,11 +1,12 @@
1
1
  import { Schema as S } from 'effect';
2
- import type { Html } from '../../html/index.js';
3
- export declare const Model: S.Struct<{}>;
4
- export type Model = typeof Model.Type;
2
+ import type { Attribute, Html } from '../../html/index.js';
5
3
  export declare const IgnoredInteraction: import("../../schema/index.js").CallableTaggedStruct<"IgnoredInteraction", {}>;
6
4
  export declare const Message: S.Union<readonly [import("../../schema/index.js").CallableTaggedStruct<"IgnoredInteraction", {}>]>;
7
5
  export type Message = typeof Message.Type;
8
- export declare const initialModel: Model;
6
+ export type Model = Readonly<{
7
+ attribute: Attribute<Message>;
8
+ }>;
9
9
  export declare const update: (model: Model, message: Message) => readonly [Model, ReadonlyArray<never>];
10
- export declare const view: (_model: Model) => Html;
11
- //# sourceMappingURL=svgAttributes.d.ts.map
10
+ export declare const testId = "attribute-host";
11
+ export declare const view: (model: Model) => Html;
12
+ //# sourceMappingURL=attributes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attributes.d.ts","sourceRoot":"","sources":["../../../src/test/apps/attributes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEhD,OAAO,KAAK,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAA;AAM1D,eAAO,MAAM,kBAAkB,gFAA0B,CAAA;AAEzD,eAAO,MAAM,OAAO,oGAAgC,CAAA;AACpD,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,MAAM,MAAM,KAAK,GAAG,QAAQ,CAAC;IAAE,SAAS,EAAE,SAAS,CAAC,OAAO,CAAC,CAAA;CAAE,CAAC,CAAA;AAI/D,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,CAMrC,CAAA;AAMH,eAAO,MAAM,MAAM,mBAAU,CAAA;AAE7B,eAAO,MAAM,IAAI,GAAI,OAAO,KAAK,KAAG,IAInC,CAAA"}
@@ -0,0 +1,17 @@
1
+ import { Match as M, Schema as S } from 'effect';
2
+ import { html } from '../../html/index.js';
3
+ import { m } from '../../message/index.js';
4
+ // MESSAGE
5
+ export const IgnoredInteraction = m('IgnoredInteraction');
6
+ export const Message = S.Union([IgnoredInteraction]);
7
+ // UPDATE
8
+ export const update = (model, message) => M.value(message).pipe(M.withReturnType(), M.tagsExhaustive({
9
+ IgnoredInteraction: () => [model, []],
10
+ }));
11
+ // VIEW
12
+ const TEST_ID = 'attribute-host';
13
+ export const testId = TEST_ID;
14
+ export const view = (model) => {
15
+ const h = html();
16
+ return h.div([h.DataAttribute('testid', TEST_ID), model.attribute], []);
17
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit",
3
- "version": "0.120.0",
3
+ "version": "0.122.0",
4
4
  "description": "A TypeScript frontend framework, built on Effect and architected like Elm",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,6 +11,10 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js"
13
13
  },
14
+ "./asyncData": {
15
+ "types": "./dist/asyncData/public.d.ts",
16
+ "import": "./dist/asyncData/public.js"
17
+ },
14
18
  "./calendar": {
15
19
  "types": "./dist/calendar/public.d.ts",
16
20
  "import": "./dist/calendar/public.js"
@@ -1 +0,0 @@
1
- {"version":3,"file":"svgAttributes.d.ts","sourceRoot":"","sources":["../../../src/test/apps/svgAttributes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEhD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAA;AAM/C,eAAO,MAAM,KAAK,cAAe,CAAA;AACjC,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,eAAO,MAAM,kBAAkB,gFAA0B,CAAA;AAEzD,eAAO,MAAM,OAAO,oGAAgC,CAAA;AACpD,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAIzC,eAAO,MAAM,YAAY,EAAE,KAAU,CAAA;AAIrC,eAAO,MAAM,MAAM,GACjB,OAAO,KAAK,EACZ,SAAS,OAAO,KACf,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,KAAK,CAAC,CAMrC,CAAA;AAIH,eAAO,MAAM,IAAI,GAAI,QAAQ,KAAK,KAAG,IAgHpC,CAAA"}
@@ -1,106 +0,0 @@
1
- import { Match as M, Schema as S } from 'effect';
2
- import { html } from '../../html/index.js';
3
- import { m } from '../../message/index.js';
4
- // MODEL
5
- export const Model = S.Struct({});
6
- // MESSAGE
7
- export const IgnoredInteraction = m('IgnoredInteraction');
8
- export const Message = S.Union([IgnoredInteraction]);
9
- // INIT
10
- export const initialModel = {};
11
- // UPDATE
12
- export const update = (model, message) => M.value(message).pipe(M.withReturnType(), M.tagsExhaustive({
13
- IgnoredInteraction: () => [model, []],
14
- }));
15
- // VIEW
16
- export const view = (_model) => {
17
- const h = html();
18
- return h.svg([
19
- h.DataAttribute('testid', 'svg-root'),
20
- h.ViewBox('0 0 100 100'),
21
- h.PreserveAspectRatio('xMidYMid meet'),
22
- h.Color('black'),
23
- h.Overflow('visible'),
24
- ], [
25
- h.text([
26
- h.DataAttribute('testid', 'svg-text'),
27
- h.Dx('1'),
28
- h.Dy('2'),
29
- h.Rotate('15'),
30
- h.TextAnchor('middle'),
31
- h.DominantBaseline('central'),
32
- h.AlignmentBaseline('middle'),
33
- h.BaselineShift('super'),
34
- h.TextLength('40'),
35
- h.LengthAdjust('spacing'),
36
- h.FontFamily('serif'),
37
- h.FontSize('12'),
38
- h.FontWeight('bold'),
39
- h.FontStyle('italic'),
40
- h.LetterSpacing('1'),
41
- h.WordSpacing('2'),
42
- h.TextDecoration('underline'),
43
- h.WritingMode('horizontal-tb'),
44
- h.Visibility('visible'),
45
- h.Display('inline'),
46
- h.Cursor('pointer'),
47
- h.PointerEvents('none'),
48
- ], ['Label']),
49
- h.rect([
50
- h.DataAttribute('testid', 'svg-rect'),
51
- h.Rx('4'),
52
- h.Ry('4'),
53
- h.PathLength('100'),
54
- h.FillOpacity('0.5'),
55
- h.StrokeOpacity('0.8'),
56
- h.StrokeMiterlimit('2'),
57
- h.PaintOrder('stroke'),
58
- h.VectorEffect('non-scaling-stroke'),
59
- h.ShapeRendering('crispEdges'),
60
- h.TextRendering('optimizeLegibility'),
61
- h.ImageRendering('pixelated'),
62
- h.ClipPath('url(#clip)'),
63
- h.Mask('url(#mask)'),
64
- h.Filter('url(#filter)'),
65
- h.MarkerStart('url(#start)'),
66
- h.MarkerMid('url(#mid)'),
67
- h.MarkerEnd('url(#end)'),
68
- ], []),
69
- h.linearGradient([
70
- h.DataAttribute('testid', 'svg-gradient'),
71
- h.GradientUnits('userSpaceOnUse'),
72
- h.GradientTransform('rotate(45)'),
73
- h.SpreadMethod('reflect'),
74
- h.Fx('0.1'),
75
- h.Fy('0.2'),
76
- h.Fr('0.3'),
77
- h.ClipPathUnits('userSpaceOnUse'),
78
- h.MaskUnits('userSpaceOnUse'),
79
- h.MaskContentUnits('userSpaceOnUse'),
80
- h.FilterUnits('userSpaceOnUse'),
81
- h.PrimitiveUnits('userSpaceOnUse'),
82
- ], [
83
- h.stop([
84
- h.DataAttribute('testid', 'svg-stop'),
85
- h.Offset('0.5'),
86
- h.StopColor('red'),
87
- h.StopOpacity('0.9'),
88
- ], []),
89
- ]),
90
- h.pattern([
91
- h.DataAttribute('testid', 'svg-pattern'),
92
- h.PatternUnits('userSpaceOnUse'),
93
- h.PatternContentUnits('userSpaceOnUse'),
94
- h.PatternTransform('scale(2)'),
95
- ], []),
96
- h.marker([
97
- h.DataAttribute('testid', 'svg-marker'),
98
- h.MarkerWidth('6'),
99
- h.MarkerHeight('6'),
100
- h.MarkerUnits('strokeWidth'),
101
- h.RefX('3'),
102
- h.RefY('3'),
103
- h.Orient('auto'),
104
- ], []),
105
- ]);
106
- };