@sveltier/ui 0.0.0-stage → 0.1.0-alpha.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.
Files changed (276) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -2
  3. package/dist/components/Alert/Alert.svelte +88 -0
  4. package/dist/components/Alert/Alert.svelte.d.ts +4 -0
  5. package/dist/components/Alert/alert.css +78 -0
  6. package/dist/components/Alert/index.d.ts +2 -0
  7. package/dist/components/Alert/index.js +1 -0
  8. package/dist/components/Alert/types.d.ts +67 -0
  9. package/dist/components/Alert/types.js +1 -0
  10. package/dist/components/Avatar/Avatar.svelte +47 -0
  11. package/dist/components/Avatar/Avatar.svelte.d.ts +4 -0
  12. package/dist/components/Avatar/avatar.css +34 -0
  13. package/dist/components/Avatar/index.d.ts +2 -0
  14. package/dist/components/Avatar/index.js +1 -0
  15. package/dist/components/Avatar/types.d.ts +47 -0
  16. package/dist/components/Avatar/types.js +1 -0
  17. package/dist/components/Badge/Badge.svelte +53 -0
  18. package/dist/components/Badge/Badge.svelte.d.ts +4 -0
  19. package/dist/components/Badge/badge.css +57 -0
  20. package/dist/components/Badge/index.d.ts +2 -0
  21. package/dist/components/Badge/index.js +1 -0
  22. package/dist/components/Badge/types.d.ts +43 -0
  23. package/dist/components/Badge/types.js +1 -0
  24. package/dist/components/Button/Button.svelte +85 -0
  25. package/dist/components/Button/Button.svelte.d.ts +4 -0
  26. package/dist/components/Button/button.css +132 -0
  27. package/dist/components/Button/index.d.ts +2 -0
  28. package/dist/components/Button/index.js +1 -0
  29. package/dist/components/Button/types.d.ts +76 -0
  30. package/dist/components/Button/types.js +1 -0
  31. package/dist/components/ButtonGroup/ButtonGroup.svelte +61 -0
  32. package/dist/components/ButtonGroup/ButtonGroup.svelte.d.ts +4 -0
  33. package/dist/components/ButtonGroup/button-group.css +39 -0
  34. package/dist/components/ButtonGroup/index.d.ts +2 -0
  35. package/dist/components/ButtonGroup/index.js +1 -0
  36. package/dist/components/ButtonGroup/types.d.ts +48 -0
  37. package/dist/components/ButtonGroup/types.js +1 -0
  38. package/dist/components/Card/Card.svelte +53 -0
  39. package/dist/components/Card/Card.svelte.d.ts +4 -0
  40. package/dist/components/Card/card.css +62 -0
  41. package/dist/components/Card/index.d.ts +2 -0
  42. package/dist/components/Card/index.js +1 -0
  43. package/dist/components/Card/types.d.ts +56 -0
  44. package/dist/components/Card/types.js +1 -0
  45. package/dist/components/Checkbox/Checkbox.svelte +105 -0
  46. package/dist/components/Checkbox/Checkbox.svelte.d.ts +4 -0
  47. package/dist/components/Checkbox/checkbox.css +138 -0
  48. package/dist/components/Checkbox/index.d.ts +2 -0
  49. package/dist/components/Checkbox/index.js +1 -0
  50. package/dist/components/Checkbox/types.d.ts +76 -0
  51. package/dist/components/Checkbox/types.js +1 -0
  52. package/dist/components/Chip/Chip.svelte +109 -0
  53. package/dist/components/Chip/Chip.svelte.d.ts +4 -0
  54. package/dist/components/Chip/chip.css +101 -0
  55. package/dist/components/Chip/index.d.ts +2 -0
  56. package/dist/components/Chip/index.js +1 -0
  57. package/dist/components/Chip/types.d.ts +69 -0
  58. package/dist/components/Chip/types.js +1 -0
  59. package/dist/components/Choice/Choice.svelte +60 -0
  60. package/dist/components/Choice/Choice.svelte.d.ts +4 -0
  61. package/dist/components/Choice/choice.css +75 -0
  62. package/dist/components/Choice/types.d.ts +43 -0
  63. package/dist/components/Choice/types.js +1 -0
  64. package/dist/components/Divider/Divider.svelte +19 -0
  65. package/dist/components/Divider/Divider.svelte.d.ts +4 -0
  66. package/dist/components/Divider/divider.css +23 -0
  67. package/dist/components/Divider/index.d.ts +2 -0
  68. package/dist/components/Divider/index.js +1 -0
  69. package/dist/components/Divider/types.d.ts +10 -0
  70. package/dist/components/Divider/types.js +1 -0
  71. package/dist/components/Field/Field.svelte +95 -0
  72. package/dist/components/Field/Field.svelte.d.ts +4 -0
  73. package/dist/components/Field/field.css +210 -0
  74. package/dist/components/Field/index.d.ts +3 -0
  75. package/dist/components/Field/index.js +2 -0
  76. package/dist/components/Field/types.d.ts +109 -0
  77. package/dist/components/Field/types.js +2 -0
  78. package/dist/components/Form/Form.svelte +93 -0
  79. package/dist/components/Form/Form.svelte.d.ts +8 -0
  80. package/dist/components/Form/form.css +16 -0
  81. package/dist/components/Form/index.d.ts +2 -0
  82. package/dist/components/Form/index.js +1 -0
  83. package/dist/components/Form/types.d.ts +38 -0
  84. package/dist/components/Form/types.js +1 -0
  85. package/dist/components/Icon/Icon.svelte +38 -0
  86. package/dist/components/Icon/Icon.svelte.d.ts +4 -0
  87. package/dist/components/Icon/icon.css +33 -0
  88. package/dist/components/Icon/index.d.ts +2 -0
  89. package/dist/components/Icon/index.js +1 -0
  90. package/dist/components/Icon/types.d.ts +34 -0
  91. package/dist/components/Icon/types.js +1 -0
  92. package/dist/components/List/List.svelte +39 -0
  93. package/dist/components/List/List.svelte.d.ts +4 -0
  94. package/dist/components/List/ListItem.svelte +57 -0
  95. package/dist/components/List/ListItem.svelte.d.ts +4 -0
  96. package/dist/components/List/index.d.ts +3 -0
  97. package/dist/components/List/index.js +2 -0
  98. package/dist/components/List/list.css +127 -0
  99. package/dist/components/List/types.d.ts +67 -0
  100. package/dist/components/List/types.js +1 -0
  101. package/dist/components/NativeSelect/NativeSelect.svelte +110 -0
  102. package/dist/components/NativeSelect/NativeSelect.svelte.d.ts +25 -0
  103. package/dist/components/NativeSelect/index.d.ts +2 -0
  104. package/dist/components/NativeSelect/index.js +1 -0
  105. package/dist/components/NativeSelect/native-select.css +45 -0
  106. package/dist/components/NativeSelect/types.d.ts +58 -0
  107. package/dist/components/NativeSelect/types.js +1 -0
  108. package/dist/components/NumberField/NumberField.svelte +246 -0
  109. package/dist/components/NumberField/NumberField.svelte.d.ts +4 -0
  110. package/dist/components/NumberField/index.d.ts +2 -0
  111. package/dist/components/NumberField/index.js +1 -0
  112. package/dist/components/NumberField/number-field.css +74 -0
  113. package/dist/components/NumberField/types.d.ts +73 -0
  114. package/dist/components/NumberField/types.js +1 -0
  115. package/dist/components/Progress/ProgressCircular.svelte +61 -0
  116. package/dist/components/Progress/ProgressCircular.svelte.d.ts +4 -0
  117. package/dist/components/Progress/ProgressLinear.svelte +44 -0
  118. package/dist/components/Progress/ProgressLinear.svelte.d.ts +4 -0
  119. package/dist/components/Progress/index.d.ts +3 -0
  120. package/dist/components/Progress/index.js +2 -0
  121. package/dist/components/Progress/progress-circular.css +61 -0
  122. package/dist/components/Progress/progress-linear.css +67 -0
  123. package/dist/components/Progress/types.d.ts +53 -0
  124. package/dist/components/Progress/types.js +1 -0
  125. package/dist/components/RadioGroup/RadioGroup.svelte +149 -0
  126. package/dist/components/RadioGroup/RadioGroup.svelte.d.ts +25 -0
  127. package/dist/components/RadioGroup/index.d.ts +2 -0
  128. package/dist/components/RadioGroup/index.js +1 -0
  129. package/dist/components/RadioGroup/radio-group.css +139 -0
  130. package/dist/components/RadioGroup/types.d.ts +93 -0
  131. package/dist/components/RadioGroup/types.js +1 -0
  132. package/dist/components/SegmentedControl/SegmentedControl.svelte +70 -0
  133. package/dist/components/SegmentedControl/SegmentedControl.svelte.d.ts +25 -0
  134. package/dist/components/SegmentedControl/index.d.ts +2 -0
  135. package/dist/components/SegmentedControl/index.js +1 -0
  136. package/dist/components/SegmentedControl/segmented-control.css +68 -0
  137. package/dist/components/SegmentedControl/types.d.ts +60 -0
  138. package/dist/components/SegmentedControl/types.js +1 -0
  139. package/dist/components/Skeleton/Skeleton.svelte +33 -0
  140. package/dist/components/Skeleton/Skeleton.svelte.d.ts +4 -0
  141. package/dist/components/Skeleton/index.d.ts +2 -0
  142. package/dist/components/Skeleton/index.js +1 -0
  143. package/dist/components/Skeleton/skeleton.css +70 -0
  144. package/dist/components/Skeleton/types.d.ts +25 -0
  145. package/dist/components/Skeleton/types.js +1 -0
  146. package/dist/components/Slider/Slider.svelte +153 -0
  147. package/dist/components/Slider/Slider.svelte.d.ts +4 -0
  148. package/dist/components/Slider/index.d.ts +2 -0
  149. package/dist/components/Slider/index.js +1 -0
  150. package/dist/components/Slider/slider.css +206 -0
  151. package/dist/components/Slider/types.d.ts +102 -0
  152. package/dist/components/Slider/types.js +1 -0
  153. package/dist/components/Switch/Switch.svelte +94 -0
  154. package/dist/components/Switch/Switch.svelte.d.ts +4 -0
  155. package/dist/components/Switch/index.d.ts +2 -0
  156. package/dist/components/Switch/index.js +1 -0
  157. package/dist/components/Switch/switch.css +118 -0
  158. package/dist/components/Switch/types.d.ts +67 -0
  159. package/dist/components/Switch/types.js +1 -0
  160. package/dist/components/Tabs/Tabs.svelte +93 -0
  161. package/dist/components/Tabs/Tabs.svelte.d.ts +25 -0
  162. package/dist/components/Tabs/index.d.ts +2 -0
  163. package/dist/components/Tabs/index.js +1 -0
  164. package/dist/components/Tabs/tabs.css +73 -0
  165. package/dist/components/Tabs/types.d.ts +59 -0
  166. package/dist/components/Tabs/types.js +1 -0
  167. package/dist/components/TextField/TextField.svelte +115 -0
  168. package/dist/components/TextField/TextField.svelte.d.ts +4 -0
  169. package/dist/components/TextField/index.d.ts +2 -0
  170. package/dist/components/TextField/index.js +1 -0
  171. package/dist/components/TextField/types.d.ts +57 -0
  172. package/dist/components/TextField/types.js +1 -0
  173. package/dist/components/Textarea/Textarea.svelte +116 -0
  174. package/dist/components/Textarea/Textarea.svelte.d.ts +4 -0
  175. package/dist/components/Textarea/index.d.ts +2 -0
  176. package/dist/components/Textarea/index.js +1 -0
  177. package/dist/components/Textarea/textarea.css +35 -0
  178. package/dist/components/Textarea/types.d.ts +60 -0
  179. package/dist/components/Textarea/types.js +1 -0
  180. package/dist/core/defaults/DefaultsProvider.svelte +30 -0
  181. package/dist/core/defaults/DefaultsProvider.svelte.d.ts +13 -0
  182. package/dist/core/defaults/context.d.ts +45 -0
  183. package/dist/core/defaults/context.js +38 -0
  184. package/dist/core/display/display.d.ts +39 -0
  185. package/dist/core/display/display.js +53 -0
  186. package/dist/core/errors/codes.d.ts +112 -0
  187. package/dist/core/errors/codes.js +96 -0
  188. package/dist/core/errors/errors.d.ts +8 -0
  189. package/dist/core/errors/errors.js +7 -0
  190. package/dist/core/form/constraints.d.ts +15 -0
  191. package/dist/core/form/constraints.js +42 -0
  192. package/dist/core/form/context.d.ts +19 -0
  193. package/dist/core/form/context.js +4 -0
  194. package/dist/core/form/field.svelte.d.ts +34 -0
  195. package/dist/core/form/field.svelte.js +66 -0
  196. package/dist/core/form/number.d.ts +24 -0
  197. package/dist/core/form/number.js +71 -0
  198. package/dist/core/form/types.d.ts +22 -0
  199. package/dist/core/form/types.js +1 -0
  200. package/dist/core/locale/locale.d.ts +55 -0
  201. package/dist/core/locale/locale.js +80 -0
  202. package/dist/core/props/classes.d.ts +21 -0
  203. package/dist/core/props/classes.js +44 -0
  204. package/dist/core/props/element.d.ts +18 -0
  205. package/dist/core/props/element.js +1 -0
  206. package/dist/core/props/types.d.ts +47 -0
  207. package/dist/core/props/types.js +16 -0
  208. package/dist/core/provider/UIProvider.svelte +60 -0
  209. package/dist/core/provider/UIProvider.svelte.d.ts +29 -0
  210. package/dist/core/theme/ThemeProvider.svelte +178 -0
  211. package/dist/core/theme/ThemeProvider.svelte.d.ts +25 -0
  212. package/dist/core/theme/color.d.ts +23 -0
  213. package/dist/core/theme/color.js +54 -0
  214. package/dist/core/theme/context.d.ts +25 -0
  215. package/dist/core/theme/context.js +14 -0
  216. package/dist/core/theme/css.d.ts +25 -0
  217. package/dist/core/theme/css.js +69 -0
  218. package/dist/core/theme/define.d.ts +11 -0
  219. package/dist/core/theme/define.js +21 -0
  220. package/dist/core/theme/resolve.d.ts +6 -0
  221. package/dist/core/theme/resolve.js +18 -0
  222. package/dist/core/theme/theme-provider.css +13 -0
  223. package/dist/core/theme/types.d.ts +35 -0
  224. package/dist/core/theme/types.js +61 -0
  225. package/dist/dev/off.d.ts +4 -0
  226. package/dist/dev/off.js +3 -0
  227. package/dist/dev/on.d.ts +8 -0
  228. package/dist/dev/on.js +33 -0
  229. package/dist/index.d.ts +58 -0
  230. package/dist/index.js +72 -0
  231. package/dist/primitives/anchored/anchored.d.ts +39 -0
  232. package/dist/primitives/anchored/anchored.js +104 -0
  233. package/dist/primitives/anchored/fallback.d.ts +31 -0
  234. package/dist/primitives/anchored/fallback.js +73 -0
  235. package/dist/primitives/focus/focus-trap.d.ts +25 -0
  236. package/dist/primitives/focus/focus-trap.js +86 -0
  237. package/dist/primitives/focus/roving-focus.d.ts +22 -0
  238. package/dist/primitives/focus/roving-focus.js +86 -0
  239. package/dist/primitives/focus/tabbable.d.ts +9 -0
  240. package/dist/primitives/focus/tabbable.js +75 -0
  241. package/dist/primitives/inert/inert-others.d.ts +12 -0
  242. package/dist/primitives/inert/inert-others.js +44 -0
  243. package/dist/primitives/keyboard/keymap.d.ts +17 -0
  244. package/dist/primitives/keyboard/keymap.js +39 -0
  245. package/dist/primitives/keyboard/typeahead.d.ts +18 -0
  246. package/dist/primitives/keyboard/typeahead.js +50 -0
  247. package/dist/primitives/layer/dismissable.d.ts +26 -0
  248. package/dist/primitives/layer/dismissable.js +84 -0
  249. package/dist/primitives/motion/easing.d.ts +9 -0
  250. package/dist/primitives/motion/easing.js +46 -0
  251. package/dist/primitives/motion/motion.d.ts +59 -0
  252. package/dist/primitives/motion/motion.js +80 -0
  253. package/dist/primitives/portal/Portal.svelte +37 -0
  254. package/dist/primitives/portal/Portal.svelte.d.ts +9 -0
  255. package/dist/primitives/portal/PortalContent.svelte +15 -0
  256. package/dist/primitives/portal/PortalContent.svelte.d.ts +9 -0
  257. package/dist/primitives/portal/portal.css +8 -0
  258. package/dist/primitives/scroll-lock/scroll-lock.d.ts +10 -0
  259. package/dist/primitives/scroll-lock/scroll-lock.js +33 -0
  260. package/dist/primitives/virtual/virtual-list.svelte.d.ts +65 -0
  261. package/dist/primitives/virtual/virtual-list.svelte.js +291 -0
  262. package/dist/styles/base.css +44 -0
  263. package/dist/styles/props.css +137 -0
  264. package/dist/styles/tokens.css +84 -0
  265. package/dist/styles/visually-hidden.css +16 -0
  266. package/dist/themes/aurora.d.ts +13 -0
  267. package/dist/themes/aurora.js +103 -0
  268. package/dist/themes/heritage.d.ts +13 -0
  269. package/dist/themes/heritage.js +102 -0
  270. package/dist/themes/maison.d.ts +13 -0
  271. package/dist/themes/maison.js +99 -0
  272. package/dist/themes/milano.d.ts +13 -0
  273. package/dist/themes/milano.js +101 -0
  274. package/dist/themes/monolith.d.ts +13 -0
  275. package/dist/themes/monolith.js +101 -0
  276. package/package.json +87 -5
@@ -0,0 +1,104 @@
1
+ export const SIDES = ['top', 'bottom', 'start', 'end'];
2
+ export const ALIGNS = ['start', 'center', 'end'];
3
+ export const PHYSICAL_SIDES = ['top', 'bottom', 'left', 'right'];
4
+ /**
5
+ * The physical side, and the alignment in physical terms (start is left or top), of a
6
+ * logical placement in the element's direction. Both placement paths use it, so they agree.
7
+ */
8
+ export function physical(side, align, rtl) {
9
+ if (side === 'top' || side === 'bottom') {
10
+ const mirrored = align === 'start' ? 'end' : align === 'end' ? 'start' : align;
11
+ return [side, rtl ? mirrored : align];
12
+ }
13
+ return [(side === 'end') !== rtl ? 'right' : 'left', align];
14
+ }
15
+ /** `position-area` in physical keywords: `bottom span-right` is below, from the left edge. */
16
+ export function positionArea(side, align) {
17
+ if (align === 'center')
18
+ return side;
19
+ const vertical = side === 'top' || side === 'bottom';
20
+ const span = vertical
21
+ ? align === 'start'
22
+ ? 'right'
23
+ : 'left'
24
+ : align === 'start'
25
+ ? 'bottom'
26
+ : 'top';
27
+ return `${side} span-${span}`;
28
+ }
29
+ // The margin that faces the anchor; flipping mirrors it to the other side.
30
+ const MARGIN = {
31
+ top: 'margin-bottom',
32
+ bottom: 'margin-top',
33
+ left: 'margin-right',
34
+ right: 'margin-left'
35
+ };
36
+ let names = 0;
37
+ /**
38
+ * Whether the element sits in a part of the page whose direction differs from the root's.
39
+ * Firefox (157) misplaces anchored elements there, so the script places them instead.
40
+ */
41
+ function mixedDirection(element) {
42
+ const root = getComputedStyle(document.documentElement).direction;
43
+ return [element, element.parentElement].some((node) => node && getComputedStyle(node).direction !== root);
44
+ }
45
+ /**
46
+ * Places the element next to `anchor` and keeps it there on scroll and resize, flipping to
47
+ * the opposite side when there is not enough room. Uses CSS anchor positioning where the
48
+ * browser has it, otherwise loads a small script. The element must be positioned against the
49
+ * viewport: a popover in the top layer, or `position: fixed`.
50
+ *
51
+ * @example
52
+ * <button bind:this={trigger}>Account</button>
53
+ * <div popover="manual" {@attach anchored({ anchor: trigger, side: 'bottom', align: 'end' })}>…</div>
54
+ */
55
+ export function anchored({ anchor, side = 'bottom', align = 'start', offset = 5, flip = true }) {
56
+ return (floating) => {
57
+ if (!anchor)
58
+ return;
59
+ const [physicalSide, physicalAlign] = physical(side, align, getComputedStyle(floating).direction === 'rtl');
60
+ if (!CSS.supports('position-try-fallbacks: flip-block') || mixedDirection(floating)) {
61
+ let stop;
62
+ let cancelled = false;
63
+ const placement = { side: physicalSide, align: physicalAlign, offset, flip };
64
+ void import('./fallback.js').then(({ follow }) => {
65
+ if (!cancelled)
66
+ stop = follow(anchor, floating, placement);
67
+ });
68
+ return () => {
69
+ cancelled = true;
70
+ stop?.();
71
+ };
72
+ }
73
+ const name = `--sv-anchor-${++names}`;
74
+ const anchorStyle = anchor.style;
75
+ const existing = getComputedStyle(anchor).getPropertyValue('anchor-name');
76
+ anchorStyle.setProperty('anchor-name', existing && existing !== 'none' ? `${existing}, ${name}` : name);
77
+ const vertical = physicalSide === 'top' || physicalSide === 'bottom';
78
+ const style = floating.style;
79
+ const properties = {
80
+ 'position-anchor': name,
81
+ 'position-area': positionArea(physicalSide, physicalAlign),
82
+ 'position-try-fallbacks': flip ? (vertical ? 'flip-block' : 'flip-inline') : 'none',
83
+ inset: 'auto',
84
+ margin: '0',
85
+ [MARGIN[physicalSide]]: `${offset}px`
86
+ };
87
+ for (const [property, value] of Object.entries(properties))
88
+ style.setProperty(property, value);
89
+ return () => {
90
+ for (const property of Object.keys(properties))
91
+ style.removeProperty(property);
92
+ // Other floating elements may share this anchor: remove only this name.
93
+ const rest = anchorStyle
94
+ .getPropertyValue('anchor-name')
95
+ .split(',')
96
+ .map((entry) => entry.trim())
97
+ .filter((entry) => entry && entry !== name);
98
+ if (rest.length)
99
+ anchorStyle.setProperty('anchor-name', rest.join(', '));
100
+ else
101
+ anchorStyle.removeProperty('anchor-name');
102
+ };
103
+ };
104
+ }
@@ -0,0 +1,31 @@
1
+ import type { Align, PhysicalSide as Physical } from './anchored.js';
2
+ interface Size {
3
+ width: number;
4
+ height: number;
5
+ }
6
+ interface Rect extends Size {
7
+ top: number;
8
+ left: number;
9
+ bottom: number;
10
+ right: number;
11
+ }
12
+ /**
13
+ * Where the floating element goes: on `side` of the anchor, aligned along it, on the opposite
14
+ * side when that has more room (with `flip`), and kept inside the viewport.
15
+ */
16
+ export declare function place(anchor: Rect, floating: Size, viewport: Size, side: Physical, align: Align, offset: number, flip?: boolean): {
17
+ x: number;
18
+ y: number;
19
+ side: Physical;
20
+ };
21
+ export interface Placement {
22
+ /** Physical side, as anchored() resolves it from the writing direction. */
23
+ side: Physical;
24
+ /** Physical alignment: start is left or top. */
25
+ align: Align;
26
+ offset: number;
27
+ flip: boolean;
28
+ }
29
+ /** Keeps `floating` placed against `anchor` until the returned function is called. */
30
+ export declare function follow(anchor: Element, floating: HTMLElement, { side, align, offset, flip }: Placement): () => void;
31
+ export {};
@@ -0,0 +1,73 @@
1
+ const OPPOSITE = {
2
+ top: 'bottom',
3
+ bottom: 'top',
4
+ left: 'right',
5
+ right: 'left'
6
+ };
7
+ // Distance kept from the viewport edges, in px.
8
+ const PADDING = 8;
9
+ const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
10
+ /**
11
+ * Where the floating element goes: on `side` of the anchor, aligned along it, on the opposite
12
+ * side when that has more room (with `flip`), and kept inside the viewport.
13
+ */
14
+ export function place(anchor, floating, viewport, side, align, offset, flip = true) {
15
+ const room = {
16
+ top: anchor.top - PADDING,
17
+ bottom: viewport.height - anchor.bottom - PADDING,
18
+ left: anchor.left - PADDING,
19
+ right: viewport.width - anchor.right - PADDING
20
+ };
21
+ const vertical = side === 'top' || side === 'bottom';
22
+ const need = (vertical ? floating.height : floating.width) + offset;
23
+ if (flip && room[side] < need && room[OPPOSITE[side]] > room[side])
24
+ side = OPPOSITE[side];
25
+ const [start, length, size, axis] = vertical
26
+ ? [anchor.left, anchor.width, floating.width, viewport.width]
27
+ : [anchor.top, anchor.height, floating.height, viewport.height];
28
+ const cross = clamp(align === 'start'
29
+ ? start
30
+ : align === 'end'
31
+ ? start + length - size
32
+ : start + (length - size) / 2, PADDING, axis - size - PADDING);
33
+ const main = side === 'top'
34
+ ? anchor.top - offset - floating.height
35
+ : side === 'bottom'
36
+ ? anchor.bottom + offset
37
+ : side === 'left'
38
+ ? anchor.left - offset - floating.width
39
+ : anchor.right + offset;
40
+ return vertical ? { x: cross, y: main, side } : { x: main, y: cross, side };
41
+ }
42
+ /** Keeps `floating` placed against `anchor` until the returned function is called. */
43
+ export function follow(anchor, floating, { side, align, offset, flip }) {
44
+ const style = floating.style;
45
+ style.setProperty('inset', '0 auto auto 0');
46
+ style.setProperty('margin', '0');
47
+ let frame = 0;
48
+ function update() {
49
+ frame = 0;
50
+ const root = document.documentElement;
51
+ const result = place(anchor.getBoundingClientRect(), floating.getBoundingClientRect(), { width: root.clientWidth, height: root.clientHeight }, side, align, offset, flip);
52
+ style.setProperty('translate', `${result.x}px ${result.y}px`);
53
+ floating.dataset.side = result.side;
54
+ }
55
+ const schedule = () => {
56
+ frame ||= requestAnimationFrame(update);
57
+ };
58
+ const observer = new ResizeObserver(schedule);
59
+ observer.observe(anchor);
60
+ observer.observe(floating);
61
+ addEventListener('scroll', schedule, { capture: true, passive: true });
62
+ addEventListener('resize', schedule, { passive: true });
63
+ update();
64
+ return () => {
65
+ cancelAnimationFrame(frame);
66
+ observer.disconnect();
67
+ removeEventListener('scroll', schedule, { capture: true });
68
+ removeEventListener('resize', schedule);
69
+ for (const property of ['inset', 'margin', 'translate'])
70
+ style.removeProperty(property);
71
+ delete floating.dataset.side;
72
+ };
73
+ }
@@ -0,0 +1,25 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ import { type FocusableElement } from './tabbable.js';
3
+ export interface FocusTrapOptions {
4
+ /**
5
+ * Where focus goes when the trap starts, unless it is already inside: an element, a selector
6
+ * inside the container, or `false` to leave it where it is. Defaults to the first tabbable
7
+ * element, else the container.
8
+ */
9
+ initialFocus?: FocusableElement | string | false;
10
+ /**
11
+ * Where focus goes when the trap ends: `true` for the element that had it when the trap
12
+ * started, an element (pass the trigger: Safari does not focus a button on click, so the
13
+ * element that had focus may be the page itself), or `false` to leave it. Defaults to `true`.
14
+ */
15
+ returnFocus?: boolean | FocusableElement | null;
16
+ }
17
+ /**
18
+ * Keeps keyboard focus inside the element: Tab and Shift+Tab wrap at the edges, and focus
19
+ * that leaves by pointer or script is brought back. Traps stack: a trap inside another, or
20
+ * opened after it, takes over until it ends, then the previous one resumes.
21
+ *
22
+ * @example
23
+ * <div role="dialog" aria-modal="true" {@attach open && focusTrap()}>…</div>
24
+ */
25
+ export declare function focusTrap({ initialFocus, returnFocus }?: FocusTrapOptions): Attachment<HTMLElement>;
@@ -0,0 +1,86 @@
1
+ import { tabbable } from './tabbable.js';
2
+ // Active traps, innermost last. Only the last one acts.
3
+ const traps = [];
4
+ /**
5
+ * Keeps keyboard focus inside the element: Tab and Shift+Tab wrap at the edges, and focus
6
+ * that leaves by pointer or script is brought back. Traps stack: a trap inside another, or
7
+ * opened after it, takes over until it ends, then the previous one resumes.
8
+ *
9
+ * @example
10
+ * <div role="dialog" aria-modal="true" {@attach open && focusTrap()}>…</div>
11
+ */
12
+ export function focusTrap({ initialFocus, returnFocus = true } = {}) {
13
+ return (container) => {
14
+ const previous = returnFocus === true ? document.activeElement : returnFocus;
15
+ let madeFocusable = false;
16
+ let last;
17
+ function enter(target) {
18
+ if (target)
19
+ return target.focus();
20
+ if (!container.hasAttribute('tabindex')) {
21
+ container.tabIndex = -1;
22
+ madeFocusable = true;
23
+ }
24
+ container.focus();
25
+ }
26
+ const trap = {
27
+ container,
28
+ resume: () => enter(last?.isConnected && container.contains(last) ? last : tabbable(container)[0])
29
+ };
30
+ const active = () => traps.at(-1) === trap;
31
+ function onkeydown(event) {
32
+ if (event.key !== 'Tab' || event.defaultPrevented || !active())
33
+ return;
34
+ const items = tabbable(container);
35
+ const current = document.activeElement;
36
+ const edge = event.shiftKey ? items[0] : items.at(-1);
37
+ if (items.length === 0 || current === edge || !container.contains(current)) {
38
+ event.preventDefault();
39
+ enter(event.shiftKey ? items.at(-1) : items[0]);
40
+ }
41
+ }
42
+ function onfocusin(event) {
43
+ if (!active())
44
+ return;
45
+ const target = event.target;
46
+ if (container.contains(target))
47
+ last = target;
48
+ else
49
+ trap.resume();
50
+ }
51
+ // A trap mounted around active traps (children attach first) goes beneath them.
52
+ const inner = traps.findIndex((other) => container.contains(other.container));
53
+ if (inner < 0)
54
+ traps.push(trap);
55
+ else
56
+ traps.splice(inner, 0, trap);
57
+ document.addEventListener('keydown', onkeydown, true);
58
+ document.addEventListener('focusin', onfocusin, true);
59
+ if (initialFocus !== false && !container.contains(document.activeElement)) {
60
+ enter(typeof initialFocus === 'string'
61
+ ? container.querySelector(initialFocus)
62
+ : (initialFocus ?? tabbable(container)[0]));
63
+ }
64
+ return () => {
65
+ document.removeEventListener('keydown', onkeydown, true);
66
+ document.removeEventListener('focusin', onfocusin, true);
67
+ traps.splice(traps.indexOf(trap), 1);
68
+ if (madeFocusable)
69
+ container.removeAttribute('tabindex');
70
+ // In a later task: after the other attachments of the closing surface have cleaned up
71
+ // (inertOthers may still hold the page inert now) and after the browser has moved
72
+ // focus for the press that closed it. Then give focus back if it is still ours
73
+ // (inside the trap, or lost to the page), and let the trap below take over again.
74
+ setTimeout(() => {
75
+ const focused = document.activeElement;
76
+ const lost = !focused || focused === document.body || container.contains(focused);
77
+ const restorable = previous instanceof HTMLElement || previous instanceof SVGElement;
78
+ if (lost && restorable && previous.isConnected)
79
+ previous.focus();
80
+ const below = traps.at(-1);
81
+ if (below && !below.container.contains(document.activeElement))
82
+ below.resume();
83
+ });
84
+ };
85
+ };
86
+ }
@@ -0,0 +1,22 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ export interface RovingFocusOptions {
3
+ /** Which arrow keys move: left/right, up/down, or both. Defaults to `'horizontal'`. */
4
+ orientation?: 'horizontal' | 'vertical' | 'both';
5
+ /** Wrap from the last item to the first and back. Defaults to `true`. */
6
+ loop?: boolean;
7
+ /** Selector for the items, inside the container. Defaults to `[data-sv-item]`. */
8
+ items?: string;
9
+ }
10
+ /**
11
+ * Gives a composite widget (toolbar, menu, radio group, tabs, listbox) one tab stop: the
12
+ * active item has `tabindex="0"`, the others `-1`. Arrow keys move along the orientation
13
+ * (left and right follow the writing direction), Home and End jump to the ends, and disabled
14
+ * items are skipped. The first stop is the item marked selected, checked or current.
15
+ *
16
+ * @example
17
+ * <div role="toolbar" {@attach rovingFocus()}>
18
+ * <button data-sv-item>Bold</button>
19
+ * <button data-sv-item>Italic</button>
20
+ * </div>
21
+ */
22
+ export declare function rovingFocus({ orientation, loop, items }?: RovingFocusOptions): Attachment<HTMLElement>;
@@ -0,0 +1,86 @@
1
+ const disabled = (item) => item.matches(':disabled') || item.getAttribute('aria-disabled') === 'true';
2
+ /**
3
+ * Gives a composite widget (toolbar, menu, radio group, tabs, listbox) one tab stop: the
4
+ * active item has `tabindex="0"`, the others `-1`. Arrow keys move along the orientation
5
+ * (left and right follow the writing direction), Home and End jump to the ends, and disabled
6
+ * items are skipped. The first stop is the item marked selected, checked or current.
7
+ *
8
+ * @example
9
+ * <div role="toolbar" {@attach rovingFocus()}>
10
+ * <button data-sv-item>Bold</button>
11
+ * <button data-sv-item>Italic</button>
12
+ * </div>
13
+ */
14
+ export function rovingFocus({ orientation = 'horizontal', loop = true, items = '[data-sv-item]' } = {}) {
15
+ return (container) => {
16
+ const all = () => Array.from(container.querySelectorAll(items));
17
+ let current;
18
+ /** Makes `item` the tab stop, or keeps the current one, or picks the first enabled. */
19
+ function sync(item) {
20
+ const list = all();
21
+ const enabled = list.filter((candidate) => !disabled(candidate));
22
+ if (item)
23
+ current = item;
24
+ if (!current || !list.includes(current) || disabled(current)) {
25
+ current =
26
+ enabled.find((candidate) => candidate.matches('[aria-selected="true"],[aria-checked="true"],[aria-current]:not([aria-current="false"])')) ?? enabled[0];
27
+ }
28
+ for (const candidate of list)
29
+ candidate.tabIndex = candidate === current ? 0 : -1;
30
+ }
31
+ function onkeydown(event) {
32
+ if (event.defaultPrevented || event.altKey || event.ctrlKey || event.metaKey)
33
+ return;
34
+ const list = all().filter((item) => !disabled(item));
35
+ const index = list.indexOf(event.target);
36
+ if (index < 0)
37
+ return;
38
+ const forward = getComputedStyle(container).direction === 'rtl' ? -1 : 1;
39
+ const horizontal = orientation !== 'vertical';
40
+ const vertical = orientation !== 'horizontal';
41
+ const step = {
42
+ ArrowRight: horizontal ? forward : undefined,
43
+ ArrowLeft: horizontal ? -forward : undefined,
44
+ ArrowDown: vertical ? 1 : undefined,
45
+ ArrowUp: vertical ? -1 : undefined
46
+ };
47
+ let next;
48
+ if (event.key === 'Home')
49
+ next = 0;
50
+ else if (event.key === 'End')
51
+ next = list.length - 1;
52
+ else if (step[event.key] !== undefined) {
53
+ next = index + step[event.key];
54
+ next = loop
55
+ ? (next + list.length) % list.length
56
+ : Math.min(Math.max(next, 0), list.length - 1);
57
+ }
58
+ else
59
+ return;
60
+ event.preventDefault();
61
+ sync(list[next]);
62
+ list[next]?.focus();
63
+ }
64
+ function onfocusin(event) {
65
+ const item = event.target.closest(items);
66
+ if (item && container.contains(item) && !disabled(item))
67
+ sync(item);
68
+ }
69
+ // Items come and go (filtering, async data): keep exactly one tab stop.
70
+ const observer = new MutationObserver(() => sync());
71
+ observer.observe(container, {
72
+ childList: true,
73
+ subtree: true,
74
+ attributes: true,
75
+ attributeFilter: ['disabled', 'aria-disabled']
76
+ });
77
+ container.addEventListener('keydown', onkeydown);
78
+ container.addEventListener('focusin', onfocusin);
79
+ sync();
80
+ return () => {
81
+ observer.disconnect();
82
+ container.removeEventListener('keydown', onkeydown);
83
+ container.removeEventListener('focusin', onfocusin);
84
+ };
85
+ };
86
+ }
@@ -0,0 +1,9 @@
1
+ export type FocusableElement = HTMLElement | SVGElement;
2
+ /** Whether `element` can be reached with Tab right now. */
3
+ export declare function isTabbable(element: Element): element is FocusableElement;
4
+ /**
5
+ * The elements inside `container` that Tab reaches, in the order Tab reaches them: positive
6
+ * `tabindex` first, then document order. Like browsers, a radio group contributes one stop,
7
+ * its checked radio or else its first.
8
+ */
9
+ export declare function tabbable(container: ParentNode): FocusableElement[];
@@ -0,0 +1,75 @@
1
+ // Elements that can be focusable at all; isTabbable() then checks the current state.
2
+ const CANDIDATES = [
3
+ 'a[href]',
4
+ 'area[href]',
5
+ 'button',
6
+ 'input:not([type="hidden"])',
7
+ 'select',
8
+ 'textarea',
9
+ 'iframe',
10
+ 'object',
11
+ 'embed',
12
+ 'audio[controls]',
13
+ 'video[controls]',
14
+ 'summary',
15
+ '[contenteditable]:not([contenteditable="false"])',
16
+ '[tabindex]'
17
+ ].join(',');
18
+ /**
19
+ * The element's place in the Tab order. `tabIndex` reports -1 for editing hosts and media
20
+ * controls without a `tabindex` attribute, although Tab reaches them.
21
+ */
22
+ function tabIndexOf(element) {
23
+ if (element.hasAttribute('tabindex'))
24
+ return element.tabIndex;
25
+ const reachable = element instanceof HTMLElement &&
26
+ (element.isContentEditable || element.matches('audio[controls],video[controls]'));
27
+ return reachable ? 0 : element.tabIndex;
28
+ }
29
+ /** Whether `element` can be reached with Tab right now. */
30
+ export function isTabbable(element) {
31
+ if (!(element instanceof HTMLElement || element instanceof SVGElement))
32
+ return false;
33
+ if (tabIndexOf(element) < 0 || element.matches(':disabled') || element.closest('[inert]')) {
34
+ return false;
35
+ }
36
+ // The content of a closed <details> is not rendered, except its own summary.
37
+ const details = element.closest('details:not([open])');
38
+ if (details && !(element.localName === 'summary' && element.parentElement === details)) {
39
+ return false;
40
+ }
41
+ return element.checkVisibility({ checkVisibilityCSS: true, visibilityProperty: true });
42
+ }
43
+ /**
44
+ * The elements inside `container` that Tab reaches, in the order Tab reaches them: positive
45
+ * `tabindex` first, then document order. Like browsers, a radio group contributes one stop,
46
+ * its checked radio or else its first.
47
+ */
48
+ export function tabbable(container) {
49
+ const found = [];
50
+ const radios = new Map();
51
+ for (const element of container.querySelectorAll(CANDIDATES)) {
52
+ if (!isTabbable(element))
53
+ continue;
54
+ if (element instanceof HTMLInputElement && element.type === 'radio' && element.name) {
55
+ const group = `${element.form?.id ?? ''}|${element.name}`;
56
+ const chosen = radios.get(group);
57
+ if (!chosen) {
58
+ radios.set(group, element);
59
+ found.push(element);
60
+ }
61
+ else if (element.checked && !chosen.checked) {
62
+ radios.set(group, element);
63
+ found[found.indexOf(chosen)] = element;
64
+ }
65
+ continue;
66
+ }
67
+ found.push(element);
68
+ }
69
+ // Stable sort: positive tabindex in ascending order, then everything else as it was.
70
+ const rank = (element) => {
71
+ const index = tabIndexOf(element);
72
+ return index > 0 ? index : Infinity;
73
+ };
74
+ return found.sort((a, b) => rank(a) - rank(b));
75
+ }
@@ -0,0 +1,12 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ /**
3
+ * Makes everything outside the element inert (not focusable, not clickable, hidden from
4
+ * assistive technology) while attached, for modal surfaces that are not a `<dialog>`.
5
+ * Live regions stay active so announcements still reach screen readers. Calls stack: an
6
+ * inner modal never reactivates what an outer one made inert, and elements that were inert
7
+ * before stay inert.
8
+ *
9
+ * @example
10
+ * <div role="dialog" aria-modal="true" {@attach open && inertOthers()}>…</div>
11
+ */
12
+ export declare function inertOthers(): Attachment<HTMLElement>;
@@ -0,0 +1,44 @@
1
+ // Elements this module made inert, with how many active calls need them inert.
2
+ const owned = new Map();
3
+ /**
4
+ * Makes everything outside the element inert (not focusable, not clickable, hidden from
5
+ * assistive technology) while attached, for modal surfaces that are not a `<dialog>`.
6
+ * Live regions stay active so announcements still reach screen readers. Calls stack: an
7
+ * inner modal never reactivates what an outer one made inert, and elements that were inert
8
+ * before stay inert.
9
+ *
10
+ * @example
11
+ * <div role="dialog" aria-modal="true" {@attach open && inertOthers()}>…</div>
12
+ */
13
+ export function inertOthers() {
14
+ return (element) => {
15
+ const changed = [];
16
+ for (let node = element; node.parentElement && node !== document.body; node = node.parentElement) {
17
+ for (const sibling of node.parentElement.children) {
18
+ if (sibling === node || sibling.matches('[aria-live]'))
19
+ continue;
20
+ const count = owned.get(sibling);
21
+ if (count !== undefined)
22
+ owned.set(sibling, count + 1);
23
+ else if (sibling.hasAttribute('inert'))
24
+ continue;
25
+ else {
26
+ owned.set(sibling, 1);
27
+ sibling.setAttribute('inert', '');
28
+ }
29
+ changed.push(sibling);
30
+ }
31
+ }
32
+ return () => {
33
+ for (const sibling of changed) {
34
+ const count = (owned.get(sibling) ?? 1) - 1;
35
+ if (count > 0)
36
+ owned.set(sibling, count);
37
+ else {
38
+ owned.delete(sibling);
39
+ sibling.removeAttribute('inert');
40
+ }
41
+ }
42
+ };
43
+ };
44
+ }
@@ -0,0 +1,17 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ /** Runs for a key combination. Return `false` to let the event through unhandled. */
3
+ export type KeyHandler = (event: KeyboardEvent) => void | false;
4
+ /**
5
+ * The combination of an event as `keymap` names it: `Ctrl+Alt+Shift+Meta+Key`. With `rtl`
6
+ * given, horizontal arrows become `ArrowForward` / `ArrowBackward` for that direction.
7
+ */
8
+ export declare function combination(event: KeyboardEvent, rtl?: boolean): string;
9
+ /**
10
+ * Binds keys to handlers on the element. A handled key has its default prevented. Write
11
+ * combinations as `Ctrl+Alt+Shift+Meta+Key` with `event.key` names; `ArrowForward` and
12
+ * `ArrowBackward` follow the writing direction, and physical arrow names work too.
13
+ *
14
+ * @example
15
+ * <div role="slider" {@attach keymap({ ArrowForward: up, ArrowBackward: down, Home: min, End: max })}>
16
+ */
17
+ export declare function keymap(map: Record<string, KeyHandler>): Attachment<HTMLElement>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The combination of an event as `keymap` names it: `Ctrl+Alt+Shift+Meta+Key`. With `rtl`
3
+ * given, horizontal arrows become `ArrowForward` / `ArrowBackward` for that direction.
4
+ */
5
+ export function combination(event, rtl) {
6
+ let key = event.key;
7
+ if (rtl !== undefined && (key === 'ArrowRight' || key === 'ArrowLeft')) {
8
+ key = (key === 'ArrowRight') !== rtl ? 'ArrowForward' : 'ArrowBackward';
9
+ }
10
+ // event.key already carries Shift for characters: "?" rather than "Shift+/".
11
+ const shift = event.shiftKey && key.length > 1;
12
+ return ((event.ctrlKey ? 'Ctrl+' : '') +
13
+ (event.altKey ? 'Alt+' : '') +
14
+ (shift ? 'Shift+' : '') +
15
+ (event.metaKey ? 'Meta+' : '') +
16
+ key);
17
+ }
18
+ /**
19
+ * Binds keys to handlers on the element. A handled key has its default prevented. Write
20
+ * combinations as `Ctrl+Alt+Shift+Meta+Key` with `event.key` names; `ArrowForward` and
21
+ * `ArrowBackward` follow the writing direction, and physical arrow names work too.
22
+ *
23
+ * @example
24
+ * <div role="slider" {@attach keymap({ ArrowForward: up, ArrowBackward: down, Home: min, End: max })}>
25
+ */
26
+ export function keymap(map) {
27
+ return (element) => {
28
+ function onkeydown(event) {
29
+ if (event.defaultPrevented)
30
+ return;
31
+ const rtl = getComputedStyle(element).direction === 'rtl';
32
+ const handler = map[combination(event, rtl)] ?? map[combination(event)];
33
+ if (handler && handler(event) !== false)
34
+ event.preventDefault();
35
+ }
36
+ element.addEventListener('keydown', onkeydown);
37
+ return () => element.removeEventListener('keydown', onkeydown);
38
+ };
39
+ }
@@ -0,0 +1,18 @@
1
+ import type { Attachment } from 'svelte/attachments';
2
+ export interface TypeaheadOptions {
3
+ /** Selector for the items, inside the container. Defaults to `[data-sv-item]`. */
4
+ items?: string;
5
+ /** How long typed characters keep building one search, in ms. Defaults to 500. */
6
+ timeout?: number;
7
+ /** What a match does. Defaults to focusing the item. */
8
+ onMatch?: (item: HTMLElement) => void;
9
+ }
10
+ /**
11
+ * Typing moves focus to the next item whose label starts with what was typed, as menus,
12
+ * listboxes and trees do. Repeating one character cycles through the items starting with it.
13
+ * Labels come from `data-sv-label`, `aria-label`, then the text.
14
+ *
15
+ * @example
16
+ * <ul role="listbox" {@attach rovingFocus({ orientation: 'vertical' })} {@attach typeahead()}>
17
+ */
18
+ export declare function typeahead({ items, timeout, onMatch }?: TypeaheadOptions): Attachment<HTMLElement>;