@foldkit/ui 0.112.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/LICENSE +21 -0
- package/README.md +67 -0
- package/dist/anchor.d.ts +38 -0
- package/dist/anchor.d.ts.map +1 -0
- package/dist/anchor.js +142 -0
- package/dist/animation/index.d.ts +49 -0
- package/dist/animation/index.d.ts.map +1 -0
- package/dist/animation/index.js +75 -0
- package/dist/animation/public.d.ts +3 -0
- package/dist/animation/public.d.ts.map +1 -0
- package/dist/animation/public.js +1 -0
- package/dist/animation/schema.d.ts +43 -0
- package/dist/animation/schema.d.ts.map +1 -0
- package/dist/animation/schema.js +41 -0
- package/dist/animation/update.d.ts +24 -0
- package/dist/animation/update.d.ts.map +1 -0
- package/dist/animation/update.js +67 -0
- package/dist/button/index.d.ts +17 -0
- package/dist/button/index.d.ts.map +1 -0
- package/dist/button/index.js +22 -0
- package/dist/button/public.d.ts +3 -0
- package/dist/button/public.d.ts.map +1 -0
- package/dist/button/public.js +1 -0
- package/dist/calendar/index.d.ts +462 -0
- package/dist/calendar/index.d.ts.map +1 -0
- package/dist/calendar/index.js +825 -0
- package/dist/calendar/public.d.ts +3 -0
- package/dist/calendar/public.d.ts.map +1 -0
- package/dist/calendar/public.js +1 -0
- package/dist/checkbox/index.d.ts +119 -0
- package/dist/checkbox/index.d.ts.map +1 -0
- package/dist/checkbox/index.js +111 -0
- package/dist/checkbox/public.d.ts +3 -0
- package/dist/checkbox/public.d.ts.map +1 -0
- package/dist/checkbox/public.js +1 -0
- package/dist/combobox/multi.d.ts +183 -0
- package/dist/combobox/multi.d.ts.map +1 -0
- package/dist/combobox/multi.js +81 -0
- package/dist/combobox/multiPublic.d.ts +3 -0
- package/dist/combobox/multiPublic.d.ts.map +1 -0
- package/dist/combobox/multiPublic.js +1 -0
- package/dist/combobox/public.d.ts +7 -0
- package/dist/combobox/public.d.ts.map +1 -0
- package/dist/combobox/public.js +3 -0
- package/dist/combobox/shared.d.ts +423 -0
- package/dist/combobox/shared.d.ts.map +1 -0
- package/dist/combobox/shared.js +708 -0
- package/dist/combobox/single.d.ts +198 -0
- package/dist/combobox/single.d.ts.map +1 -0
- package/dist/combobox/single.js +106 -0
- package/dist/datePicker/index.d.ts +457 -0
- package/dist/datePicker/index.d.ts.map +1 -0
- package/dist/datePicker/index.js +318 -0
- package/dist/datePicker/public.d.ts +3 -0
- package/dist/datePicker/public.d.ts.map +1 -0
- package/dist/datePicker/public.js +1 -0
- package/dist/dialog/index.d.ts +160 -0
- package/dist/dialog/index.d.ts.map +1 -0
- package/dist/dialog/index.js +211 -0
- package/dist/dialog/public.d.ts +3 -0
- package/dist/dialog/public.d.ts.map +1 -0
- package/dist/dialog/public.js +1 -0
- package/dist/disclosure/index.d.ts +110 -0
- package/dist/disclosure/index.d.ts.map +1 -0
- package/dist/disclosure/index.js +111 -0
- package/dist/disclosure/public.d.ts +3 -0
- package/dist/disclosure/public.d.ts.map +1 -0
- package/dist/disclosure/public.js +1 -0
- package/dist/dragAndDrop/index.d.ts +540 -0
- package/dist/dragAndDrop/index.d.ts.map +1 -0
- package/dist/dragAndDrop/index.js +535 -0
- package/dist/dragAndDrop/public.d.ts +3 -0
- package/dist/dragAndDrop/public.d.ts.map +1 -0
- package/dist/dragAndDrop/public.js +1 -0
- package/dist/fieldset/index.d.ts +21 -0
- package/dist/fieldset/index.d.ts.map +1 -0
- package/dist/fieldset/index.js +25 -0
- package/dist/fieldset/public.d.ts +3 -0
- package/dist/fieldset/public.d.ts.map +1 -0
- package/dist/fieldset/public.js +1 -0
- package/dist/fileDrop/index.d.ts +109 -0
- package/dist/fileDrop/index.d.ts.map +1 -0
- package/dist/fileDrop/index.js +127 -0
- package/dist/fileDrop/public.d.ts +3 -0
- package/dist/fileDrop/public.d.ts.map +1 -0
- package/dist/fileDrop/public.js +1 -0
- package/dist/group.d.ts +8 -0
- package/dist/group.d.ts.map +1 -0
- package/dist/group.js +13 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/input/index.d.ts +26 -0
- package/dist/input/index.d.ts.map +1 -0
- package/dist/input/index.js +43 -0
- package/dist/input/public.d.ts +3 -0
- package/dist/input/public.d.ts.map +1 -0
- package/dist/input/public.js +1 -0
- package/dist/internal/optionExtensions.d.ts +6 -0
- package/dist/internal/optionExtensions.d.ts.map +1 -0
- package/dist/internal/optionExtensions.js +2 -0
- package/dist/keyboard.d.ts +6 -0
- package/dist/keyboard.d.ts.map +1 -0
- package/dist/keyboard.js +9 -0
- package/dist/listbox/multi.d.ts +189 -0
- package/dist/listbox/multi.d.ts.map +1 -0
- package/dist/listbox/multi.js +65 -0
- package/dist/listbox/multiPublic.d.ts +3 -0
- package/dist/listbox/multiPublic.d.ts.map +1 -0
- package/dist/listbox/multiPublic.js +1 -0
- package/dist/listbox/public.d.ts +7 -0
- package/dist/listbox/public.d.ts.map +1 -0
- package/dist/listbox/public.js +3 -0
- package/dist/listbox/shared.d.ts +432 -0
- package/dist/listbox/shared.d.ts.map +1 -0
- package/dist/listbox/shared.js +670 -0
- package/dist/listbox/single.d.ts +207 -0
- package/dist/listbox/single.d.ts.map +1 -0
- package/dist/listbox/single.js +73 -0
- package/dist/menu/index.d.ts +368 -0
- package/dist/menu/index.d.ts.map +1 -0
- package/dist/menu/index.js +682 -0
- package/dist/menu/public.d.ts +4 -0
- package/dist/menu/public.d.ts.map +1 -0
- package/dist/menu/public.js +1 -0
- package/dist/popover/index.d.ts +267 -0
- package/dist/popover/index.d.ts.map +1 -0
- package/dist/popover/index.js +346 -0
- package/dist/popover/public.d.ts +4 -0
- package/dist/popover/public.d.ts.map +1 -0
- package/dist/popover/public.js +1 -0
- package/dist/radioGroup/index.d.ts +169 -0
- package/dist/radioGroup/index.d.ts.map +1 -0
- package/dist/radioGroup/index.js +197 -0
- package/dist/radioGroup/public.d.ts +3 -0
- package/dist/radioGroup/public.d.ts.map +1 -0
- package/dist/radioGroup/public.js +1 -0
- package/dist/select/index.d.ts +24 -0
- package/dist/select/index.d.ts.map +1 -0
- package/dist/select/index.js +40 -0
- package/dist/select/public.d.ts +3 -0
- package/dist/select/public.d.ts.map +1 -0
- package/dist/select/public.js +1 -0
- package/dist/slider/index.d.ts +318 -0
- package/dist/slider/index.d.ts.map +1 -0
- package/dist/slider/index.js +337 -0
- package/dist/slider/public.d.ts +3 -0
- package/dist/slider/public.d.ts.map +1 -0
- package/dist/slider/public.js +1 -0
- package/dist/switch/index.d.ts +99 -0
- package/dist/switch/index.d.ts.map +1 -0
- package/dist/switch/index.js +107 -0
- package/dist/switch/public.d.ts +3 -0
- package/dist/switch/public.d.ts.map +1 -0
- package/dist/switch/public.js +1 -0
- package/dist/tabs/index.d.ts +155 -0
- package/dist/tabs/index.d.ts.map +1 -0
- package/dist/tabs/index.js +185 -0
- package/dist/tabs/public.d.ts +3 -0
- package/dist/tabs/public.d.ts.map +1 -0
- package/dist/tabs/public.js +1 -0
- package/dist/test/apps/disabledButton.d.ts +38 -0
- package/dist/test/apps/disabledButton.d.ts.map +1 -0
- package/dist/test/apps/disabledButton.js +71 -0
- package/dist/textarea/index.d.ts +26 -0
- package/dist/textarea/index.d.ts.map +1 -0
- package/dist/textarea/index.js +44 -0
- package/dist/textarea/public.d.ts +3 -0
- package/dist/textarea/public.d.ts.map +1 -0
- package/dist/textarea/public.js +1 -0
- package/dist/toast/index.d.ts +608 -0
- package/dist/toast/index.d.ts.map +1 -0
- package/dist/toast/index.js +146 -0
- package/dist/toast/public.d.ts +4 -0
- package/dist/toast/public.d.ts.map +1 -0
- package/dist/toast/public.js +1 -0
- package/dist/toast/schema.d.ts +154 -0
- package/dist/toast/schema.d.ts.map +1 -0
- package/dist/toast/schema.js +93 -0
- package/dist/toast/update.d.ts +510 -0
- package/dist/toast/update.d.ts.map +1 -0
- package/dist/toast/update.js +225 -0
- package/dist/tooltip/index.d.ts +170 -0
- package/dist/tooltip/index.d.ts.map +1 -0
- package/dist/tooltip/index.js +253 -0
- package/dist/tooltip/public.d.ts +4 -0
- package/dist/tooltip/public.d.ts.map +1 -0
- package/dist/tooltip/public.js +1 -0
- package/dist/typeahead.d.ts +4 -0
- package/dist/typeahead.d.ts.map +1 -0
- package/dist/typeahead.js +14 -0
- package/dist/virtualList/index.d.ts +203 -0
- package/dist/virtualList/index.d.ts.map +1 -0
- package/dist/virtualList/index.js +392 -0
- package/dist/virtualList/public.d.ts +3 -0
- package/dist/virtualList/public.d.ts.map +1 -0
- package/dist/virtualList/public.js +1 -0
- package/dist/vitest-setup.d.ts +2 -0
- package/dist/vitest-setup.d.ts.map +1 -0
- package/dist/vitest-setup.js +2 -0
- package/package.json +161 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Devin Jameson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @foldkit/ui
|
|
2
|
+
|
|
3
|
+
Headless, accessible UI components for [Foldkit](https://foldkit.dev).
|
|
4
|
+
|
|
5
|
+
Each component ships behavior, not markup: the ARIA attributes, keyboard navigation, focus management, and state machine. You provide the elements and the styling. Components follow The Elm Architecture, so they compose into a Foldkit app the same way the rest of your code does.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @foldkit/ui
|
|
11
|
+
# or
|
|
12
|
+
npm install @foldkit/ui
|
|
13
|
+
# or
|
|
14
|
+
yarn add @foldkit/ui
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`@foldkit/ui` lists `foldkit` and `effect` as peer dependencies, so install those alongside it.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
Import a component by name. Each import is a namespace that groups the component's `view` and, for stateful components, its `Model`, `Message`, `init`, and `update`.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { html } from 'foldkit/html'
|
|
25
|
+
|
|
26
|
+
import { Button } from '@foldkit/ui'
|
|
27
|
+
|
|
28
|
+
const view = () => {
|
|
29
|
+
const h = html<Message>()
|
|
30
|
+
|
|
31
|
+
return Button.view({
|
|
32
|
+
onClick: ClickedSave(), // your Message
|
|
33
|
+
toView: attributes =>
|
|
34
|
+
h.button(
|
|
35
|
+
[...attributes.button, h.Class('px-4 py-2 rounded-lg')],
|
|
36
|
+
['Save'],
|
|
37
|
+
),
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Components come in two shapes:
|
|
43
|
+
|
|
44
|
+
- **Render helpers** (Button, Input, Textarea, Select, Fieldset) are stateless. Call their `view` directly with a typed config.
|
|
45
|
+
- **Submodels** (Checkbox, Combobox, Dialog, Listbox, Menu, Popover, RadioGroup, and the like) own a Model. Embed them with `h.submodel`, drive them through their `update`, and consume their `OutMessage` in the parent.
|
|
46
|
+
|
|
47
|
+
Every component is also available as a subpath import:
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { Button } from '@foldkit/ui/button'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
When a component name collides with another import (for example core's `Calendar`), alias it:
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { Calendar as UiCalendar } from '@foldkit/ui'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Components
|
|
60
|
+
|
|
61
|
+
Animation, Button, Calendar, Checkbox, Combobox, DatePicker, Dialog, Disclosure, DragAndDrop, Fieldset, FileDrop, Input, Listbox, Menu, Popover, RadioGroup, Select, Slider, Switch, Tabs, Textarea, Toast, Tooltip, and VirtualList.
|
|
62
|
+
|
|
63
|
+
See the [component documentation](https://foldkit.dev/ui/overview) for the full API and a live example of each.
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
MIT
|
package/dist/anchor.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Schema as S } from 'effect';
|
|
2
|
+
/** Schema mirroring `@floating-ui/dom`'s `Placement` literal union: a side
|
|
3
|
+
* (`top`/`right`/`bottom`/`left`) optionally suffixed with `-start` or `-end`. */
|
|
4
|
+
export declare const Placement: S.Literals<readonly ["top", "right", "bottom", "left", "top-start", "top-end", "right-start", "right-end", "bottom-start", "bottom-end", "left-start", "left-end"]>;
|
|
5
|
+
/** Static configuration for anchor-based positioning of a floating element relative to a button. */
|
|
6
|
+
export declare const AnchorConfig: S.Struct<{
|
|
7
|
+
readonly placement: S.optional<S.Literals<readonly ["top", "right", "bottom", "left", "top-start", "top-end", "right-start", "right-end", "bottom-start", "bottom-end", "left-start", "left-end"]>>;
|
|
8
|
+
readonly gap: S.optional<S.Number>;
|
|
9
|
+
readonly offset: S.optional<S.Number>;
|
|
10
|
+
readonly padding: S.optional<S.Number>;
|
|
11
|
+
readonly portal: S.optional<S.Boolean>;
|
|
12
|
+
}>;
|
|
13
|
+
export type AnchorConfig = typeof AnchorConfig.Type;
|
|
14
|
+
/** Relocates an element into the shared `foldkit-portal-root` div appended to
|
|
15
|
+
* `document.body`, escaping any ancestor stacking context. Returns a cleanup
|
|
16
|
+
* function that removes the element from the portal root. Designed to be
|
|
17
|
+
* called from inside an `OnMount` action: the consumer wraps the call in
|
|
18
|
+
* `Effect.sync` and stashes the returned cleanup in the `Mount` result. */
|
|
19
|
+
export declare const portalToBody: (element: Element) => (() => void);
|
|
20
|
+
/** Positions a floating element relative to its button using Floating UI, then
|
|
21
|
+
* returns a cleanup function. Designed to be called inside an `OnMount`
|
|
22
|
+
* action: the consumer wraps the call in `Effect.sync` and stashes the
|
|
23
|
+
* returned cleanup in the `Mount` result. When `interceptTab` is true
|
|
24
|
+
* (default), Tab key in portal mode refocuses the button — set to false for
|
|
25
|
+
* components like Popover where Tab should navigate naturally within the
|
|
26
|
+
* panel. When `focusAfterPosition` is true, the element is focused after the
|
|
27
|
+
* first position computation clears visibility — deferred via
|
|
28
|
+
* requestAnimationFrame so the element is painted before focus fires.
|
|
29
|
+
* `focusSelector` optionally targets a descendant (e.g. a calendar grid
|
|
30
|
+
* inside a popover panel) instead of the panel itself. */
|
|
31
|
+
export declare const anchorSetup: (config: {
|
|
32
|
+
buttonId: string;
|
|
33
|
+
anchor: AnchorConfig;
|
|
34
|
+
interceptTab?: boolean;
|
|
35
|
+
focusAfterPosition?: boolean;
|
|
36
|
+
focusSelector?: string;
|
|
37
|
+
}) => (element: Element) => (() => void);
|
|
38
|
+
//# sourceMappingURL=anchor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"anchor.d.ts","sourceRoot":"","sources":["../src/anchor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAW9C;mFACmF;AACnF,eAAO,MAAM,SAAS,qKAapB,CAAA;AAEF,oGAAoG;AACpG,eAAO,MAAM,YAAY;;;;;;EAMvB,CAAA;AAEF,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAA;AAwBnD;;;;4EAI4E;AAC5E,eAAO,MAAM,YAAY,GAAI,SAAS,OAAO,KAAG,CAAC,MAAM,IAAI,CAW1D,CAAA;AAED;;;;;;;;;;2DAU2D;AAC3D,eAAO,MAAM,WAAW,GACrB,QAAQ;IACP,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,YAAY,CAAA;IACpB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAA;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB,MACA,SAAS,OAAO,KAAG,CAAC,MAAM,IAAI,CAiF9B,CAAA"}
|
package/dist/anchor.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Function, Schema as S } from 'effect';
|
|
2
|
+
import { autoUpdate, computePosition, flip, offset as floatingOffset, shift, size, } from '@floating-ui/dom';
|
|
3
|
+
/** Schema mirroring `@floating-ui/dom`'s `Placement` literal union: a side
|
|
4
|
+
* (`top`/`right`/`bottom`/`left`) optionally suffixed with `-start` or `-end`. */
|
|
5
|
+
export const Placement = S.Literals([
|
|
6
|
+
'top',
|
|
7
|
+
'right',
|
|
8
|
+
'bottom',
|
|
9
|
+
'left',
|
|
10
|
+
'top-start',
|
|
11
|
+
'top-end',
|
|
12
|
+
'right-start',
|
|
13
|
+
'right-end',
|
|
14
|
+
'bottom-start',
|
|
15
|
+
'bottom-end',
|
|
16
|
+
'left-start',
|
|
17
|
+
'left-end',
|
|
18
|
+
]);
|
|
19
|
+
/** Static configuration for anchor-based positioning of a floating element relative to a button. */
|
|
20
|
+
export const AnchorConfig = S.Struct({
|
|
21
|
+
placement: S.optional(Placement),
|
|
22
|
+
gap: S.optional(S.Number),
|
|
23
|
+
offset: S.optional(S.Number),
|
|
24
|
+
padding: S.optional(S.Number),
|
|
25
|
+
portal: S.optional(S.Boolean),
|
|
26
|
+
});
|
|
27
|
+
const PORTAL_ROOT_ID = 'foldkit-portal-root';
|
|
28
|
+
const getOrCreatePortalRoot = () => {
|
|
29
|
+
const existing = document.getElementById(PORTAL_ROOT_ID);
|
|
30
|
+
if (existing) {
|
|
31
|
+
return existing;
|
|
32
|
+
}
|
|
33
|
+
const root = document.createElement('div');
|
|
34
|
+
root.id = PORTAL_ROOT_ID;
|
|
35
|
+
// NOTE: prepended (not appended) so portaled overlays sit BEFORE the page's
|
|
36
|
+
// listbox/popover/menu wrappers in tree order. Those wrappers are
|
|
37
|
+
// `position: relative; z-index: auto` and paint at CSS step 8 in tree order;
|
|
38
|
+
// a backdrop appended after them would paint on top of every button on the
|
|
39
|
+
// page, breaking click-outside detection. Prepending makes wrappers paint
|
|
40
|
+
// above the backdrop, while panels (z-10) still win via step 9.
|
|
41
|
+
document.body.insertBefore(root, document.body.firstChild);
|
|
42
|
+
return root;
|
|
43
|
+
};
|
|
44
|
+
/** Relocates an element into the shared `foldkit-portal-root` div appended to
|
|
45
|
+
* `document.body`, escaping any ancestor stacking context. Returns a cleanup
|
|
46
|
+
* function that removes the element from the portal root. Designed to be
|
|
47
|
+
* called from inside an `OnMount` action: the consumer wraps the call in
|
|
48
|
+
* `Effect.sync` and stashes the returned cleanup in the `Mount` result. */
|
|
49
|
+
export const portalToBody = (element) => {
|
|
50
|
+
getOrCreatePortalRoot().appendChild(element);
|
|
51
|
+
return () => {
|
|
52
|
+
try {
|
|
53
|
+
element.remove();
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// NOTE: a re-render may unmount the element before this cleanup fires,
|
|
57
|
+
// so the remove() call can throw on a node that's already been removed.
|
|
58
|
+
// Swallow the error.
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
/** Positions a floating element relative to its button using Floating UI, then
|
|
63
|
+
* returns a cleanup function. Designed to be called inside an `OnMount`
|
|
64
|
+
* action: the consumer wraps the call in `Effect.sync` and stashes the
|
|
65
|
+
* returned cleanup in the `Mount` result. When `interceptTab` is true
|
|
66
|
+
* (default), Tab key in portal mode refocuses the button — set to false for
|
|
67
|
+
* components like Popover where Tab should navigate naturally within the
|
|
68
|
+
* panel. When `focusAfterPosition` is true, the element is focused after the
|
|
69
|
+
* first position computation clears visibility — deferred via
|
|
70
|
+
* requestAnimationFrame so the element is painted before focus fires.
|
|
71
|
+
* `focusSelector` optionally targets a descendant (e.g. a calendar grid
|
|
72
|
+
* inside a popover panel) instead of the panel itself. */
|
|
73
|
+
export const anchorSetup = (config) => (element) => {
|
|
74
|
+
const button = document.getElementById(config.buttonId);
|
|
75
|
+
if (!(button instanceof HTMLElement) || !(element instanceof HTMLElement)) {
|
|
76
|
+
return Function.constVoid;
|
|
77
|
+
}
|
|
78
|
+
const isPortal = config.anchor.portal ?? true;
|
|
79
|
+
const portalCleanup = isPortal ? portalToBody(element) : undefined;
|
|
80
|
+
const { placement, gap, offset: crossAxis, padding } = config.anchor;
|
|
81
|
+
const shouldInterceptTab = config.interceptTab ?? true;
|
|
82
|
+
let isFirstUpdate = true;
|
|
83
|
+
const floatingCleanup = autoUpdate(button, element, () => {
|
|
84
|
+
computePosition(button, element, {
|
|
85
|
+
placement: placement ?? 'bottom-start',
|
|
86
|
+
strategy: 'absolute',
|
|
87
|
+
middleware: [
|
|
88
|
+
floatingOffset({
|
|
89
|
+
mainAxis: gap ?? 0,
|
|
90
|
+
crossAxis: crossAxis ?? 0,
|
|
91
|
+
}),
|
|
92
|
+
flip({ padding: padding ?? 0 }),
|
|
93
|
+
shift({ padding: padding ?? 0 }),
|
|
94
|
+
size({
|
|
95
|
+
padding: padding ?? 0,
|
|
96
|
+
apply({ rects, availableHeight }) {
|
|
97
|
+
element.style.setProperty('--button-width', `${rects.reference.width}px`);
|
|
98
|
+
element.style.maxHeight = `${availableHeight}px`;
|
|
99
|
+
element.style.overflowY = 'auto';
|
|
100
|
+
element.style.overscrollBehavior = 'none';
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
}).then(({ x, y }) => {
|
|
105
|
+
element.style.left = `${x}px`;
|
|
106
|
+
element.style.top = `${y}px`;
|
|
107
|
+
if (isFirstUpdate) {
|
|
108
|
+
isFirstUpdate = false;
|
|
109
|
+
element.style.visibility = '';
|
|
110
|
+
if (config.focusAfterPosition ?? false) {
|
|
111
|
+
requestAnimationFrame(() => {
|
|
112
|
+
const target = config.focusSelector
|
|
113
|
+
? document.querySelector(config.focusSelector)
|
|
114
|
+
: element;
|
|
115
|
+
if (target instanceof HTMLElement) {
|
|
116
|
+
target.focus();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
if (isPortal && shouldInterceptTab) {
|
|
124
|
+
const handleTabKey = (event) => {
|
|
125
|
+
if (event instanceof KeyboardEvent && event.key === 'Tab') {
|
|
126
|
+
button.focus();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
element.addEventListener('keydown', handleTabKey);
|
|
130
|
+
return () => {
|
|
131
|
+
floatingCleanup();
|
|
132
|
+
element.removeEventListener('keydown', handleTabKey);
|
|
133
|
+
portalCleanup?.();
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
return () => {
|
|
138
|
+
floatingCleanup();
|
|
139
|
+
portalCleanup?.();
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type ChildAttribute, type Html, type TagName } from 'foldkit/html';
|
|
2
|
+
import { AdvancedAnimationFrame, EndedAnimation, Hid, Message, Model, OutMessage, Showed, StartedLeaveAnimating, TransitionState, TransitionedOut, init } from './schema.js';
|
|
3
|
+
import { RequestFrame, WaitForAnimationSettled, defaultLeaveCommand, update } from './update.js';
|
|
4
|
+
export type { InitConfig } from './schema.js';
|
|
5
|
+
export { AdvancedAnimationFrame, EndedAnimation, Hid, init, Message, Model, OutMessage, Showed, StartedLeaveAnimating, TransitionState, TransitionedOut, };
|
|
6
|
+
export { RequestFrame, WaitForAnimationSettled, defaultLeaveCommand, update };
|
|
7
|
+
/** Per-render view inputs passed to `view` via `h.submodel`'s `viewInputs` field. */
|
|
8
|
+
export type ViewInputs = Readonly<{
|
|
9
|
+
content: Html;
|
|
10
|
+
className?: string;
|
|
11
|
+
attributes?: ReadonlyArray<ChildAttribute>;
|
|
12
|
+
element?: TagName;
|
|
13
|
+
/** When true, wraps content in a CSS grid container that smoothly animates
|
|
14
|
+
* height via `grid-template-rows: 0fr → 1fr`. The element stays in the DOM
|
|
15
|
+
* when hidden (collapsed to zero height) instead of being removed. */
|
|
16
|
+
animateSize?: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
/** Renders a headless animation wrapper that coordinates CSS transitions and
|
|
19
|
+
* CSS keyframe animations via data attributes.
|
|
20
|
+
*
|
|
21
|
+
* Data attributes reflect the current lifecycle phase:
|
|
22
|
+
* - `data-closed`: element is in its hidden/initial state
|
|
23
|
+
* - `data-enter`: enter animation is active
|
|
24
|
+
* - `data-leave`: leave animation is active
|
|
25
|
+
* - `data-transition`: any animation is active
|
|
26
|
+
*/
|
|
27
|
+
export declare const view: import("foldkit/submodel").View<{
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly isShowing: boolean;
|
|
30
|
+
readonly transitionState: "Idle" | "EnterStart" | "EnterAnimating" | "LeaveStart" | "LeaveAnimating";
|
|
31
|
+
}, {
|
|
32
|
+
readonly _tag: "Showed";
|
|
33
|
+
} | {
|
|
34
|
+
readonly _tag: "Hid";
|
|
35
|
+
} | {
|
|
36
|
+
readonly _tag: "AdvancedAnimationFrame";
|
|
37
|
+
} | {
|
|
38
|
+
readonly _tag: "EndedAnimation";
|
|
39
|
+
}, Readonly<{
|
|
40
|
+
content: Html;
|
|
41
|
+
className?: string;
|
|
42
|
+
attributes?: ReadonlyArray<ChildAttribute>;
|
|
43
|
+
element?: TagName;
|
|
44
|
+
/** When true, wraps content in a CSS grid container that smoothly animates
|
|
45
|
+
* height via `grid-template-rows: 0fr → 1fr`. The element stays in the DOM
|
|
46
|
+
* when hidden (collapsed to zero height) instead of being removed. */
|
|
47
|
+
animateSize?: boolean;
|
|
48
|
+
}>>;
|
|
49
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/animation/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,IAAI,EACT,KAAK,OAAO,EAEb,MAAM,cAAc,CAAA;AAGrB,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,GAAG,EACH,OAAO,EACP,KAAK,EACL,UAAU,EACV,MAAM,EACN,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,IAAI,EACL,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,YAAY,EACZ,uBAAuB,EACvB,mBAAmB,EACnB,MAAM,EACP,MAAM,aAAa,CAAA;AAEpB,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC7C,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,GAAG,EACH,IAAI,EACJ,OAAO,EACP,KAAK,EACL,UAAU,EACV,MAAM,EACN,qBAAqB,EACrB,eAAe,EACf,eAAe,GAChB,CAAA;AAED,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,MAAM,EAAE,CAAA;AAI7E,qFAAqF;AACrF,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,OAAO,EAAE,IAAI,CAAA;IACb,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,aAAa,CAAC,cAAc,CAAC,CAAA;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB;;2EAEuE;IACvE,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,CAAC,CAAA;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,IAAI;;;;;;;;;;;;;aAnBN,IAAI;gBACD,MAAM;iBACL,aAAa,CAAC,cAAc,CAAC;cAChC,OAAO;IACjB;;2EAEuE;kBACzD,OAAO;GA6GtB,CAAA"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Match as M } from 'effect';
|
|
2
|
+
import { html, } from 'foldkit/html';
|
|
3
|
+
import { defineView } from 'foldkit/submodel';
|
|
4
|
+
import { AdvancedAnimationFrame, EndedAnimation, Hid, Message, Model, OutMessage, Showed, StartedLeaveAnimating, TransitionState, TransitionedOut, init, } from './schema.js';
|
|
5
|
+
import { RequestFrame, WaitForAnimationSettled, defaultLeaveCommand, update, } from './update.js';
|
|
6
|
+
export { AdvancedAnimationFrame, EndedAnimation, Hid, init, Message, Model, OutMessage, Showed, StartedLeaveAnimating, TransitionState, TransitionedOut, };
|
|
7
|
+
export { RequestFrame, WaitForAnimationSettled, defaultLeaveCommand, update };
|
|
8
|
+
/** Renders a headless animation wrapper that coordinates CSS transitions and
|
|
9
|
+
* CSS keyframe animations via data attributes.
|
|
10
|
+
*
|
|
11
|
+
* Data attributes reflect the current lifecycle phase:
|
|
12
|
+
* - `data-closed`: element is in its hidden/initial state
|
|
13
|
+
* - `data-enter`: enter animation is active
|
|
14
|
+
* - `data-leave`: leave animation is active
|
|
15
|
+
* - `data-transition`: any animation is active
|
|
16
|
+
*/
|
|
17
|
+
export const view = defineView((model, viewInputs) => {
|
|
18
|
+
const h = html();
|
|
19
|
+
const { id, isShowing, transitionState } = model;
|
|
20
|
+
const { content, className, attributes = [], element = 'div', animateSize = false, } = viewInputs;
|
|
21
|
+
const isLeaving = transitionState === 'LeaveStart' || transitionState === 'LeaveAnimating';
|
|
22
|
+
const isVisible = isShowing || isLeaving;
|
|
23
|
+
const transitionAttributes = M.value(transitionState).pipe(M.when('EnterStart', () => [
|
|
24
|
+
h.DataAttribute('closed', ''),
|
|
25
|
+
h.DataAttribute('enter', ''),
|
|
26
|
+
h.DataAttribute('transition', ''),
|
|
27
|
+
]), M.when('EnterAnimating', () => [
|
|
28
|
+
h.DataAttribute('enter', ''),
|
|
29
|
+
h.DataAttribute('transition', ''),
|
|
30
|
+
]), M.when('LeaveStart', () => [
|
|
31
|
+
h.DataAttribute('leave', ''),
|
|
32
|
+
h.DataAttribute('transition', ''),
|
|
33
|
+
]), M.when('LeaveAnimating', () => [
|
|
34
|
+
h.DataAttribute('closed', ''),
|
|
35
|
+
h.DataAttribute('leave', ''),
|
|
36
|
+
h.DataAttribute('transition', ''),
|
|
37
|
+
]), M.orElse(() => []));
|
|
38
|
+
if (animateSize) {
|
|
39
|
+
const isClosed = transitionState === 'EnterStart' ||
|
|
40
|
+
transitionState === 'LeaveAnimating' ||
|
|
41
|
+
!isVisible;
|
|
42
|
+
return h.div([
|
|
43
|
+
h.Style({
|
|
44
|
+
display: 'grid',
|
|
45
|
+
gridTemplateRows: isClosed ? '0fr' : '1fr',
|
|
46
|
+
transition: 'grid-template-rows 200ms ease-out',
|
|
47
|
+
overflow: 'hidden',
|
|
48
|
+
}),
|
|
49
|
+
], [
|
|
50
|
+
h.div([
|
|
51
|
+
h.Style({ minHeight: '0px', overflow: 'hidden' }),
|
|
52
|
+
...(!isVisible ? [h.AriaHidden(true)] : []),
|
|
53
|
+
], [
|
|
54
|
+
h.keyed(element)(id, [
|
|
55
|
+
h.Id(id),
|
|
56
|
+
...(isClosed && transitionState === 'Idle'
|
|
57
|
+
? [h.DataAttribute('closed', '')]
|
|
58
|
+
: []),
|
|
59
|
+
...transitionAttributes,
|
|
60
|
+
...(className ? [h.Class(className)] : []),
|
|
61
|
+
...attributes,
|
|
62
|
+
], [content]),
|
|
63
|
+
]),
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
if (!isVisible) {
|
|
67
|
+
return h.empty;
|
|
68
|
+
}
|
|
69
|
+
return h.keyed(element)(id, [
|
|
70
|
+
h.Id(id),
|
|
71
|
+
...transitionAttributes,
|
|
72
|
+
...(className ? [h.Class(className)] : []),
|
|
73
|
+
...attributes,
|
|
74
|
+
], [content]);
|
|
75
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { init, update, view, defaultLeaveCommand, Model, Message, OutMessage, Showed, Hid, AdvancedAnimationFrame, EndedAnimation, StartedLeaveAnimating, TransitionedOut, TransitionState, RequestFrame, WaitForAnimationSettled, } from './index.js';
|
|
2
|
+
export type { InitConfig, ViewInputs } from './index.js';
|
|
3
|
+
//# sourceMappingURL=public.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public.d.ts","sourceRoot":"","sources":["../../src/animation/public.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,IAAI,EACJ,MAAM,EACN,IAAI,EACJ,mBAAmB,EACnB,KAAK,EACL,OAAO,EACP,UAAU,EACV,MAAM,EACN,GAAG,EACH,sBAAsB,EACtB,cAAc,EACd,qBAAqB,EACrB,eAAe,EACf,eAAe,EACf,YAAY,EACZ,uBAAuB,GACxB,MAAM,YAAY,CAAA;AAEnB,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { init, update, view, defaultLeaveCommand, Model, Message, OutMessage, Showed, Hid, AdvancedAnimationFrame, EndedAnimation, StartedLeaveAnimating, TransitionedOut, TransitionState, RequestFrame, WaitForAnimationSettled, } from './index.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { Schema as S } from 'effect';
|
|
2
|
+
/** Schema for the animation lifecycle state, tracking enter/leave phases. */
|
|
3
|
+
export declare const TransitionState: S.Literals<readonly ["Idle", "EnterStart", "EnterAnimating", "LeaveStart", "LeaveAnimating"]>;
|
|
4
|
+
export type TransitionState = typeof TransitionState.Type;
|
|
5
|
+
/** Schema for the animation component's state, tracking its unique ID, visibility intent, and lifecycle phase. */
|
|
6
|
+
export declare const Model: S.Struct<{
|
|
7
|
+
readonly id: S.String;
|
|
8
|
+
readonly isShowing: S.Boolean;
|
|
9
|
+
readonly transitionState: S.Literals<readonly ["Idle", "EnterStart", "EnterAnimating", "LeaveStart", "LeaveAnimating"]>;
|
|
10
|
+
}>;
|
|
11
|
+
export type Model = typeof Model.Type;
|
|
12
|
+
/** Sent when the animation should enter (become visible). Starts the enter sequence. */
|
|
13
|
+
export declare const Showed: import("foldkit/schema").CallableTaggedStruct<"Showed", {}>;
|
|
14
|
+
/** Sent when the animation should leave (become hidden). Starts the leave sequence. */
|
|
15
|
+
export declare const Hid: import("foldkit/schema").CallableTaggedStruct<"Hid", {}>;
|
|
16
|
+
/** Sent internally when a double-rAF completes, advancing the lifecycle to its animating phase. */
|
|
17
|
+
export declare const AdvancedAnimationFrame: import("foldkit/schema").CallableTaggedStruct<"AdvancedAnimationFrame", {}>;
|
|
18
|
+
/** Sent internally when all CSS animations on the element have settled. Covers both CSS transitions and CSS keyframe animations. */
|
|
19
|
+
export declare const EndedAnimation: import("foldkit/schema").CallableTaggedStruct<"EndedAnimation", {}>;
|
|
20
|
+
/** Union of all messages the animation component can produce. */
|
|
21
|
+
export declare const Message: S.Union<[
|
|
22
|
+
typeof Showed,
|
|
23
|
+
typeof Hid,
|
|
24
|
+
typeof AdvancedAnimationFrame,
|
|
25
|
+
typeof EndedAnimation
|
|
26
|
+
]>;
|
|
27
|
+
export type Message = typeof Message.Type;
|
|
28
|
+
export type Showed = typeof Showed.Type;
|
|
29
|
+
export type Hid = typeof Hid.Type;
|
|
30
|
+
/** Sent to the parent when the leave sequence advances to LeaveAnimating. The parent is responsible for providing the command that detects when the leave animation completes (e.g. WaitForAnimationSettled or a racing command). Use `defaultLeaveCommand` for the standard behavior. */
|
|
31
|
+
export declare const StartedLeaveAnimating: import("foldkit/schema").CallableTaggedStruct<"StartedLeaveAnimating", {}>;
|
|
32
|
+
/** Sent to the parent when the leave animation completes. The parent can use this to unmount content or update its own state. */
|
|
33
|
+
export declare const TransitionedOut: import("foldkit/schema").CallableTaggedStruct<"TransitionedOut", {}>;
|
|
34
|
+
export declare const OutMessage: S.Union<readonly [import("foldkit/schema").CallableTaggedStruct<"StartedLeaveAnimating", {}>, import("foldkit/schema").CallableTaggedStruct<"TransitionedOut", {}>]>;
|
|
35
|
+
export type OutMessage = typeof OutMessage.Type;
|
|
36
|
+
/** Configuration for creating an animation model with `init`. */
|
|
37
|
+
export type InitConfig = Readonly<{
|
|
38
|
+
id: string;
|
|
39
|
+
isShowing?: boolean;
|
|
40
|
+
}>;
|
|
41
|
+
/** Creates an initial animation model from a config. Defaults to hidden. */
|
|
42
|
+
export declare const init: (config: InitConfig) => Model;
|
|
43
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/animation/schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAKpC,6EAA6E;AAC7E,eAAO,MAAM,eAAe,+FAM1B,CAAA;AACF,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAC,IAAI,CAAA;AAIzD,kHAAkH;AAClH,eAAO,MAAM,KAAK;;;;EAIhB,CAAA;AAEF,MAAM,MAAM,KAAK,GAAG,OAAO,KAAK,CAAC,IAAI,CAAA;AAIrC,wFAAwF;AACxF,eAAO,MAAM,MAAM,6DAAc,CAAA;AACjC,uFAAuF;AACvF,eAAO,MAAM,GAAG,0DAAW,CAAA;AAC3B,mGAAmG;AACnG,eAAO,MAAM,sBAAsB,6EAA8B,CAAA;AACjE,oIAAoI;AACpI,eAAO,MAAM,cAAc,qEAAsB,CAAA;AAEjD,iEAAiE;AACjE,eAAO,MAAM,OAAO,EAAE,CAAC,CAAC,KAAK,CAC3B;IACE,OAAO,MAAM;IACb,OAAO,GAAG;IACV,OAAO,sBAAsB;IAC7B,OAAO,cAAc;CACtB,CAC+D,CAAA;AAClE,MAAM,MAAM,OAAO,GAAG,OAAO,OAAO,CAAC,IAAI,CAAA;AAEzC,MAAM,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,IAAI,CAAA;AACvC,MAAM,MAAM,GAAG,GAAG,OAAO,GAAG,CAAC,IAAI,CAAA;AAIjC,0RAA0R;AAC1R,eAAO,MAAM,qBAAqB,4EAA6B,CAAA;AAC/D,iIAAiI;AACjI,eAAO,MAAM,eAAe,sEAAuB,CAAA;AAEnD,eAAO,MAAM,UAAU,sKAAoD,CAAA;AAC3E,MAAM,MAAM,UAAU,GAAG,OAAO,UAAU,CAAC,IAAI,CAAA;AAI/C,iEAAiE;AACjE,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAC,CAAA;AAEF,4EAA4E;AAC5E,eAAO,MAAM,IAAI,GAAI,QAAQ,UAAU,KAAG,KAIxC,CAAA"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Schema as S } from 'effect';
|
|
2
|
+
import { m } from 'foldkit/message';
|
|
3
|
+
// TRANSITION STATE
|
|
4
|
+
/** Schema for the animation lifecycle state, tracking enter/leave phases. */
|
|
5
|
+
export const TransitionState = S.Literals([
|
|
6
|
+
'Idle',
|
|
7
|
+
'EnterStart',
|
|
8
|
+
'EnterAnimating',
|
|
9
|
+
'LeaveStart',
|
|
10
|
+
'LeaveAnimating',
|
|
11
|
+
]);
|
|
12
|
+
// MODEL
|
|
13
|
+
/** Schema for the animation component's state, tracking its unique ID, visibility intent, and lifecycle phase. */
|
|
14
|
+
export const Model = S.Struct({
|
|
15
|
+
id: S.String,
|
|
16
|
+
isShowing: S.Boolean,
|
|
17
|
+
transitionState: TransitionState,
|
|
18
|
+
});
|
|
19
|
+
// MESSAGE
|
|
20
|
+
/** Sent when the animation should enter (become visible). Starts the enter sequence. */
|
|
21
|
+
export const Showed = m('Showed');
|
|
22
|
+
/** Sent when the animation should leave (become hidden). Starts the leave sequence. */
|
|
23
|
+
export const Hid = m('Hid');
|
|
24
|
+
/** Sent internally when a double-rAF completes, advancing the lifecycle to its animating phase. */
|
|
25
|
+
export const AdvancedAnimationFrame = m('AdvancedAnimationFrame');
|
|
26
|
+
/** Sent internally when all CSS animations on the element have settled. Covers both CSS transitions and CSS keyframe animations. */
|
|
27
|
+
export const EndedAnimation = m('EndedAnimation');
|
|
28
|
+
/** Union of all messages the animation component can produce. */
|
|
29
|
+
export const Message = S.Union([Showed, Hid, AdvancedAnimationFrame, EndedAnimation]);
|
|
30
|
+
// OUT MESSAGE
|
|
31
|
+
/** Sent to the parent when the leave sequence advances to LeaveAnimating. The parent is responsible for providing the command that detects when the leave animation completes (e.g. WaitForAnimationSettled or a racing command). Use `defaultLeaveCommand` for the standard behavior. */
|
|
32
|
+
export const StartedLeaveAnimating = m('StartedLeaveAnimating');
|
|
33
|
+
/** Sent to the parent when the leave animation completes. The parent can use this to unmount content or update its own state. */
|
|
34
|
+
export const TransitionedOut = m('TransitionedOut');
|
|
35
|
+
export const OutMessage = S.Union([StartedLeaveAnimating, TransitionedOut]);
|
|
36
|
+
/** Creates an initial animation model from a config. Defaults to hidden. */
|
|
37
|
+
export const init = (config) => ({
|
|
38
|
+
id: config.id,
|
|
39
|
+
isShowing: config.isShowing ?? false,
|
|
40
|
+
transitionState: 'Idle',
|
|
41
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Effect, Option, Schema as S } from 'effect';
|
|
2
|
+
import * as Command from 'foldkit/command';
|
|
3
|
+
import { type Message, type Model, type OutMessage } from './schema.js';
|
|
4
|
+
type UpdateReturn = readonly [
|
|
5
|
+
Model,
|
|
6
|
+
ReadonlyArray<Command.Command<Message>>,
|
|
7
|
+
Option.Option<OutMessage>
|
|
8
|
+
];
|
|
9
|
+
/** Advances the enter/leave lifecycle by waiting a double-rAF. */
|
|
10
|
+
export declare const RequestFrame: Command.CommandDefinitionNoArgs<"RequestFrame", Effect.Effect<{
|
|
11
|
+
readonly _tag: "AdvancedAnimationFrame";
|
|
12
|
+
}, never, never>>;
|
|
13
|
+
/** Waits for all CSS animations on the element to settle. Covers both CSS transitions and CSS keyframe animations. */
|
|
14
|
+
export declare const WaitForAnimationSettled: Command.CommandDefinitionWithArgs<"WaitForAnimationSettled", {
|
|
15
|
+
id: S.String;
|
|
16
|
+
}, Effect.Effect<{
|
|
17
|
+
readonly _tag: "EndedAnimation";
|
|
18
|
+
}, never, never>>;
|
|
19
|
+
/** Processes an animation message and returns the next model, commands, and optional OutMessage. */
|
|
20
|
+
export declare const update: (model: Model, message: Message) => UpdateReturn;
|
|
21
|
+
/** Creates the standard leave-phase command that waits for CSS animations on the element to settle. Use this when handling the `StartedLeaveAnimating` OutMessage for components that don't need custom leave behavior. */
|
|
22
|
+
export declare const defaultLeaveCommand: (model: Model) => Command.Command<Message>;
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/animation/update.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAc,MAAM,EAAE,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAChE,OAAO,KAAK,OAAO,MAAM,iBAAiB,CAAA;AAK1C,OAAO,EAGL,KAAK,OAAO,EACZ,KAAK,KAAK,EACV,KAAK,UAAU,EAGhB,MAAM,aAAa,CAAA;AAMpB,KAAK,YAAY,GAAG,SAAS;IAC3B,KAAK;IACL,aAAa,CAAC,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACvC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC;CAC1B,CAAA;AAGD,kEAAkE;AAClE,eAAO,MAAM,YAAY;;iBAGqC,CAAA;AAC9D,sHAAsH;AACtH,eAAO,MAAM,uBAAuB;;;;iBAQnC,CAAA;AAED,oGAAoG;AACpG,eAAO,MAAM,MAAM,GAAI,OAAO,KAAK,EAAE,SAAS,OAAO,KAAG,YAyEvD,CAAA;AAED,2NAA2N;AAC3N,eAAO,MAAM,mBAAmB,GAAI,OAAO,KAAK,KAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAC/B,CAAA"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Effect, Match as M, Option, Schema as S } from 'effect';
|
|
2
|
+
import * as Command from 'foldkit/command';
|
|
3
|
+
import * as Dom from 'foldkit/dom';
|
|
4
|
+
import * as Render from 'foldkit/render';
|
|
5
|
+
import { evo } from 'foldkit/struct';
|
|
6
|
+
import { AdvancedAnimationFrame, EndedAnimation, StartedLeaveAnimating, TransitionedOut, } from './schema.js';
|
|
7
|
+
// UPDATE
|
|
8
|
+
const elementSelector = (id) => `#${id}`;
|
|
9
|
+
const withUpdateReturn = M.withReturnType();
|
|
10
|
+
/** Advances the enter/leave lifecycle by waiting a double-rAF. */
|
|
11
|
+
export const RequestFrame = Command.define('RequestFrame', AdvancedAnimationFrame)(Render.afterPaint.pipe(Effect.as(AdvancedAnimationFrame())));
|
|
12
|
+
/** Waits for all CSS animations on the element to settle. Covers both CSS transitions and CSS keyframe animations. */
|
|
13
|
+
export const WaitForAnimationSettled = Command.define('WaitForAnimationSettled', { id: S.String }, EndedAnimation)(({ id }) => Dom.waitForAnimationSettled(elementSelector(id)).pipe(Effect.as(EndedAnimation())));
|
|
14
|
+
/** Processes an animation message and returns the next model, commands, and optional OutMessage. */
|
|
15
|
+
export const update = (model, message) => {
|
|
16
|
+
const maybeNextFrame = RequestFrame();
|
|
17
|
+
return M.value(message).pipe(withUpdateReturn, M.tagsExhaustive({
|
|
18
|
+
Showed: () => {
|
|
19
|
+
if (model.isShowing) {
|
|
20
|
+
return [model, [], Option.none()];
|
|
21
|
+
}
|
|
22
|
+
return [
|
|
23
|
+
evo(model, {
|
|
24
|
+
isShowing: () => true,
|
|
25
|
+
transitionState: () => 'EnterStart',
|
|
26
|
+
}),
|
|
27
|
+
[maybeNextFrame],
|
|
28
|
+
Option.none(),
|
|
29
|
+
];
|
|
30
|
+
},
|
|
31
|
+
Hid: () => {
|
|
32
|
+
const isLeaving = model.transitionState === 'LeaveStart' ||
|
|
33
|
+
model.transitionState === 'LeaveAnimating';
|
|
34
|
+
if (isLeaving || !model.isShowing) {
|
|
35
|
+
return [model, [], Option.none()];
|
|
36
|
+
}
|
|
37
|
+
return [
|
|
38
|
+
evo(model, {
|
|
39
|
+
isShowing: () => false,
|
|
40
|
+
transitionState: () => 'LeaveStart',
|
|
41
|
+
}),
|
|
42
|
+
[maybeNextFrame],
|
|
43
|
+
Option.none(),
|
|
44
|
+
];
|
|
45
|
+
},
|
|
46
|
+
AdvancedAnimationFrame: () => M.value(model.transitionState).pipe(withUpdateReturn, M.when('EnterStart', () => [
|
|
47
|
+
evo(model, { transitionState: () => 'EnterAnimating' }),
|
|
48
|
+
[WaitForAnimationSettled({ id: model.id })],
|
|
49
|
+
Option.none(),
|
|
50
|
+
]), M.when('LeaveStart', () => [
|
|
51
|
+
evo(model, { transitionState: () => 'LeaveAnimating' }),
|
|
52
|
+
[],
|
|
53
|
+
Option.some(StartedLeaveAnimating()),
|
|
54
|
+
]), M.orElse(() => [model, [], Option.none()])),
|
|
55
|
+
EndedAnimation: () => M.value(model.transitionState).pipe(withUpdateReturn, M.when('EnterAnimating', () => [
|
|
56
|
+
evo(model, { transitionState: () => 'Idle' }),
|
|
57
|
+
[],
|
|
58
|
+
Option.none(),
|
|
59
|
+
]), M.when('LeaveAnimating', () => [
|
|
60
|
+
evo(model, { transitionState: () => 'Idle' }),
|
|
61
|
+
[],
|
|
62
|
+
Option.some(TransitionedOut()),
|
|
63
|
+
]), M.orElse(() => [model, [], Option.none()])),
|
|
64
|
+
}));
|
|
65
|
+
};
|
|
66
|
+
/** Creates the standard leave-phase command that waits for CSS animations on the element to settle. Use this when handling the `StartedLeaveAnimating` OutMessage for components that don't need custom leave behavior. */
|
|
67
|
+
export const defaultLeaveCommand = (model) => WaitForAnimationSettled({ id: model.id });
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Attribute } from 'foldkit/html';
|
|
2
|
+
import type { Html } from 'foldkit/html';
|
|
3
|
+
/** Attribute groups the button component provides to the consumer's `toView` callback. */
|
|
4
|
+
export type ButtonAttributes<ParentMessage> = Readonly<{
|
|
5
|
+
button: ReadonlyArray<Attribute<ParentMessage>>;
|
|
6
|
+
}>;
|
|
7
|
+
/** Configuration for rendering a button with `view`. */
|
|
8
|
+
export type ViewConfig<ParentMessage> = Readonly<{
|
|
9
|
+
toView: (attributes: ButtonAttributes<ParentMessage>) => Html;
|
|
10
|
+
onClick?: ParentMessage;
|
|
11
|
+
isDisabled?: boolean;
|
|
12
|
+
type?: 'button' | 'submit' | 'reset';
|
|
13
|
+
isAutofocus?: boolean;
|
|
14
|
+
}>;
|
|
15
|
+
/** Renders an accessible button by building attribute groups and delegating layout to the consumer's `toView` callback. */
|
|
16
|
+
export declare const view: <ParentMessage>(config: ViewConfig<ParentMessage>) => Html;
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/button/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAE7C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAA;AAIxC,0FAA0F;AAC1F,MAAM,MAAM,gBAAgB,CAAC,aAAa,IAAI,QAAQ,CAAC;IACrD,MAAM,EAAE,aAAa,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAA;CAChD,CAAC,CAAA;AAEF,wDAAwD;AACxD,MAAM,MAAM,UAAU,CAAC,aAAa,IAAI,QAAQ,CAAC;IAC/C,MAAM,EAAE,CAAC,UAAU,EAAE,gBAAgB,CAAC,aAAa,CAAC,KAAK,IAAI,CAAA;IAC7D,OAAO,CAAC,EAAE,aAAa,CAAA;IACvB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAA;IACpC,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB,CAAC,CAAA;AAEF,2HAA2H;AAC3H,eAAO,MAAM,IAAI,GAAI,aAAa,EAChC,QAAQ,UAAU,CAAC,aAAa,CAAC,KAChC,IA+BF,CAAA"}
|