@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.
Files changed (131) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +60 -0
  3. package/dist/FishiesImg.d.ts +9 -0
  4. package/dist/FishiesImg.js +64 -0
  5. package/dist/Select.d.ts +22 -0
  6. package/dist/Select.js +95 -0
  7. package/dist/fx-image.d.ts +3 -0
  8. package/dist/fx-image.js +25 -0
  9. package/dist/icons.d.ts +28 -0
  10. package/dist/icons.generated.d.ts +5 -0
  11. package/dist/icons.generated.js +658 -0
  12. package/dist/icons.js +53 -0
  13. package/dist/index.d.ts +7 -0
  14. package/dist/index.js +7 -0
  15. package/dist/kit.css +267 -0
  16. package/dist/kit.d.ts +159 -0
  17. package/dist/kit.js +104 -0
  18. package/dist/loader.css +48 -0
  19. package/dist/loader.d.ts +17 -0
  20. package/dist/loader.js +27 -0
  21. package/dist/select-state.d.ts +34 -0
  22. package/dist/select-state.js +55 -0
  23. package/dist/select.css +87 -0
  24. package/dist/tokens.d.ts +17 -0
  25. package/dist/tokens.generated.css +97 -0
  26. package/dist/tokens.js +175 -0
  27. package/handbook/README.md +236 -0
  28. package/icons/add.png +0 -0
  29. package/icons/announce.png +0 -0
  30. package/icons/arrange.png +0 -0
  31. package/icons/bell.png +0 -0
  32. package/icons/block.png +0 -0
  33. package/icons/book.png +0 -0
  34. package/icons/bug.png +0 -0
  35. package/icons/camera.png +0 -0
  36. package/icons/cart-add.png +0 -0
  37. package/icons/cart.png +0 -0
  38. package/icons/chat.png +0 -0
  39. package/icons/check.png +0 -0
  40. package/icons/clean.png +0 -0
  41. package/icons/clock.png +0 -0
  42. package/icons/coin.png +0 -0
  43. package/icons/coral.png +0 -0
  44. package/icons/decorate.png +0 -0
  45. package/icons/download.png +0 -0
  46. package/icons/drop.png +0 -0
  47. package/icons/edit.png +0 -0
  48. package/icons/egg.png +0 -0
  49. package/icons/emote-food.png +0 -0
  50. package/icons/emote-heart.png +0 -0
  51. package/icons/emote-hello.png +0 -0
  52. package/icons/emote-music.png +0 -0
  53. package/icons/emote-question.png +0 -0
  54. package/icons/emote-sleep.png +0 -0
  55. package/icons/emote-sparkle.png +0 -0
  56. package/icons/emote-surprise.png +0 -0
  57. package/icons/exit.png +0 -0
  58. package/icons/explore.png +0 -0
  59. package/icons/external.png +0 -0
  60. package/icons/feed.png +0 -0
  61. package/icons/fish.png +0 -0
  62. package/icons/follow.png +0 -0
  63. package/icons/fullscreen.png +0 -0
  64. package/icons/gear.png +0 -0
  65. package/icons/gem.png +0 -0
  66. package/icons/gift.png +0 -0
  67. package/icons/globe.png +0 -0
  68. package/icons/growth.png +0 -0
  69. package/icons/heart.png +0 -0
  70. package/icons/help.png +0 -0
  71. package/icons/home.png +0 -0
  72. package/icons/idea.png +0 -0
  73. package/icons/inspect.png +0 -0
  74. package/icons/inventory.png +0 -0
  75. package/icons/keeper.png +0 -0
  76. package/icons/kelp.png +0 -0
  77. package/icons/keyboard.png +0 -0
  78. package/icons/level.png +0 -0
  79. package/icons/link.png +0 -0
  80. package/icons/lock.png +0 -0
  81. package/icons/mail.png +0 -0
  82. package/icons/map.png +0 -0
  83. package/icons/menu.png +0 -0
  84. package/icons/music.png +0 -0
  85. package/icons/mute.png +0 -0
  86. package/icons/net.png +0 -0
  87. package/icons/people.png +0 -0
  88. package/icons/pin.png +0 -0
  89. package/icons/quest.png +0 -0
  90. package/icons/quote.png +0 -0
  91. package/icons/remedy.png +0 -0
  92. package/icons/reply.png +0 -0
  93. package/icons/report.png +0 -0
  94. package/icons/search.png +0 -0
  95. package/icons/sell.png +0 -0
  96. package/icons/send.png +0 -0
  97. package/icons/share.png +0 -0
  98. package/icons/shell.png +0 -0
  99. package/icons/shop.png +0 -0
  100. package/icons/showcase.png +0 -0
  101. package/icons/smile.png +0 -0
  102. package/icons/solved.png +0 -0
  103. package/icons/sound.png +0 -0
  104. package/icons/staff.png +0 -0
  105. package/icons/star.png +0 -0
  106. package/icons/tank.png +0 -0
  107. package/icons/thermometer.png +0 -0
  108. package/icons/trash.png +0 -0
  109. package/icons/trophy.png +0 -0
  110. package/icons/updates.png +0 -0
  111. package/icons/upvote.png +0 -0
  112. package/icons/views.png +0 -0
  113. package/icons/warning.png +0 -0
  114. package/icons/wave.png +0 -0
  115. package/icons/whisper.png +0 -0
  116. package/package.json +66 -0
  117. package/src/FishiesImg.tsx +58 -0
  118. package/src/Select.tsx +119 -0
  119. package/src/css.d.ts +2 -0
  120. package/src/fx-image.ts +17 -0
  121. package/src/icons.generated.ts +658 -0
  122. package/src/icons.ts +74 -0
  123. package/src/index.ts +8 -0
  124. package/src/kit.css +267 -0
  125. package/src/kit.tsx +190 -0
  126. package/src/loader.css +48 -0
  127. package/src/loader.tsx +35 -0
  128. package/src/select-state.ts +59 -0
  129. package/src/select.css +87 -0
  130. package/src/tokens.generated.css +97 -0
  131. package/src/tokens.ts +196 -0
package/src/Select.tsx ADDED
@@ -0,0 +1,119 @@
1
+ 'use client'
2
+
3
+ import { SystemIcon } from './kit'
4
+ import { useCallback, useEffect, useId, useLayoutEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react'
5
+ import { createPortal } from 'react-dom'
6
+ import { placePopup, selectedIndex, selectKey } from './select-state'
7
+ import './select.css'
8
+
9
+ export type SelectOption = { value: string; label: string; disabled?: boolean; icon?: ReactNode }
10
+
11
+ type Props = {
12
+ value: string
13
+ onChange: (value: string) => void
14
+ options: readonly SelectOption[]
15
+ 'aria-label'?: string
16
+ 'aria-labelledby'?: string
17
+ disabled?: boolean
18
+
19
+ name?: string
20
+ className?: string
21
+ size?: 'regular' | 'compact'
22
+ autoFocus?: boolean
23
+ }
24
+
25
+ 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']
26
+
27
+ export default function Select({ value, onChange, options, disabled, name, className, size = 'regular', autoFocus, ...aria }: Props) {
28
+ const id = useId()
29
+ const trigger = useRef<HTMLButtonElement>(null), list = useRef<HTMLUListElement>(null)
30
+ const [open, setOpen] = useState(false)
31
+ const [active, setActive] = useState(-1)
32
+ const [style, setStyle] = useState<CSSProperties>({})
33
+ const [up, setUp] = useState(false)
34
+ const current = options.find(option => option.value === value)
35
+
36
+ const place = useCallback(() => {
37
+ const button = trigger.current
38
+ if (!button) return
39
+ const rect = button.getBoundingClientRect()
40
+ const spot = placePopup(rect, { width: window.innerWidth, height: window.innerHeight }, 320)
41
+ const computed = getComputedStyle(button)
42
+ const tokens: Record<string, string> = {}
43
+ for (const token of TOKENS) { const resolved = computed.getPropertyValue(token).trim(); if (resolved) tokens[token] = resolved }
44
+ setUp(spot.up)
45
+ 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 } as CSSProperties)
46
+ }, [])
47
+
48
+ const show = (index: number) => { place(); setActive(index); setOpen(true) }
49
+ const close = (focus: boolean) => { setOpen(false); if (focus) trigger.current?.focus() }
50
+ const choose = (next: string) => { if (next !== value) onChange(next) }
51
+
52
+ useLayoutEffect(() => { if (open) place() }, [open, place])
53
+
54
+ useLayoutEffect(() => {
55
+ const popup = list.current
56
+ if (!popup || typeof style.left !== 'number') return
57
+ const left = Math.max(8, Math.min(style.left, window.innerWidth - 8 - popup.offsetWidth))
58
+ if (left !== style.left) setStyle(previous => ({ ...previous, left }))
59
+ }, [style])
60
+ useEffect(() => {
61
+ if (!open) return
62
+ const outside = (event: PointerEvent) => {
63
+ const target = event.target as Node
64
+ if (!trigger.current?.contains(target) && !list.current?.contains(target)) setOpen(false)
65
+ }
66
+ window.addEventListener('pointerdown', outside, true)
67
+ window.addEventListener('resize', place)
68
+ const scrolled = (event: Event) => { if (!list.current?.contains(event.target as Node)) place() }
69
+ window.addEventListener('scroll', scrolled, true)
70
+ return () => { window.removeEventListener('pointerdown', outside, true); window.removeEventListener('resize', place); window.removeEventListener('scroll', scrolled, true) }
71
+ }, [open, place])
72
+ useEffect(() => {
73
+ if (open && active >= 0) list.current?.querySelector<HTMLElement>(`[data-index="${active}"]`)?.scrollIntoView({ block: 'nearest' })
74
+ }, [open, active])
75
+ useEffect(() => { if (disabled) setOpen(false) }, [disabled])
76
+
77
+ const keydown = (event: React.KeyboardEvent<HTMLButtonElement>) => {
78
+ if (event.altKey || event.ctrlKey || event.metaKey) return
79
+ const result = selectKey({ open, active }, event.key, options, value)
80
+ if (!result.handled && result.close !== 'blur') return
81
+ if (result.handled) { event.preventDefault(); event.stopPropagation() }
82
+ if (result.open && !open) { show(result.active); return }
83
+ setActive(result.active)
84
+ if (!result.open) close(result.close === 'focus')
85
+ if (result.choose !== undefined) choose(result.choose)
86
+ }
87
+
88
+ const labelId = `${id}-label`, valueId = `${id}-value`, listId = `${id}-list`
89
+ const labelledby = aria['aria-labelledby'] ?? (aria['aria-label'] ? labelId : undefined)
90
+ const classes = ['fs-select', size === 'compact' ? 'fs-select-compact' : '', open ? 'is-open' : '', className ?? ''].filter(Boolean).join(' ')
91
+
92
+ return <>
93
+ {aria['aria-label'] && !aria['aria-labelledby'] && <span id={labelId} hidden>{aria['aria-label']}</span>}
94
+ <button ref={trigger} type="button" className={classes} disabled={disabled} autoFocus={autoFocus}
95
+ aria-haspopup="listbox" aria-expanded={open} aria-controls={open ? listId : undefined}
96
+ aria-labelledby={labelledby ? `${labelledby} ${valueId}` : undefined}
97
+ aria-activedescendant={open && active >= 0 ? `${id}-option-${active}` : undefined}
98
+ onClick={() => open ? close(false) : show(selectedIndex(options, value))} onKeyDown={keydown}>
99
+ {current?.icon && <span className="fs-select-icon" aria-hidden="true">{current.icon}</span>}
100
+ <span id={valueId} className="fs-select-value">{current?.label ?? ''}</span>
101
+ <i aria-hidden="true">▾</i>
102
+ </button>
103
+ {name && <input type="hidden" name={name} value={value} />}
104
+ {open && typeof document !== 'undefined' && createPortal(
105
+ <ul ref={list} id={listId} role="listbox" aria-labelledby={labelledby} tabIndex={-1}
106
+ className={`fs-select-popover${up ? ' is-up' : ''}${size === 'compact' ? ' fs-select-compact' : ''}`} style={style}
107
+ onPointerDown={event => event.preventDefault()}>
108
+ {options.map((option, index) => <li key={option.value} id={`${id}-option-${index}`} data-index={index} role="option"
109
+ aria-selected={option.value === value} aria-disabled={option.disabled || undefined}
110
+ className={index === active ? 'is-active' : undefined}
111
+ onPointerEnter={() => { if (!option.disabled) setActive(index) }}
112
+ onClick={() => { if (option.disabled) return; close(true); choose(option.value) }}>
113
+ {option.icon && <span className="fs-select-icon" aria-hidden="true">{option.icon}</span>}
114
+ <span>{option.label}</span>
115
+ {option.value === value && <b aria-hidden="true"><SystemIcon name="check" size={16}/></b>}
116
+ </li>)}
117
+ </ul>, document.body)}
118
+ </>
119
+ }
package/src/css.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ // The components import their own stylesheets; a bundler brings them in.
2
+ declare module '*.css'
@@ -0,0 +1,17 @@
1
+ export type FxState = 'wait' | 'fade' | 'ready' | 'error'
2
+
3
+ export function reveal(img: HTMLImageElement, instant?: boolean): void {
4
+ if (img.getAttribute('data-fx') !== 'wait' || !img.complete) return
5
+ const src = img.currentSrc || img.src
6
+ const set = (state: string) => {
7
+ if (img.getAttribute('data-fx') === 'wait' && (img.currentSrc || img.src) === src) img.setAttribute('data-fx', state)
8
+ }
9
+ if (!img.naturalWidth) { set('error'); return }
10
+ if (instant) { set('ready'); return }
11
+ if (typeof img.decode !== 'function') { set('fade'); return }
12
+ img.decode().then(() => set('fade'), () => set(img.naturalWidth ? 'fade' : 'error'))
13
+ }
14
+
15
+ export const FX_IMAGE_SCRIPT = `(function(){var r=${reveal.toString()};` +
16
+ `function h(e){var t=e.target;if(t&&t.tagName==='IMG'&&t.hasAttribute('data-fx'))r(t)}` +
17
+ `document.addEventListener('load',h,true);document.addEventListener('error',h,true)})()`