foldkit 0.69.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
+ };
@@ -0,0 +1,96 @@
1
+ import { Duration, Schema as S } from 'effect';
2
+ import * as Command from '../../command';
3
+ import { type Attribute, type Html } from '../../html';
4
+ import type { AnchorConfig } from '../anchor';
5
+ /** Schema for the tooltip component's state. `isOpen` is visibility; `isHovered` tracks pointer on trigger; `isFocused` tracks tooltip-affirming focus on the trigger (focus arriving without a preceding mouse press — keyboard, touch, or pen; mouse-click-induced focus is excluded since it doesn't affirm the user wants the tooltip visible); `isDismissed` suppresses re-opening after the user dismissed the tooltip (via Escape or left-click) until they disengage (leave or blur). `showDelay` is the hover-to-show duration. `maybeLastPointerType` records the most recent pointer type that pressed the trigger, so a mouse-click-induced focus can be distinguished from other focus. */
6
+ export declare const Model: S.Struct<{
7
+ id: typeof S.String;
8
+ isOpen: typeof S.Boolean;
9
+ isHovered: typeof S.Boolean;
10
+ isFocused: typeof S.Boolean;
11
+ isDismissed: typeof S.Boolean;
12
+ showDelay: typeof S.DurationFromMillis;
13
+ pendingShowVersion: typeof S.Number;
14
+ maybeLastPointerType: S.OptionFromSelf<typeof S.String>;
15
+ }>;
16
+ export type Model = typeof Model.Type;
17
+ /** Sent when the pointer enters the tooltip trigger. Starts the show-delay timer. */
18
+ export declare const EnteredTrigger: import("../../schema").CallableTaggedStruct<"EnteredTrigger", {}>;
19
+ /** Sent when the pointer leaves the tooltip trigger. Cancels any pending show-delay and hides the tooltip unless focus is active. */
20
+ export declare const LeftTrigger: import("../../schema").CallableTaggedStruct<"LeftTrigger", {}>;
21
+ /** Sent when focus enters the trigger. Shows the tooltip immediately unless the focus was caused by a mouse press, in which case the hover-delay path handles it instead. */
22
+ export declare const FocusedTrigger: import("../../schema").CallableTaggedStruct<"FocusedTrigger", {}>;
23
+ /** Sent when focus leaves the trigger. Hides the tooltip unless hover is active. */
24
+ export declare const BlurredTrigger: import("../../schema").CallableTaggedStruct<"BlurredTrigger", {}>;
25
+ /** Sent when Escape is pressed while the tooltip is visible. Hides the tooltip and flags `isDismissed` so hover and focus do not re-open it until the user disengages (leaves or blurs the trigger). */
26
+ export declare const PressedEscape: import("../../schema").CallableTaggedStruct<"PressedEscape", {}>;
27
+ /** Sent when a pointer presses the trigger. Records the pointer type so a following focus event from the same mouse click can be suppressed, and a left-click on an open tooltip dismisses it (the user is clicking the button for its action, not to keep the tooltip visible). */
28
+ export declare const PressedPointerOnTrigger: import("../../schema").CallableTaggedStruct<"PressedPointerOnTrigger", {
29
+ pointerType: typeof S.String;
30
+ button: typeof S.Number;
31
+ }>;
32
+ /** Sent when the show-delay timer fires. Carries a generation number that is compared against the current pending version to discard stale timers. */
33
+ export declare const ElapsedShowDelay: import("../../schema").CallableTaggedStruct<"ElapsedShowDelay", {
34
+ version: typeof S.Number;
35
+ }>;
36
+ /** Signals that the show-delay has changed (e.g. in response to a user preference, input-method change, or reduced-motion setting). Does not affect the current open/closed state; the new delay applies to the next hover. Typically dispatched via the `setShowDelay` helper. */
37
+ export declare const ChangedShowDelay: import("../../schema").CallableTaggedStruct<"ChangedShowDelay", {
38
+ showDelay: typeof S.DurationFromMillis;
39
+ }>;
40
+ /** Union of all messages the tooltip component can produce. */
41
+ export declare const Message: S.Union<[
42
+ typeof EnteredTrigger,
43
+ typeof LeftTrigger,
44
+ typeof FocusedTrigger,
45
+ typeof BlurredTrigger,
46
+ typeof PressedEscape,
47
+ typeof PressedPointerOnTrigger,
48
+ typeof ElapsedShowDelay,
49
+ typeof ChangedShowDelay
50
+ ]>;
51
+ export type EnteredTrigger = typeof EnteredTrigger.Type;
52
+ export type LeftTrigger = typeof LeftTrigger.Type;
53
+ export type FocusedTrigger = typeof FocusedTrigger.Type;
54
+ export type BlurredTrigger = typeof BlurredTrigger.Type;
55
+ export type PressedEscape = typeof PressedEscape.Type;
56
+ export type PressedPointerOnTrigger = typeof PressedPointerOnTrigger.Type;
57
+ export type Message = typeof Message.Type;
58
+ /** Configuration for creating a tooltip model with `init`. `showDelay` controls how long the pointer must hover before the tooltip appears (default 500ms). Accepts any `Duration.DurationInput` — a bare number is interpreted as milliseconds. Keyboard focus shows the tooltip immediately regardless of this value. */
59
+ export type InitConfig = Readonly<{
60
+ id: string;
61
+ showDelay?: Duration.DurationInput;
62
+ }>;
63
+ /** Creates an initial tooltip model from a config. Defaults to hidden. */
64
+ export declare const init: (config: InitConfig) => Model;
65
+ type UpdateReturn = readonly [Model, ReadonlyArray<Command.Command<Message>>];
66
+ /** Waits for the tooltip's show delay before emitting `ElapsedShowDelay`. The version is echoed back so a stale timer is ignored when the user leaves before the delay fires. */
67
+ export declare const ShowAfterDelay: Command.CommandDefinition<"ShowAfterDelay", {
68
+ readonly _tag: "ElapsedShowDelay";
69
+ readonly version: number;
70
+ }>;
71
+ /** Processes a tooltip message and returns the next model and commands. */
72
+ export declare const update: (model: Model, message: Message) => UpdateReturn;
73
+ /** Programmatically updates the tooltip's hover show-delay. Use this in response to user preference changes, input-method switches, or reduced-motion settings. The new delay applies to the next hover; any pending timer is unaffected (its stale version will discard harmlessly when it fires). */
74
+ export declare const setShowDelay: (model: Model, showDelay: Duration.DurationInput) => readonly [Model, ReadonlyArray<Command.Command<Message>>];
75
+ /** Configuration for rendering a tooltip with `view`. */
76
+ export type ViewConfig<Message> = Readonly<{
77
+ model: Model;
78
+ toParentMessage: (message: EnteredTrigger | LeftTrigger | FocusedTrigger | BlurredTrigger | PressedEscape | PressedPointerOnTrigger) => Message;
79
+ anchor: AnchorConfig;
80
+ triggerContent: Html;
81
+ triggerClassName?: string;
82
+ triggerAttributes?: ReadonlyArray<Attribute<Message>>;
83
+ content: Html;
84
+ panelClassName?: string;
85
+ panelAttributes?: ReadonlyArray<Attribute<Message>>;
86
+ isDisabled?: boolean;
87
+ className?: string;
88
+ attributes?: ReadonlyArray<Attribute<Message>>;
89
+ }>;
90
+ /** Renders a headless tooltip with an anchored non-interactive panel. Shows on hover (after delay) or focus (from keyboard, touch, or pen; mouse-click focus is excluded); hides on leave, blur, Escape, or left-click of the trigger. Uses `role="tooltip"` and links the trigger via `aria-describedby`. */
91
+ export declare const view: <Message>(config: ViewConfig<Message>) => Html;
92
+ /** Creates a memoized tooltip view. Static config is captured in a closure;
93
+ * only `model` and `toParentMessage` are compared per render via `createLazy`. */
94
+ export declare const lazy: <Message>(staticConfig: Omit<ViewConfig<Message>, "model" | "toParentMessage">) => ((model: Model, toParentMessage: ViewConfig<Message>["toParentMessage"]) => Html);
95
+ export {};
96
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/ui/tooltip/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,244 @@
1
+ import { Duration, Effect, Equal, Match as M, Number, Option, Schema as S, } from 'effect';
2
+ import * as Command from '../../command';
3
+ import { OptionExt } from '../../effectExtensions';
4
+ import { createLazy, html } from '../../html';
5
+ import { m } from '../../message';
6
+ import { evo } from '../../struct';
7
+ import * as Task from '../../task';
8
+ import { anchorHooks } from '../anchor';
9
+ // MODEL
10
+ /** Schema for the tooltip component's state. `isOpen` is visibility; `isHovered` tracks pointer on trigger; `isFocused` tracks tooltip-affirming focus on the trigger (focus arriving without a preceding mouse press — keyboard, touch, or pen; mouse-click-induced focus is excluded since it doesn't affirm the user wants the tooltip visible); `isDismissed` suppresses re-opening after the user dismissed the tooltip (via Escape or left-click) until they disengage (leave or blur). `showDelay` is the hover-to-show duration. `maybeLastPointerType` records the most recent pointer type that pressed the trigger, so a mouse-click-induced focus can be distinguished from other focus. */
11
+ export const Model = S.Struct({
12
+ id: S.String,
13
+ isOpen: S.Boolean,
14
+ isHovered: S.Boolean,
15
+ isFocused: S.Boolean,
16
+ isDismissed: S.Boolean,
17
+ showDelay: S.DurationFromMillis,
18
+ pendingShowVersion: S.Number,
19
+ maybeLastPointerType: S.OptionFromSelf(S.String),
20
+ });
21
+ // MESSAGE
22
+ /** Sent when the pointer enters the tooltip trigger. Starts the show-delay timer. */
23
+ export const EnteredTrigger = m('EnteredTrigger');
24
+ /** Sent when the pointer leaves the tooltip trigger. Cancels any pending show-delay and hides the tooltip unless focus is active. */
25
+ export const LeftTrigger = m('LeftTrigger');
26
+ /** Sent when focus enters the trigger. Shows the tooltip immediately unless the focus was caused by a mouse press, in which case the hover-delay path handles it instead. */
27
+ export const FocusedTrigger = m('FocusedTrigger');
28
+ /** Sent when focus leaves the trigger. Hides the tooltip unless hover is active. */
29
+ export const BlurredTrigger = m('BlurredTrigger');
30
+ /** Sent when Escape is pressed while the tooltip is visible. Hides the tooltip and flags `isDismissed` so hover and focus do not re-open it until the user disengages (leaves or blurs the trigger). */
31
+ export const PressedEscape = m('PressedEscape');
32
+ /** Sent when a pointer presses the trigger. Records the pointer type so a following focus event from the same mouse click can be suppressed, and a left-click on an open tooltip dismisses it (the user is clicking the button for its action, not to keep the tooltip visible). */
33
+ export const PressedPointerOnTrigger = m('PressedPointerOnTrigger', {
34
+ pointerType: S.String,
35
+ button: S.Number,
36
+ });
37
+ /** Sent when the show-delay timer fires. Carries a generation number that is compared against the current pending version to discard stale timers. */
38
+ export const ElapsedShowDelay = m('ElapsedShowDelay', {
39
+ version: S.Number,
40
+ });
41
+ /** Signals that the show-delay has changed (e.g. in response to a user preference, input-method change, or reduced-motion setting). Does not affect the current open/closed state; the new delay applies to the next hover. Typically dispatched via the `setShowDelay` helper. */
42
+ export const ChangedShowDelay = m('ChangedShowDelay', {
43
+ showDelay: S.DurationFromMillis,
44
+ });
45
+ /** Union of all messages the tooltip component can produce. */
46
+ export const Message = S.Union(EnteredTrigger, LeftTrigger, FocusedTrigger, BlurredTrigger, PressedEscape, PressedPointerOnTrigger, ElapsedShowDelay, ChangedShowDelay);
47
+ // INIT
48
+ const DEFAULT_SHOW_DELAY = Duration.millis(500);
49
+ const LEFT_MOUSE_BUTTON = 0;
50
+ /** Creates an initial tooltip model from a config. Defaults to hidden. */
51
+ export const init = (config) => ({
52
+ id: config.id,
53
+ isOpen: false,
54
+ isHovered: false,
55
+ isFocused: false,
56
+ isDismissed: false,
57
+ showDelay: config.showDelay === undefined
58
+ ? DEFAULT_SHOW_DELAY
59
+ : Duration.decode(config.showDelay),
60
+ pendingShowVersion: 0,
61
+ maybeLastPointerType: Option.none(),
62
+ });
63
+ const withUpdateReturn = M.withReturnType();
64
+ /** Waits for the tooltip's show delay before emitting `ElapsedShowDelay`. The version is echoed back so a stale timer is ignored when the user leaves before the delay fires. */
65
+ export const ShowAfterDelay = Command.define('ShowAfterDelay', ElapsedShowDelay);
66
+ /** Processes a tooltip message and returns the next model and commands. */
67
+ export const update = (model, message) => M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
68
+ EnteredTrigger: () => {
69
+ if (model.isOpen || model.isDismissed) {
70
+ return [evo(model, { isHovered: () => true }), []];
71
+ }
72
+ const nextVersion = Number.increment(model.pendingShowVersion);
73
+ return [
74
+ evo(model, {
75
+ isHovered: () => true,
76
+ pendingShowVersion: () => nextVersion,
77
+ }),
78
+ [
79
+ ShowAfterDelay(Task.delay(model.showDelay).pipe(Effect.as(ElapsedShowDelay({ version: nextVersion })))),
80
+ ],
81
+ ];
82
+ },
83
+ LeftTrigger: () => [
84
+ evo(model, {
85
+ isHovered: () => false,
86
+ isOpen: () => model.isFocused && model.isOpen,
87
+ isDismissed: () => false,
88
+ pendingShowVersion: Number.increment,
89
+ }),
90
+ [],
91
+ ],
92
+ FocusedTrigger: () => {
93
+ const isFromMousePress = Option.exists(model.maybeLastPointerType, Equal.equals('mouse'));
94
+ if (isFromMousePress) {
95
+ return [
96
+ evo(model, {
97
+ maybeLastPointerType: () => Option.none(),
98
+ }),
99
+ [],
100
+ ];
101
+ }
102
+ if (model.isDismissed) {
103
+ return [
104
+ evo(model, {
105
+ isFocused: () => true,
106
+ maybeLastPointerType: () => Option.none(),
107
+ }),
108
+ [],
109
+ ];
110
+ }
111
+ return [
112
+ evo(model, {
113
+ isFocused: () => true,
114
+ isOpen: () => true,
115
+ pendingShowVersion: Number.increment,
116
+ }),
117
+ [],
118
+ ];
119
+ },
120
+ BlurredTrigger: () => [
121
+ evo(model, {
122
+ isFocused: () => false,
123
+ isOpen: () => model.isHovered && model.isOpen,
124
+ isDismissed: () => false,
125
+ pendingShowVersion: Number.increment,
126
+ maybeLastPointerType: () => Option.none(),
127
+ }),
128
+ [],
129
+ ],
130
+ PressedEscape: () => [
131
+ evo(model, {
132
+ isOpen: () => false,
133
+ isDismissed: () => true,
134
+ pendingShowVersion: Number.increment,
135
+ }),
136
+ [],
137
+ ],
138
+ PressedPointerOnTrigger: ({ pointerType, button }) => {
139
+ const isLeftClickOnOpen = button === LEFT_MOUSE_BUTTON && model.isOpen;
140
+ if (isLeftClickOnOpen) {
141
+ return [
142
+ evo(model, {
143
+ maybeLastPointerType: () => Option.some(pointerType),
144
+ isOpen: () => false,
145
+ isFocused: () => false,
146
+ isDismissed: () => true,
147
+ pendingShowVersion: Number.increment,
148
+ }),
149
+ [],
150
+ ];
151
+ }
152
+ return [
153
+ evo(model, {
154
+ maybeLastPointerType: () => Option.some(pointerType),
155
+ }),
156
+ [],
157
+ ];
158
+ },
159
+ ElapsedShowDelay: ({ version }) => {
160
+ if (version !== model.pendingShowVersion) {
161
+ return [model, []];
162
+ }
163
+ if (!model.isHovered) {
164
+ return [model, []];
165
+ }
166
+ return [evo(model, { isOpen: () => true }), []];
167
+ },
168
+ ChangedShowDelay: ({ showDelay }) => [
169
+ evo(model, { showDelay: () => showDelay }),
170
+ [],
171
+ ],
172
+ }));
173
+ /** Programmatically updates the tooltip's hover show-delay. Use this in response to user preference changes, input-method switches, or reduced-motion settings. The new delay applies to the next hover; any pending timer is unaffected (its stale version will discard harmlessly when it fires). */
174
+ export const setShowDelay = (model, showDelay) => update(model, ChangedShowDelay({ showDelay: Duration.decode(showDelay) }));
175
+ /** Renders a headless tooltip with an anchored non-interactive panel. Shows on hover (after delay) or focus (from keyboard, touch, or pen; mouse-click focus is excluded); hides on leave, blur, Escape, or left-click of the trigger. Uses `role="tooltip"` and links the trigger via `aria-describedby`. */
176
+ export const view = (config) => {
177
+ const { div, AriaDescribedBy, AriaDisabled, Class, DataAttribute, Id, OnBlur, OnDestroy, OnFocus, OnInsert, OnKeyDownPreventDefault, OnMouseEnter, OnMouseLeave, OnPointerDown, Role, Style, Type, keyed, } = html();
178
+ const { model: { id, isOpen }, toParentMessage, anchor, triggerContent, triggerClassName, triggerAttributes = [], content, panelClassName, panelAttributes = [], isDisabled, className, attributes = [], } = config;
179
+ const handleTriggerKeyDown = (key) => M.value(key).pipe(M.when('Escape', () => OptionExt.when(isOpen, toParentMessage(PressedEscape()))), M.orElse(() => Option.none()));
180
+ const handleTriggerPointerDown = (pointerType, button) => Option.some(toParentMessage(PressedPointerOnTrigger({ pointerType, button })));
181
+ const resolvedTriggerAttributes = [
182
+ Id(`${id}-trigger`),
183
+ Type('button'),
184
+ AriaDescribedBy(`${id}-panel`),
185
+ ...(isOpen ? [DataAttribute('open', '')] : []),
186
+ ...(isDisabled
187
+ ? [AriaDisabled(true), DataAttribute('disabled', '')]
188
+ : [
189
+ OnMouseEnter(toParentMessage(EnteredTrigger())),
190
+ OnMouseLeave(toParentMessage(LeftTrigger())),
191
+ OnFocus(toParentMessage(FocusedTrigger())),
192
+ OnBlur(toParentMessage(BlurredTrigger())),
193
+ OnKeyDownPreventDefault(handleTriggerKeyDown),
194
+ OnPointerDown(handleTriggerPointerDown),
195
+ ]),
196
+ ...(triggerClassName ? [Class(triggerClassName)] : []),
197
+ ...triggerAttributes,
198
+ ];
199
+ const hooks = anchorHooks({
200
+ buttonId: `${id}-trigger`,
201
+ anchor,
202
+ interceptTab: false,
203
+ });
204
+ const anchorAttributes = [
205
+ Style({
206
+ position: 'absolute',
207
+ margin: '0',
208
+ visibility: 'hidden',
209
+ pointerEvents: 'none',
210
+ }),
211
+ OnInsert(hooks.onInsert),
212
+ OnDestroy(hooks.onDestroy),
213
+ ];
214
+ const resolvedPanelAttributes = [
215
+ Id(`${id}-panel`),
216
+ Role('tooltip'),
217
+ ...anchorAttributes,
218
+ ...(isOpen ? [DataAttribute('open', '')] : []),
219
+ ...(panelClassName ? [Class(panelClassName)] : []),
220
+ ...panelAttributes,
221
+ ];
222
+ const wrapperAttributes = [
223
+ ...(className ? [Class(className)] : []),
224
+ ...attributes,
225
+ ];
226
+ return div(wrapperAttributes, [
227
+ keyed('button')(`${id}-trigger`, resolvedTriggerAttributes, [
228
+ triggerContent,
229
+ ]),
230
+ ...(isOpen
231
+ ? [keyed('div')(`${id}-panel`, resolvedPanelAttributes, [content])]
232
+ : []),
233
+ ]);
234
+ };
235
+ /** Creates a memoized tooltip view. Static config is captured in a closure;
236
+ * only `model` and `toParentMessage` are compared per render via `createLazy`. */
237
+ export const lazy = (staticConfig) => {
238
+ const lazyView = createLazy();
239
+ return (model, toParentMessage) => lazyView((currentModel, currentToMessage) => view({
240
+ ...staticConfig,
241
+ model: currentModel,
242
+ toParentMessage: currentToMessage,
243
+ }), [model, toParentMessage]);
244
+ };
@@ -0,0 +1,4 @@
1
+ export { init, update, view, lazy, setShowDelay, Model, Message, EnteredTrigger, LeftTrigger, FocusedTrigger, BlurredTrigger, PressedEscape, PressedPointerOnTrigger, ElapsedShowDelay, ChangedShowDelay, ShowAfterDelay, } from './index';
2
+ export type { InitConfig, ViewConfig } from './index';
3
+ export type { AnchorConfig } from '../anchor';
4
+ //# sourceMappingURL=public.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../../src/ui/tooltip/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,IAAI,EACJ,IAAI,EACJ,YAAY,EACZ,KAAK,EACL,OAAO,EACP,cAAc,EACd,WAAW,EACX,cAAc,EACd,cAAc,EACd,aAAa,EACb,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,cAAc,GACf,MAAM,SAAS,CAAA;AAEhB,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAErD,YAAY,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA"}
@@ -0,0 +1 @@
1
+ export { init, update, view, lazy, setShowDelay, Model, Message, EnteredTrigger, LeftTrigger, FocusedTrigger, BlurredTrigger, PressedEscape, PressedPointerOnTrigger, ElapsedShowDelay, ChangedShowDelay, ShowAfterDelay, } from './index';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foldkit",
3
- "version": "0.69.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,14 @@
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
+ },
150
+ "./ui/tooltip": {
151
+ "types": "./dist/ui/tooltip/public.d.ts",
152
+ "import": "./dist/ui/tooltip/public.js"
153
+ },
146
154
  "./ui/transition": {
147
155
  "types": "./dist/ui/transition/public.d.ts",
148
156
  "import": "./dist/ui/transition/public.js"