@fishies/ui 0.8.3
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 +26 -0
- package/README.md +60 -0
- package/dist/FishiesImg.d.ts +9 -0
- package/dist/FishiesImg.js +64 -0
- package/dist/Select.d.ts +22 -0
- package/dist/Select.js +95 -0
- package/dist/fx-image.d.ts +3 -0
- package/dist/fx-image.js +25 -0
- package/dist/icons.d.ts +28 -0
- package/dist/icons.generated.d.ts +5 -0
- package/dist/icons.generated.js +658 -0
- package/dist/icons.js +53 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/kit.css +267 -0
- package/dist/kit.d.ts +159 -0
- package/dist/kit.js +104 -0
- package/dist/loader.css +48 -0
- package/dist/loader.d.ts +17 -0
- package/dist/loader.js +27 -0
- package/dist/select-state.d.ts +34 -0
- package/dist/select-state.js +55 -0
- package/dist/select.css +87 -0
- package/dist/tokens.d.ts +17 -0
- package/dist/tokens.generated.css +97 -0
- package/dist/tokens.js +175 -0
- package/handbook/README.md +236 -0
- package/icons/add.png +0 -0
- package/icons/announce.png +0 -0
- package/icons/arrange.png +0 -0
- package/icons/bell.png +0 -0
- package/icons/block.png +0 -0
- package/icons/book.png +0 -0
- package/icons/bug.png +0 -0
- package/icons/camera.png +0 -0
- package/icons/cart-add.png +0 -0
- package/icons/cart.png +0 -0
- package/icons/chat.png +0 -0
- package/icons/check.png +0 -0
- package/icons/clean.png +0 -0
- package/icons/clock.png +0 -0
- package/icons/coin.png +0 -0
- package/icons/coral.png +0 -0
- package/icons/decorate.png +0 -0
- package/icons/download.png +0 -0
- package/icons/drop.png +0 -0
- package/icons/edit.png +0 -0
- package/icons/egg.png +0 -0
- package/icons/emote-food.png +0 -0
- package/icons/emote-heart.png +0 -0
- package/icons/emote-hello.png +0 -0
- package/icons/emote-music.png +0 -0
- package/icons/emote-question.png +0 -0
- package/icons/emote-sleep.png +0 -0
- package/icons/emote-sparkle.png +0 -0
- package/icons/emote-surprise.png +0 -0
- package/icons/exit.png +0 -0
- package/icons/explore.png +0 -0
- package/icons/external.png +0 -0
- package/icons/feed.png +0 -0
- package/icons/fish.png +0 -0
- package/icons/follow.png +0 -0
- package/icons/fullscreen.png +0 -0
- package/icons/gear.png +0 -0
- package/icons/gem.png +0 -0
- package/icons/gift.png +0 -0
- package/icons/globe.png +0 -0
- package/icons/growth.png +0 -0
- package/icons/heart.png +0 -0
- package/icons/help.png +0 -0
- package/icons/home.png +0 -0
- package/icons/idea.png +0 -0
- package/icons/inspect.png +0 -0
- package/icons/inventory.png +0 -0
- package/icons/keeper.png +0 -0
- package/icons/kelp.png +0 -0
- package/icons/keyboard.png +0 -0
- package/icons/level.png +0 -0
- package/icons/link.png +0 -0
- package/icons/lock.png +0 -0
- package/icons/mail.png +0 -0
- package/icons/map.png +0 -0
- package/icons/menu.png +0 -0
- package/icons/music.png +0 -0
- package/icons/mute.png +0 -0
- package/icons/net.png +0 -0
- package/icons/people.png +0 -0
- package/icons/pin.png +0 -0
- package/icons/quest.png +0 -0
- package/icons/quote.png +0 -0
- package/icons/remedy.png +0 -0
- package/icons/reply.png +0 -0
- package/icons/report.png +0 -0
- package/icons/search.png +0 -0
- package/icons/sell.png +0 -0
- package/icons/send.png +0 -0
- package/icons/share.png +0 -0
- package/icons/shell.png +0 -0
- package/icons/shop.png +0 -0
- package/icons/showcase.png +0 -0
- package/icons/smile.png +0 -0
- package/icons/solved.png +0 -0
- package/icons/sound.png +0 -0
- package/icons/staff.png +0 -0
- package/icons/star.png +0 -0
- package/icons/tank.png +0 -0
- package/icons/thermometer.png +0 -0
- package/icons/trash.png +0 -0
- package/icons/trophy.png +0 -0
- package/icons/updates.png +0 -0
- package/icons/upvote.png +0 -0
- package/icons/views.png +0 -0
- package/icons/warning.png +0 -0
- package/icons/wave.png +0 -0
- package/icons/whisper.png +0 -0
- package/package.json +66 -0
- package/src/FishiesImg.tsx +58 -0
- package/src/Select.tsx +119 -0
- package/src/css.d.ts +2 -0
- package/src/fx-image.ts +17 -0
- package/src/icons.generated.ts +658 -0
- package/src/icons.ts +74 -0
- package/src/index.ts +8 -0
- package/src/kit.css +267 -0
- package/src/kit.tsx +190 -0
- package/src/loader.css +48 -0
- package/src/loader.tsx +35 -0
- package/src/select-state.ts +59 -0
- package/src/select.css +87 -0
- package/src/tokens.generated.css +97 -0
- package/src/tokens.ts +196 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
The code in this package (everything under src/ and dist/) is under the MIT License:
|
|
2
|
+
|
|
3
|
+
MIT License
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026 Fishies
|
|
6
|
+
|
|
7
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
in the Software without restriction, including without limitation the rights
|
|
10
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
furnished to do so, subject to the following conditions:
|
|
13
|
+
|
|
14
|
+
The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
copies or substantial portions of the Software.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
SOFTWARE.
|
|
24
|
+
|
|
25
|
+
The icons (everything under icons/) and the Fishies name and marks are
|
|
26
|
+
© 2026 Fishies. All rights reserved.
|
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://fishies.lol/ui"><img src="https://fishies.lol/ui/banner.webp" alt="Fishies UI: plump buttons, water glass with a gold rim and a painted icon set" width="100%"></a>
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/@fishies/ui"><img src="https://img.shields.io/npm/v/@fishies/ui?style=flat-square&color=f4c656&labelColor=0b2233&label=%40fishies%2Fui" alt="npm version"></a>
|
|
7
|
+
<a href="https://www.npmjs.com/package/@fishies/ui"><img src="https://img.shields.io/npm/dm/@fishies/ui?style=flat-square&color=5cd8c6&labelColor=0b2233&label=downloads" alt="npm downloads"></a>
|
|
8
|
+
<a href="https://bundlephobia.com/package/@fishies/ui"><img src="https://img.shields.io/bundlephobia/minzip/@fishies/ui?style=flat-square&color=83cfbd&labelColor=0b2233&label=min%2Bgzip" alt="bundle size"></a>
|
|
9
|
+
<img src="https://img.shields.io/badge/react-18%2B-83cfbd?style=flat-square&labelColor=0b2233" alt="React 18 or newer">
|
|
10
|
+
<img src="https://img.shields.io/badge/types-included-83cfbd?style=flat-square&labelColor=0b2233" alt="TypeScript types included">
|
|
11
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/code-MIT-f4c656?style=flat-square&labelColor=0b2233" alt="code MIT"></a>
|
|
12
|
+
<a href="https://fishies.lol/ui"><img src="https://img.shields.io/badge/showcase-fishies.lol%2Fui-f4c656?style=flat-square&labelColor=0b2233" alt="live showcase"></a>
|
|
13
|
+
</p>
|
|
14
|
+
|
|
15
|
+
# @fishies/ui
|
|
16
|
+
|
|
17
|
+
The kit the social aquarium game [Fishies](https://fishies.lol) is drawn with: reef-toy React
|
|
18
|
+
components, design tokens as CSS variables and as data, and the painted system icon set. See every
|
|
19
|
+
piece live, with the Fishies Handbook, at **https://fishies.lol/ui**.
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
bun add @fishies/ui
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
<sub>npm i @fishies/ui · pnpm add @fishies/ui · yarn add @fishies/ui</sub>
|
|
26
|
+
|
|
27
|
+
## Use it
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import '@fishies/ui/tokens.css' // the --fx-* custom properties
|
|
31
|
+
import '@fishies/ui/kit.css' // the components
|
|
32
|
+
import { Button, Panel, configureIcons } from '@fishies/ui'
|
|
33
|
+
|
|
34
|
+
// The icons ship in this package under icons/. Serve that folder, or point at a CDN copy:
|
|
35
|
+
configureIcons({ url: icon => `https://cdn.jsdelivr.net/npm/@fishies/ui@0.8.3/icons/${icon.name}.png` })
|
|
36
|
+
|
|
37
|
+
export function Shop() {
|
|
38
|
+
return <Panel title="Shop" icon="shop">
|
|
39
|
+
<Button tone="gold" icon="coin">Buy for 120</Button>
|
|
40
|
+
</Panel>
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
React 18 or newer is a peer dependency. Each component is its own module, so a bundler keeps only
|
|
45
|
+
what you import. `Select` and the loader carry their own stylesheets (`@fishies/ui/select.css`,
|
|
46
|
+
`@fishies/ui/loader.css`).
|
|
47
|
+
|
|
48
|
+
The motion tokens (`--m-quick`, `--m-ease-spring` and the rest) are slots: set them, or load a
|
|
49
|
+
stylesheet that does, and every component animates with them; leave them out and nothing moves.
|
|
50
|
+
|
|
51
|
+
## What is in it
|
|
52
|
+
|
|
53
|
+
| Folder | Holds |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| `dist/` | ES modules with type declarations, and the stylesheets |
|
|
56
|
+
| `src/` | the same components and tokens as TypeScript source |
|
|
57
|
+
| `icons/` | the painted icon set, one PNG per name, each twice its largest display size |
|
|
58
|
+
| `handbook/` | the Fishies Handbook as Markdown |
|
|
59
|
+
|
|
60
|
+
Version 0.8.3: the minor version follows the Fishies release it was taken from; a patch is a fix to the kit itself.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ImgHTMLAttributes, type Ref } from 'react';
|
|
2
|
+
import './loader.css';
|
|
3
|
+
export default function FishiesImg({ ref, decoding, ...props }: ImgHTMLAttributes<HTMLImageElement> & {
|
|
4
|
+
ref?: Ref<HTMLImageElement>;
|
|
5
|
+
}): import("react").JSX.Element;
|
|
6
|
+
export declare function useFxPoster(video: {
|
|
7
|
+
current: HTMLVideoElement | null;
|
|
8
|
+
}, poster: string | null | undefined): void;
|
|
9
|
+
export declare function useDecodedUrl(url: string | null | undefined): string | null;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
4
|
+
import { reveal } from './fx-image.js';
|
|
5
|
+
import './loader.css';
|
|
6
|
+
const useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect;
|
|
7
|
+
export default function FishiesImg({ ref, decoding, ...props }) {
|
|
8
|
+
const own = useRef(null);
|
|
9
|
+
const mounted = useRef(false);
|
|
10
|
+
const setRef = useCallback((node) => {
|
|
11
|
+
own.current = node;
|
|
12
|
+
if (typeof ref === 'function')
|
|
13
|
+
ref(node);
|
|
14
|
+
else if (ref)
|
|
15
|
+
ref.current = node;
|
|
16
|
+
}, [ref]);
|
|
17
|
+
useIsomorphicLayoutEffect(() => {
|
|
18
|
+
const img = own.current;
|
|
19
|
+
if (!img)
|
|
20
|
+
return;
|
|
21
|
+
if (mounted.current)
|
|
22
|
+
img.setAttribute('data-fx', 'wait');
|
|
23
|
+
const first = !mounted.current;
|
|
24
|
+
mounted.current = true;
|
|
25
|
+
reveal(img, first && img.complete);
|
|
26
|
+
const settle = () => reveal(img);
|
|
27
|
+
img.addEventListener('load', settle);
|
|
28
|
+
img.addEventListener('error', settle);
|
|
29
|
+
return () => { img.removeEventListener('load', settle); img.removeEventListener('error', settle); };
|
|
30
|
+
}, [props.src, props.srcSet]);
|
|
31
|
+
return _jsx("img", { ...props, ref: setRef, decoding: decoding ?? 'async', "data-fx": "wait", suppressHydrationWarning: true });
|
|
32
|
+
}
|
|
33
|
+
export function useFxPoster(video, poster) {
|
|
34
|
+
const decoded = useDecodedUrl(poster);
|
|
35
|
+
useIsomorphicLayoutEffect(() => {
|
|
36
|
+
const element = video.current;
|
|
37
|
+
if (!element || !poster)
|
|
38
|
+
return;
|
|
39
|
+
const show = () => { if (element.getAttribute('data-fx') === 'wait')
|
|
40
|
+
element.setAttribute('data-fx', 'fade'); };
|
|
41
|
+
if (decoded || element.readyState >= 2) {
|
|
42
|
+
show();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
element.addEventListener('loadeddata', show);
|
|
46
|
+
return () => element.removeEventListener('loadeddata', show);
|
|
47
|
+
}, [video, poster, decoded]);
|
|
48
|
+
}
|
|
49
|
+
export function useDecodedUrl(url) {
|
|
50
|
+
const [ready, setReady] = useState(null);
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!url)
|
|
53
|
+
return;
|
|
54
|
+
let live = true;
|
|
55
|
+
const probe = new Image();
|
|
56
|
+
probe.decoding = 'async';
|
|
57
|
+
probe.src = url;
|
|
58
|
+
const done = () => { if (live)
|
|
59
|
+
setReady(url); };
|
|
60
|
+
(typeof probe.decode === 'function' ? probe.decode() : new Promise(resolve => { probe.onload = resolve; })).then(done, done);
|
|
61
|
+
return () => { live = false; };
|
|
62
|
+
}, [url]);
|
|
63
|
+
return url && ready === url ? url : null;
|
|
64
|
+
}
|
package/dist/Select.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import './select.css';
|
|
3
|
+
export type SelectOption = {
|
|
4
|
+
value: string;
|
|
5
|
+
label: string;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
icon?: ReactNode;
|
|
8
|
+
};
|
|
9
|
+
type Props = {
|
|
10
|
+
value: string;
|
|
11
|
+
onChange: (value: string) => void;
|
|
12
|
+
options: readonly SelectOption[];
|
|
13
|
+
'aria-label'?: string;
|
|
14
|
+
'aria-labelledby'?: string;
|
|
15
|
+
disabled?: boolean;
|
|
16
|
+
name?: string;
|
|
17
|
+
className?: string;
|
|
18
|
+
size?: 'regular' | 'compact';
|
|
19
|
+
autoFocus?: boolean;
|
|
20
|
+
};
|
|
21
|
+
export default function Select({ value, onChange, options, disabled, name, className, size, autoFocus, ...aria }: Props): import("react").JSX.Element;
|
|
22
|
+
export {};
|
package/dist/Select.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { SystemIcon } from './kit.js';
|
|
4
|
+
import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState } from 'react';
|
|
5
|
+
import { createPortal } from 'react-dom';
|
|
6
|
+
import { placePopup, selectedIndex, selectKey } from './select-state.js';
|
|
7
|
+
import './select.css';
|
|
8
|
+
const TOKENS = ['--fs-select-ink', '--fs-select-muted', '--fs-select-line', '--fs-select-panel', '--fs-select-hover', '--fs-select-accent', '--fs-select-radius', '--fs-select-shadow', '--fs-select-font'];
|
|
9
|
+
export default function Select({ value, onChange, options, disabled, name, className, size = 'regular', autoFocus, ...aria }) {
|
|
10
|
+
const id = useId();
|
|
11
|
+
const trigger = useRef(null), list = useRef(null);
|
|
12
|
+
const [open, setOpen] = useState(false);
|
|
13
|
+
const [active, setActive] = useState(-1);
|
|
14
|
+
const [style, setStyle] = useState({});
|
|
15
|
+
const [up, setUp] = useState(false);
|
|
16
|
+
const current = options.find(option => option.value === value);
|
|
17
|
+
const place = useCallback(() => {
|
|
18
|
+
const button = trigger.current;
|
|
19
|
+
if (!button)
|
|
20
|
+
return;
|
|
21
|
+
const rect = button.getBoundingClientRect();
|
|
22
|
+
const spot = placePopup(rect, { width: window.innerWidth, height: window.innerHeight }, 320);
|
|
23
|
+
const computed = getComputedStyle(button);
|
|
24
|
+
const tokens = {};
|
|
25
|
+
for (const token of TOKENS) {
|
|
26
|
+
const resolved = computed.getPropertyValue(token).trim();
|
|
27
|
+
if (resolved)
|
|
28
|
+
tokens[token] = resolved;
|
|
29
|
+
}
|
|
30
|
+
setUp(spot.up);
|
|
31
|
+
setStyle({ ...tokens, fontFamily: computed.fontFamily, left: spot.left, minWidth: spot.width, maxWidth: Math.min(360, window.innerWidth - 16), top: spot.top, bottom: spot.bottom, maxHeight: spot.maxHeight });
|
|
32
|
+
}, []);
|
|
33
|
+
const show = (index) => { place(); setActive(index); setOpen(true); };
|
|
34
|
+
const close = (focus) => { setOpen(false); if (focus)
|
|
35
|
+
trigger.current?.focus(); };
|
|
36
|
+
const choose = (next) => { if (next !== value)
|
|
37
|
+
onChange(next); };
|
|
38
|
+
useLayoutEffect(() => { if (open)
|
|
39
|
+
place(); }, [open, place]);
|
|
40
|
+
useLayoutEffect(() => {
|
|
41
|
+
const popup = list.current;
|
|
42
|
+
if (!popup || typeof style.left !== 'number')
|
|
43
|
+
return;
|
|
44
|
+
const left = Math.max(8, Math.min(style.left, window.innerWidth - 8 - popup.offsetWidth));
|
|
45
|
+
if (left !== style.left)
|
|
46
|
+
setStyle(previous => ({ ...previous, left }));
|
|
47
|
+
}, [style]);
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
if (!open)
|
|
50
|
+
return;
|
|
51
|
+
const outside = (event) => {
|
|
52
|
+
const target = event.target;
|
|
53
|
+
if (!trigger.current?.contains(target) && !list.current?.contains(target))
|
|
54
|
+
setOpen(false);
|
|
55
|
+
};
|
|
56
|
+
window.addEventListener('pointerdown', outside, true);
|
|
57
|
+
window.addEventListener('resize', place);
|
|
58
|
+
const scrolled = (event) => { if (!list.current?.contains(event.target))
|
|
59
|
+
place(); };
|
|
60
|
+
window.addEventListener('scroll', scrolled, true);
|
|
61
|
+
return () => { window.removeEventListener('pointerdown', outside, true); window.removeEventListener('resize', place); window.removeEventListener('scroll', scrolled, true); };
|
|
62
|
+
}, [open, place]);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (open && active >= 0)
|
|
65
|
+
list.current?.querySelector(`[data-index="${active}"]`)?.scrollIntoView({ block: 'nearest' });
|
|
66
|
+
}, [open, active]);
|
|
67
|
+
useEffect(() => { if (disabled)
|
|
68
|
+
setOpen(false); }, [disabled]);
|
|
69
|
+
const keydown = (event) => {
|
|
70
|
+
if (event.altKey || event.ctrlKey || event.metaKey)
|
|
71
|
+
return;
|
|
72
|
+
const result = selectKey({ open, active }, event.key, options, value);
|
|
73
|
+
if (!result.handled && result.close !== 'blur')
|
|
74
|
+
return;
|
|
75
|
+
if (result.handled) {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
event.stopPropagation();
|
|
78
|
+
}
|
|
79
|
+
if (result.open && !open) {
|
|
80
|
+
show(result.active);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
setActive(result.active);
|
|
84
|
+
if (!result.open)
|
|
85
|
+
close(result.close === 'focus');
|
|
86
|
+
if (result.choose !== undefined)
|
|
87
|
+
choose(result.choose);
|
|
88
|
+
};
|
|
89
|
+
const labelId = `${id}-label`, valueId = `${id}-value`, listId = `${id}-list`;
|
|
90
|
+
const labelledby = aria['aria-labelledby'] ?? (aria['aria-label'] ? labelId : undefined);
|
|
91
|
+
const classes = ['fs-select', size === 'compact' ? 'fs-select-compact' : '', open ? 'is-open' : '', className ?? ''].filter(Boolean).join(' ');
|
|
92
|
+
return _jsxs(_Fragment, { children: [aria['aria-label'] && !aria['aria-labelledby'] && _jsx("span", { id: labelId, hidden: true, children: aria['aria-label'] }), _jsxs("button", { ref: trigger, type: "button", className: classes, disabled: disabled, autoFocus: autoFocus, "aria-haspopup": "listbox", "aria-expanded": open, "aria-controls": open ? listId : undefined, "aria-labelledby": labelledby ? `${labelledby} ${valueId}` : undefined, "aria-activedescendant": open && active >= 0 ? `${id}-option-${active}` : undefined, onClick: () => open ? close(false) : show(selectedIndex(options, value)), onKeyDown: keydown, children: [current?.icon && _jsx("span", { className: "fs-select-icon", "aria-hidden": "true", children: current.icon }), _jsx("span", { id: valueId, className: "fs-select-value", children: current?.label ?? '' }), _jsx("i", { "aria-hidden": "true", children: "\u25BE" })] }), name && _jsx("input", { type: "hidden", name: name, value: value }), open && typeof document !== 'undefined' && createPortal(_jsx("ul", { ref: list, id: listId, role: "listbox", "aria-labelledby": labelledby, tabIndex: -1, className: `fs-select-popover${up ? ' is-up' : ''}${size === 'compact' ? ' fs-select-compact' : ''}`, style: style, onPointerDown: event => event.preventDefault(), children: options.map((option, index) => _jsxs("li", { id: `${id}-option-${index}`, "data-index": index, role: "option", "aria-selected": option.value === value, "aria-disabled": option.disabled || undefined, className: index === active ? 'is-active' : undefined, onPointerEnter: () => { if (!option.disabled)
|
|
93
|
+
setActive(index); }, onClick: () => { if (option.disabled)
|
|
94
|
+
return; close(true); choose(option.value); }, children: [option.icon && _jsx("span", { className: "fs-select-icon", "aria-hidden": "true", children: option.icon }), _jsx("span", { children: option.label }), option.value === value && _jsx("b", { "aria-hidden": "true", children: _jsx(SystemIcon, { name: "check", size: 16 }) })] }, option.value)) }), document.body)] });
|
|
95
|
+
}
|
package/dist/fx-image.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function reveal(img, instant) {
|
|
2
|
+
if (img.getAttribute('data-fx') !== 'wait' || !img.complete)
|
|
3
|
+
return;
|
|
4
|
+
const src = img.currentSrc || img.src;
|
|
5
|
+
const set = (state) => {
|
|
6
|
+
if (img.getAttribute('data-fx') === 'wait' && (img.currentSrc || img.src) === src)
|
|
7
|
+
img.setAttribute('data-fx', state);
|
|
8
|
+
};
|
|
9
|
+
if (!img.naturalWidth) {
|
|
10
|
+
set('error');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (instant) {
|
|
14
|
+
set('ready');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (typeof img.decode !== 'function') {
|
|
18
|
+
set('fade');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
img.decode().then(() => set('fade'), () => set(img.naturalWidth ? 'fade' : 'error'));
|
|
22
|
+
}
|
|
23
|
+
export const FX_IMAGE_SCRIPT = `(function(){var r=${reveal.toString()};` +
|
|
24
|
+
`function h(e){var t=e.target;if(t&&t.tagName==='IMG'&&t.hasAttribute('data-fx'))r(t)}` +
|
|
25
|
+
`document.addEventListener('load',h,true);document.addEventListener('error',h,true)})()`;
|
package/dist/icons.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type IconGroup = 'care' | 'places' | 'system' | 'things' | 'expressions';
|
|
2
|
+
export declare const ICON_GROUPS: readonly {
|
|
3
|
+
readonly id: IconGroup;
|
|
4
|
+
readonly title: string;
|
|
5
|
+
readonly use: string;
|
|
6
|
+
}[];
|
|
7
|
+
export type SystemIcon = {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly file: string;
|
|
10
|
+
readonly label: string;
|
|
11
|
+
readonly meaning: string;
|
|
12
|
+
readonly group: IconGroup;
|
|
13
|
+
};
|
|
14
|
+
export declare const SYSTEM_ICONS: readonly [SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon, SystemIcon];
|
|
15
|
+
export type SystemIconName = (typeof SYSTEM_ICONS)[number]['name'];
|
|
16
|
+
export declare const ICON_SIZES: {
|
|
17
|
+
readonly xs: 16;
|
|
18
|
+
readonly sm: 20;
|
|
19
|
+
readonly md: 28;
|
|
20
|
+
readonly lg: 46;
|
|
21
|
+
readonly xl: 84;
|
|
22
|
+
};
|
|
23
|
+
export type IconSize = keyof typeof ICON_SIZES;
|
|
24
|
+
export declare const iconByName: (name: string) => SystemIcon | undefined;
|
|
25
|
+
export declare function configureIcons(options: {
|
|
26
|
+
url: (icon: SystemIcon) => string;
|
|
27
|
+
}): void;
|
|
28
|
+
export declare const iconUrl: (icon: SystemIcon) => string;
|