foldkit 0.70.0 → 0.71.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,203 @@
1
+ import { Array, Duration, Effect, Match as M, Number, Option, } from 'effect';
2
+ import * as Command from '../../command';
3
+ import { OptionExt } from '../../effectExtensions';
4
+ import { evo } from '../../struct';
5
+ import * as Task from '../../task';
6
+ import { Hid as TransitionHid, Showed as TransitionShowed, init as transitionInit, } from '../transition/schema';
7
+ import { defaultLeaveCommand as transitionDefaultLeaveCommand, update as transitionUpdate, } from '../transition/update';
8
+ import { DEFAULT_DURATION, Dismissed, DismissedAll, ElapsedDuration, GotTransitionMessage, makeAdded, makeEntry, makeMessage, makeModel, } from './schema';
9
+ /** Schedules an auto-dismiss timer for an entry. The result Message carries a
10
+ * version so stale timers (from hover or manual dismiss) are discarded in
11
+ * the update function. Static — the Command definition doesn't depend on
12
+ * payload. */
13
+ export const DismissAfter = Command.define('DismissAfter', ElapsedDuration);
14
+ const DEFAULT_VARIANT = 'Info';
15
+ /** Factory that binds Toast's runtime (update fn, helpers, commands) to a
16
+ * specific payload schema. Called by `make` in index.ts; inner helpers close
17
+ * over the payload-specific Entry / Model / Added types so generics don't
18
+ * have to propagate through every helper signature.
19
+ *
20
+ * @internal Consumers should use `Ui.Toast.make(PayloadSchema)`. This is
21
+ * only exported so `index.ts` can wire the view into the bound runtime. */
22
+ export const makeRuntime = (payloadSchema) => {
23
+ const EntrySchema = makeEntry(payloadSchema);
24
+ const ModelSchema = makeModel(payloadSchema);
25
+ const MessageSchema = makeMessage(payloadSchema);
26
+ const Added = makeAdded(payloadSchema);
27
+ const withUpdateReturn = M.withReturnType();
28
+ const updateEntry = (model, entryId, f) => evo(model, {
29
+ entries: Array.map(entry => (entry.id === entryId ? f(entry) : entry)),
30
+ });
31
+ const removeEntry = (model, entryId) => evo(model, {
32
+ entries: Array.filter(({ id }) => id !== entryId),
33
+ });
34
+ const isEntryLeaving = (entry) => {
35
+ const { transitionState } = entry.transition;
36
+ return (transitionState === 'LeaveStart' || transitionState === 'LeaveAnimating');
37
+ };
38
+ const scheduleDismiss = (entryId, version, duration) => DismissAfter(Task.delay(duration).pipe(Effect.as(ElapsedDuration({ entryId, version }))));
39
+ const rescheduleDismissCommands = (entry) => {
40
+ if (isEntryLeaving(entry) || entry.isHovered) {
41
+ return [];
42
+ }
43
+ else {
44
+ return Option.match(entry.maybeDuration, {
45
+ onNone: () => [],
46
+ onSome: duration => [
47
+ scheduleDismiss(entry.id, entry.pendingDismissVersion, duration),
48
+ ],
49
+ });
50
+ }
51
+ };
52
+ const delegateToEntryTransition = (model, entryId, transitionMessage) => {
53
+ const maybeEntry = Array.findFirst(model.entries, ({ id }) => id === entryId);
54
+ return Option.match(maybeEntry, {
55
+ onNone: () => [model, []],
56
+ onSome: entry => {
57
+ const [nextTransition, transitionCommands, maybeOutMessage] = transitionUpdate(entry.transition, transitionMessage);
58
+ const toMessage = (message) => GotTransitionMessage({ entryId, message });
59
+ const mappedCommands = transitionCommands.map(Command.mapEffect(Effect.map(toMessage)));
60
+ const nextEntry = evo(entry, {
61
+ transition: () => nextTransition,
62
+ });
63
+ return Option.match(maybeOutMessage, {
64
+ onNone: () => [
65
+ updateEntry(model, entryId, () => nextEntry),
66
+ mappedCommands,
67
+ ],
68
+ onSome: M.type().pipe(withUpdateReturn, M.tagsExhaustive({
69
+ StartedLeaveAnimating: () => [
70
+ updateEntry(model, entryId, () => nextEntry),
71
+ [
72
+ ...mappedCommands,
73
+ Command.mapEffect(transitionDefaultLeaveCommand(nextTransition), Effect.map(toMessage)),
74
+ ],
75
+ ],
76
+ TransitionedOut: () => [
77
+ removeEntry(model, entryId),
78
+ mappedCommands,
79
+ ],
80
+ })),
81
+ });
82
+ },
83
+ });
84
+ };
85
+ const createEntry = (model, input) => {
86
+ const entryId = `${model.id}-entry-${model.nextEntryKey}`;
87
+ const duration = input.duration === undefined
88
+ ? model.defaultDuration
89
+ : Duration.decode(input.duration);
90
+ const maybeDuration = OptionExt.when(!input.sticky, duration);
91
+ return {
92
+ id: entryId,
93
+ variant: input.variant ?? DEFAULT_VARIANT,
94
+ transition: transitionInit({ id: entryId, isShowing: false }),
95
+ maybeDuration,
96
+ pendingDismissVersion: 0,
97
+ isHovered: false,
98
+ payload: input.payload,
99
+ };
100
+ };
101
+ /** Creates an initial toast container model from a config. Starts empty. */
102
+ const init = (config) => ({
103
+ id: config.id,
104
+ defaultDuration: config.defaultDuration === undefined
105
+ ? DEFAULT_DURATION
106
+ : Duration.decode(config.defaultDuration),
107
+ entries: [],
108
+ nextEntryKey: 0,
109
+ });
110
+ /** Processes a toast message and returns the next model and commands. */
111
+ const update = (model, message) => M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
112
+ Added: ({ entry }) => {
113
+ const modelWithEntry = evo(model, {
114
+ entries: entries => Array.append(entries, entry),
115
+ nextEntryKey: Number.increment,
116
+ });
117
+ const [modelAfterShow, showCommands] = delegateToEntryTransition(modelWithEntry, entry.id, TransitionShowed());
118
+ const postShowEntry = Array.findFirst(modelAfterShow.entries, ({ id }) => id === entry.id);
119
+ const dismissCommands = Option.match(postShowEntry, {
120
+ onNone: () => [],
121
+ onSome: rescheduleDismissCommands,
122
+ });
123
+ return [modelAfterShow, [...showCommands, ...dismissCommands]];
124
+ },
125
+ Dismissed: ({ entryId }) => {
126
+ const maybeEntry = Array.findFirst(model.entries, ({ id }) => id === entryId);
127
+ return Option.match(maybeEntry, {
128
+ onNone: () => [model, []],
129
+ onSome: entry => {
130
+ if (isEntryLeaving(entry)) {
131
+ return [model, []];
132
+ }
133
+ else {
134
+ return delegateToEntryTransition(model, entryId, TransitionHid());
135
+ }
136
+ },
137
+ });
138
+ },
139
+ DismissedAll: () => Array.reduce(model.entries, [model, []], ([currentModel, currentCommands], entry) => {
140
+ if (isEntryLeaving(entry)) {
141
+ return [currentModel, currentCommands];
142
+ }
143
+ const [nextModel, nextCommands] = delegateToEntryTransition(currentModel, entry.id, TransitionHid());
144
+ return [nextModel, [...currentCommands, ...nextCommands]];
145
+ }),
146
+ ElapsedDuration: ({ entryId, version }) => {
147
+ const maybeEntry = Array.findFirst(model.entries, ({ id }) => id === entryId);
148
+ return Option.match(maybeEntry, {
149
+ onNone: () => [model, []],
150
+ onSome: entry => {
151
+ const isStale = version !== entry.pendingDismissVersion;
152
+ if (isStale || isEntryLeaving(entry)) {
153
+ return [model, []];
154
+ }
155
+ else {
156
+ return delegateToEntryTransition(model, entryId, TransitionHid());
157
+ }
158
+ },
159
+ });
160
+ },
161
+ HoveredEntry: ({ entryId }) => [
162
+ updateEntry(model, entryId, entry => evo(entry, {
163
+ isHovered: () => true,
164
+ pendingDismissVersion: Number.increment,
165
+ })),
166
+ [],
167
+ ],
168
+ LeftEntry: ({ entryId }) => {
169
+ const maybeEntry = Array.findFirst(model.entries, ({ id }) => id === entryId);
170
+ return Option.match(maybeEntry, {
171
+ onNone: () => [model, []],
172
+ onSome: entry => {
173
+ const nextEntry = evo(entry, {
174
+ isHovered: () => false,
175
+ pendingDismissVersion: Number.increment,
176
+ });
177
+ return [
178
+ updateEntry(model, entryId, () => nextEntry),
179
+ rescheduleDismissCommands(nextEntry),
180
+ ];
181
+ },
182
+ });
183
+ },
184
+ GotTransitionMessage: ({ entryId, message: transitionMessage }) => delegateToEntryTransition(model, entryId, transitionMessage),
185
+ }));
186
+ /** Adds a new toast entry. */
187
+ const show = (model, input) => update(model, Added({ entry: createEntry(model, input) }));
188
+ /** Begins dismissing a specific entry. */
189
+ const dismiss = (model, entryId) => update(model, Dismissed({ entryId }));
190
+ /** Begins dismissing every currently-visible entry. */
191
+ const dismissAll = (model) => update(model, DismissedAll());
192
+ return {
193
+ Entry: EntrySchema,
194
+ Model: ModelSchema,
195
+ Message: MessageSchema,
196
+ Added,
197
+ init,
198
+ update,
199
+ show,
200
+ dismiss,
201
+ dismissAll,
202
+ };
203
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit",
3
- "version": "0.70.0",
3
+ "version": "0.71.0",
4
4
  "description": "A frontend framework for TypeScript, built on Effect, using The Elm Architecture",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -143,6 +143,10 @@
143
143
  "types": "./dist/ui/tabs/public.d.ts",
144
144
  "import": "./dist/ui/tabs/public.js"
145
145
  },
146
+ "./ui/toast": {
147
+ "types": "./dist/ui/toast/public.d.ts",
148
+ "import": "./dist/ui/toast/public.js"
149
+ },
146
150
  "./ui/tooltip": {
147
151
  "types": "./dist/ui/tooltip/public.d.ts",
148
152
  "import": "./dist/ui/tooltip/public.js"