svelte-detent 0.0.0 → 0.1.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 (63) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +209 -1
  3. package/dist/Detent.svelte +657 -0
  4. package/dist/Detent.svelte.d.ts +25 -0
  5. package/dist/DetentNumber.svelte +125 -0
  6. package/dist/DetentNumber.svelte.d.ts +34 -0
  7. package/dist/DetentText.svelte +103 -0
  8. package/dist/DetentText.svelte.d.ts +30 -0
  9. package/dist/core/engine/engine.d.ts +76 -0
  10. package/dist/core/engine/engine.js +500 -0
  11. package/dist/core/feedback/index.d.ts +36 -0
  12. package/dist/core/feedback/index.js +89 -0
  13. package/dist/core/index.d.ts +11 -0
  14. package/dist/core/index.js +8 -0
  15. package/dist/core/number/controller.d.ts +8 -0
  16. package/dist/core/number/controller.js +208 -0
  17. package/dist/core/number/tokens.d.ts +14 -0
  18. package/dist/core/number/tokens.js +32 -0
  19. package/dist/core/physics/decay.d.ts +12 -0
  20. package/dist/core/physics/decay.js +23 -0
  21. package/dist/core/physics/index.d.ts +8 -0
  22. package/dist/core/physics/index.js +8 -0
  23. package/dist/core/physics/release.d.ts +37 -0
  24. package/dist/core/physics/release.js +62 -0
  25. package/dist/core/physics/rubberband.d.ts +7 -0
  26. package/dist/core/physics/rubberband.js +13 -0
  27. package/dist/core/physics/snap.d.ts +17 -0
  28. package/dist/core/physics/snap.js +18 -0
  29. package/dist/core/physics/spring.d.ts +17 -0
  30. package/dist/core/physics/spring.js +84 -0
  31. package/dist/core/physics/ticks.d.ts +5 -0
  32. package/dist/core/physics/ticks.js +12 -0
  33. package/dist/core/physics/types.d.ts +22 -0
  34. package/dist/core/physics/types.js +1 -0
  35. package/dist/core/physics/velocity.d.ts +14 -0
  36. package/dist/core/physics/velocity.js +42 -0
  37. package/dist/core/presets.d.ts +29 -0
  38. package/dist/core/presets.js +81 -0
  39. package/dist/core/text/controller.d.ts +16 -0
  40. package/dist/core/text/controller.js +163 -0
  41. package/dist/core/text/layout.d.ts +28 -0
  42. package/dist/core/text/layout.js +90 -0
  43. package/dist/core/text/roll.d.ts +57 -0
  44. package/dist/core/text/roll.js +219 -0
  45. package/dist/createDetent.svelte.d.ts +15 -0
  46. package/dist/createDetent.svelte.js +473 -0
  47. package/dist/css.d.ts +16 -0
  48. package/dist/css.js +61 -0
  49. package/dist/elasticRows.svelte.d.ts +11 -0
  50. package/dist/elasticRows.svelte.js +90 -0
  51. package/dist/grip.svelte.d.ts +11 -0
  52. package/dist/grip.svelte.js +75 -0
  53. package/dist/index.d.ts +12 -0
  54. package/dist/index.js +7 -0
  55. package/dist/indicator.svelte.d.ts +88 -0
  56. package/dist/indicator.svelte.js +359 -0
  57. package/dist/props.d.ts +136 -0
  58. package/dist/props.js +1 -0
  59. package/dist/refraction.svelte.d.ts +16 -0
  60. package/dist/refraction.svelte.js +96 -0
  61. package/dist/types.d.ts +120 -0
  62. package/dist/types.js +1 -0
  63. package/package.json +67 -4
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hayder
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.
22
+
23
+ Copyright (c) 2026 Olivier Refalo (Svelte port)
package/README.md CHANGED
@@ -1,3 +1,211 @@
1
1
  # svelte-detent
2
2
 
3
- Name reserved. A Svelte 5 port of [detent](https://github.com/hayderkg/detent). See https://github.com/orefalo/svelte-detent.
3
+ A wheel picker for Svelte 5 that clicks into place.
4
+
5
+ Custom physics (not `scroll-snap`): real inertia, a throw that lands exactly on a row with no visible correction, rubber-banded ends, and frame-rate independent motion at 60, 120 and 144 Hz. Vertical and horizontal, RTL-aware, accessible, no runtime dependencies.
6
+
7
+ **[Live examples →](https://orefalo.github.io/svelte-detent/examples/)** · **[Docs →](https://orefalo.github.io/svelte-detent/docs/)** (30 recipes: iOS pickers, odometers, carousels, radio dials, time zones…)
8
+
9
+ This is an unofficial Svelte 5 port of [detent](https://github.com/hayderkg/detent) by Hayder, which is React-only. The engine, physics and rolling-label controllers come from there, and the examples reproduce [detent.hayder.sh/examples](https://detent.hayder.sh/examples). The conversion was done with Claude Code. All credit for the design belongs to the original author. Thanks, Hayder.
10
+
11
+ > **Status:** pre-release. The API may still change.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ npm i svelte-detent
17
+ ```
18
+
19
+ Requires Svelte 5.29 or newer. SvelteKit and Vite work out of the box, and server rendering is supported.
20
+
21
+ ## Quickstart
22
+
23
+ The styled component looks finished with no styling props (light and dark, a subtle drum, an iOS-style magnifying lens, soft edges). Give it `items` and a `label`, its accessible name:
24
+
25
+ ```svelte
26
+ <script>
27
+ import { Detent } from 'svelte-detent'
28
+
29
+ const minutes = Array.from({ length: 60 }, (_, i) => String(i).padStart(2, '0'))
30
+ let value = $state('30')
31
+ </script>
32
+
33
+ <Detent items={minutes} bind:value label="Minutes" />
34
+ ```
35
+
36
+ Rows can be any markup: `item` is a snippet that receives the item and its state.
37
+
38
+ ```svelte
39
+ <Detent items={['Jan', 'Feb', 'Mar', 'Apr']} bind:value={month} label="Month">
40
+ {#snippet item(name, { selected })}<span class:on={selected}>{name}</span>{/snippet}
41
+ </Detent>
42
+ ```
43
+
44
+ Other snippets: `indicatorContent`, `header`, `footer`, `emptyState`, `loadingRow`. Callbacks: `onChange`, `onTick`, `onSettle`, `onScrollProgress`.
45
+
46
+ ## Headless
47
+
48
+ `createDetent(() => options)` brings the physics, input, keyboard, typeahead and ARIA; the markup and CSS are yours.
49
+
50
+ ```svelte
51
+ <script>
52
+ import { createDetent } from 'svelte-detent'
53
+
54
+ const sizes = ['XS', 'S', 'M', 'L', 'XL']
55
+ const d = createDetent(() => ({ items: sizes, defaultValue: 'M', itemSize: 44, visibleCount: 5, label: 'Size' }))
56
+ </script>
57
+
58
+ <div {...d.rootAttrs} style={d.rootStyle} {@attach d.attachRoot}>
59
+ <div {...d.trackAttrs} style={d.trackStyle} {@attach d.attachTrack}>
60
+ {#each d.rows as r (r.key)}
61
+ <div {...r.attrs} style={r.style}>{r.item}</div>
62
+ {/each}
63
+ </div>
64
+ </div>
65
+ <div {...d.liveRegionAttrs} style={d.liveRegionStyle}>{d.announcement}</div>
66
+ ```
67
+
68
+ Every row gets `--offset`, `--abs-offset`, `--progress` and `--selected` CSS variables, updated every frame outside Svelte's reactivity, so drum effects are plain CSS:
69
+
70
+ ```css
71
+ .row {
72
+ transform: rotateX(calc(var(--offset) * -17deg));
73
+ opacity: calc(1 - var(--abs-offset) * 0.25);
74
+ }
75
+ ```
76
+
77
+ ## Indicator
78
+
79
+ The selection marker is a first-class part: shape, material, style, and fit.
80
+
81
+ ```svelte
82
+ <Detent items={words} label="Word" indicator={{ fit: 'content', shape: 'pill', material: 'glass' }} />
83
+ ```
84
+
85
+ `fit: 'content'` sizes it to the centered item and morphs it as items pass. Shapes: `rounded`, `pill`, `lines`, `underline`, `brackets`, `dot`, `needle`, `none`. Materials (rounded and pill only): `solid`, `glass`, `outline`, `gradient-border`. `color` can be a function of the item. Live values are CSS variables, written every frame: `--indicator-w`, `--indicator-h`, `--indicator-r`, `--indicator-progress`, `--indicator-velocity`.
86
+
87
+ ## Mouse wheels
88
+
89
+ `wheelMode` (default `'auto'`) tells a notched mouse wheel from a trackpad by the first event of a gesture: each notch moves exactly one row, and the accelerated bursts of a fast spin become a throw. Trackpads keep 1:1 tracking with momentum. Use `'smooth'` to follow every delta 1:1, or `'step'` to always step.
90
+
91
+ ## Feedback (opt-in)
92
+
93
+ ```svelte
94
+ <script>
95
+ import { Detent, ticks } from 'svelte-detent'
96
+
97
+ // Create it once: a new handler per render restarts its throttle and audio.
98
+ const onTick = ticks({ haptics: true, sound: { volume: 0.4 } })
99
+ </script>
100
+
101
+ <Detent items={items} label="Value" {onTick} />
102
+ ```
103
+
104
+ Both are no-ops where unsupported (the Vibration API does not exist on iOS Safari). Browsers only allow audio after a user gesture. `sound: { dynamicTimbre: true }` makes fast ticks brighter and slow ones more defined.
105
+
106
+ ## Effects (all opt-in)
107
+
108
+ ```svelte
109
+ <Detent
110
+ items={items}
111
+ label="Value"
112
+ motionBlur
113
+ velocityTilt
114
+ centerLight
115
+ selectedDepth
116
+ elasticRows={0.3}
117
+ grip={0.5}
118
+ indicator={{ refraction: 0.5, refractionRGB: 0.35, impact: true }}
119
+ />
120
+ ```
121
+
122
+ - `motionBlur`: a directional blur along the scroll axis, by velocity.
123
+ - `velocityTilt`: rows lean up to 2° while moving.
124
+ - `centerLight` and `selectedDepth`: rows brighten as they pass the centre, and the centred row comes forward.
125
+ - `elasticRows`: the outer rows lag behind with a spring.
126
+ - `grip`: a short absorption when you catch a moving wheel.
127
+
128
+ `indicator.refraction` bends the visible rows through a stationary lens; `refractionRGB` adds chromatic dispersion. The displacement map is an embedded SVG image, so a strict CSP needs `data:` in `img-src`. Everything turns off with reduced motion. Filtering costs rendering time, so test rich rows on your target mobile devices.
129
+
130
+ ## DetentText
131
+
132
+ Animates short interface labels (a button's state, a counter, a status) with the same spring that settles the wheel.
133
+
134
+ ```svelte
135
+ <script>
136
+ import { DetentText } from 'svelte-detent'
137
+ let status = $state('Send')
138
+ </script>
139
+
140
+ <DetentText text={status} />
141
+ <!-- Send → Sending… → Sent: matching graphemes stay, changed ones roll. -->
142
+
143
+ <DetentText morph={false} text={status} />
144
+ <!-- Roll the entire label instead. -->
145
+ ```
146
+
147
+ - **Native text at rest.** Server rendering emits the real text. While a change rolls, an `aria-hidden` visual copy is drawn over it, so screen readers, selection and hydration only ever see plain text.
148
+ - **No list of values.** Pass the current string or number. Changes are matched grapheme by grapheme.
149
+ - **Interruptible.** A new label mid-roll continues from where each letter is, with no jump.
150
+
151
+ | Prop | Default | Purpose |
152
+ | ---------------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------- |
153
+ | `text` | required | Current string or number |
154
+ | `morph` | `true` | Keep matching graphemes; `false` rolls the whole label |
155
+ | `reveal` | `false` | New letters grow in from zero width |
156
+ | `animate` | `true` | `false` for immediate updates |
157
+ | `spring` | `{ stiffness: 420, damping: 32, mass: 1 }` | Partial spring configuration |
158
+ | `snapDuration` | `550` | Maximum spring duration, in ms |
159
+ | `rollDirection` | `'up'` | `'up'` or `'down'` |
160
+ | `locale` | browser default | Grapheme segmentation locale |
161
+ | `as` | `'span'` | Element: `span`, `strong`, `output`, `code`, a heading… |
162
+ | `ref` | | Bindable root element |
163
+ | `onAnimationStart` / `onAnimationComplete` / `onAnimationCancel` | | Transition callbacks |
164
+
165
+ `class`, `style` (a string or an object) and any HTML or ARIA attribute are passed to the element. `position: static` becomes `relative` and `display: inline` becomes `inline-block`, because the rolling layer needs a box of its own. Targets short, single-line LTR text; other text updates natively without rolling. Reduced motion is always respected.
166
+
167
+ ## DetentNumber
168
+
169
+ A formatted number whose digits roll on the same spring, through the values between, like an odometer. Digits are matched by decimal place, so the units stay the units from 99 to 100, through a change of sign or of locale.
170
+
171
+ ```svelte
172
+ <script>
173
+ import { DetentNumber } from 'svelte-detent'
174
+ </script>
175
+
176
+ <DetentNumber value={amount} locales="es-ES" format={{ style: 'currency', currency: 'EUR' }} />
177
+ ```
178
+
179
+ | Prop | Default | Purpose |
180
+ | ----------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------ |
181
+ | `value` | required | A `number` or `bigint` |
182
+ | `locales` / `format` | | Passed straight to `Intl.NumberFormat` |
183
+ | `trend` | `'auto'` | `'auto'` rolls up when the value grows and down when it shrinks; `'up'` or `'down'` always |
184
+ | `classNames` | | A class per part: `integer`, `fraction`, `decimal`, `group`, `currency`, `sign`, `unit` |
185
+ | `animate`, `spring`, `snapDuration`, `as`, `ref`, callbacks | | As in `DetentText` (`spring` default is `{ stiffness: 460, damping: 38, mass: 1 }`) |
186
+
187
+ To style the parts (a quieter currency, smaller decimals), pass `classNames` and keep the parts on the baseline: each digit rolls through a window the height of the line, so a part raised with `vertical-align` would be cut off while it moves.
188
+
189
+ ```svelte
190
+ <DetentNumber value={price} format={{ style: 'currency', currency: 'USD' }}
191
+ classNames={{ currency: 'cur', fraction: 'cents', decimal: 'cents' }} />
192
+ ```
193
+
194
+ Values that format the same do not animate. Pass an explicit `locales` when server rendering so server and browser agree.
195
+
196
+ ## Develop
197
+
198
+ ```sh
199
+ pnpm install
200
+ pnpm dev # the examples at http://localhost:5173/examples/
201
+ pnpm test # engine and physics, components, server rendering
202
+ pnpm check # svelte-check
203
+ pnpm build # the package into dist/
204
+ pnpm build:demo # the examples site into demo-dist/
205
+ ```
206
+
207
+ The framework-free engine lives in `src/lib/core`, the Svelte layer in `src/lib`, and the examples and docs site in `demo/` (the docs are `demo/docs/docs.md`). Opening `/?e=Carousel` mounts a single example on its own.
208
+
209
+ ## License
210
+
211
+ [MIT](./LICENSE). Original work © Hayder; Svelte port © Olivier Refalo.