split-flap-board 0.0.4 → 0.0.5

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/README.md CHANGED
@@ -17,67 +17,154 @@
17
17
  </a>
18
18
  </p>
19
19
 
20
- Web component that simulates a split-flap display, the mechanical boards found in airports and train stations. Built with [Lit](https://lit.dev/), works in any framework or plain HTML.
20
+ Web component library for split-flap displays, the mechanical boards found in airports and train stations. Built with [Lit](https://lit.dev/), it works in any framework or plain HTML.
21
+
22
+ - **Simple mental model**: flap -> spool -> board
23
+ - **Framework friendly**: works in plain HTML and can be used from React or other frameworks
24
+ - **Flexible content**: supports character, color, image, and custom flaps
25
+ - **Two built-in looks**: `minimal` and `realistic`
21
26
 
22
27
  ## How a Split-Flap Display Works
23
28
 
24
29
  > [How a Split-Flap Display Works (YouTube)](https://www.youtube.com/watch?v=UAQJJAQSg_g)
25
30
 
26
- A split-flap display (also called a "Solari board") works through purely mechanical means:
31
+ A split-flap display, also called a Solari board, works through a simple mechanical loop:
27
32
 
28
- - A **spool** (drum) holds a series of **flaps** (thin cards), each printed with the top half of one character on the front and the bottom half of a different character on the back.
33
+ - A **spool** (drum) holds a series of **flaps** (thin cards), each printed with the top half of one character on the front and the bottom half of another on the back.
29
34
  - A **stepper motor** rotates the spool precisely. As each flap passes vertical, gravity pulls it down, snapping it against a **backstop** and creating the characteristic clacking sound.
30
- - This reveal happens one flap at a time, so going from `A` to `Z` means cycling through every character in between. The order of the flaps on the spool is fixed at manufacture.
31
- - A **hall effect sensor** and magnet on the spool give the controller a consistent home position, so it always knows which character is showing even after a power cycle.
35
+ - This reveal happens one flap at a time, so going from `A` to `Z` means cycling through every character in between. The order of the flaps on the spool is fixed.
36
+ - A **hall effect sensor** and magnet give the controller a consistent home position, so it always knows which character is showing even after a power cycle.
37
+
38
+ In code, each `<split-flap-spool>` mirrors that behavior: it holds an ordered sequence of **flaps** and steps forward through them until it reaches a target key.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pnpm add split-flap-board
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ ### Board
32
49
 
33
- In code, each `<split-flap-spool>` mirrors this: it holds a sequence of **flaps** and steps forward through them to reach a target key, never backward.
50
+ ```html
51
+ <script type="module">
52
+ import 'split-flap-board';
53
+ </script>
54
+
55
+ <split-flap-board></split-flap-board>
56
+ ```
57
+
58
+ ```js
59
+ import { fromLines } from 'split-flap-board';
60
+
61
+ const board = document.querySelector('split-flap-board');
62
+ const { spools, grid } = fromLines(['HELLO WORLD'], 11);
63
+
64
+ board.spools = spools;
65
+ board.grid = grid;
66
+ ```
67
+
68
+ For most apps, this is the easiest way to start:
69
+
70
+ 1. build `spools` once
71
+ 2. update `grid` whenever the displayed text changes
72
+
73
+ ### Single Spool
74
+
75
+ ```html
76
+ <script type="module">
77
+ import 'split-flap-board';
78
+ </script>
79
+
80
+ <split-flap-spool value="A"></split-flap-spool>
81
+ ```
82
+
83
+ ```js
84
+ const spool = document.querySelector('split-flap-spool');
85
+ spool.value = 'Z'; // steps forward: A -> B -> ... -> Z
86
+ ```
87
+
88
+ Switch to the realistic look:
89
+
90
+ ```html
91
+ <split-flap-spool variant="realistic" value="A"></split-flap-spool>
92
+ ```
34
93
 
35
94
  ## Core Concepts
36
95
 
37
96
  ### Flap
38
97
 
39
- A flap is one card on the spool, the atomic unit of content. Four built-in types:
98
+ A flap is one card on the spool, the smallest unit of display content. The library ships with four built-in flap types:
40
99
 
41
100
  ```ts
42
101
  // Character (default)
43
- { type: 'char'; value: string; color?: string; bg?: string; fontSize?: string; fontFamily?: string; fontWeight?: string }
102
+ {
103
+ type: 'char';
104
+ key?: string;
105
+ value: string;
106
+ color?: string;
107
+ bg?: string;
108
+ fontSize?: string;
109
+ fontFamily?: string;
110
+ fontWeight?: string;
111
+ }
44
112
 
45
113
  // Solid color
46
- { type: 'color'; value: string } // any CSS color
114
+ {
115
+ type: 'color';
116
+ key?: string;
117
+ value: string;
118
+ }
47
119
 
48
120
  // Image
49
- { type: 'image'; src: string; alt?: string }
121
+ {
122
+ type: 'image';
123
+ key?: string;
124
+ src: string;
125
+ alt?: string;
126
+ }
50
127
 
51
- // Custom - top and bottom halves rendered independently (Lit TemplateResult)
52
- { type: 'custom'; key: string; top: TemplateResult; bottom: TemplateResult }
128
+ // Custom, top and bottom halves rendered independently
129
+ {
130
+ type: 'custom';
131
+ key: string;
132
+ top: TemplateResult;
133
+ bottom: TemplateResult;
134
+ }
53
135
  ```
54
136
 
55
- The `key` field is optional on all types except `custom`. When omitted it defaults to the natural identifier: `value` for char and color, `src` for image.
137
+ The `key` field is optional on all types except `custom`. When omitted, the library uses the natural identifier:
56
138
 
57
- Char flaps default to `2rem` monospace bold. Override per-flap via the object, or rebuild the spool with a new `fontSize` to change all at once:
139
+ - `value` for `char` and `color`
140
+ - `src` for `image`
141
+
142
+ Char flaps can be styled per flap when needed:
58
143
 
59
144
  ```ts
60
- const bigSpool = charSpool.map((f) =>
61
- f.type === 'char' ? { ...f, fontSize: '3rem', fontWeight: '400' } : f
145
+ const styledSpool = charSpool.map((flap) =>
146
+ flap.type === 'char' ? { ...flap, fontSize: '3rem', color: '#fff', bg: '#2563eb' } : flap
62
147
  );
63
- spool.flaps = bigSpool;
148
+
149
+ spool.flaps = styledSpool;
64
150
  ```
65
151
 
66
152
  ### Spool
67
153
 
68
- A spool is an ordered array of flaps, the sequence a `<split-flap-spool>` steps through. Define it once, reference it anywhere.
154
+ A spool is an ordered array of flaps, the sequence a `<split-flap-spool>` steps through. Define it once and reuse it anywhere.
69
155
 
70
156
  ```ts
71
157
  type TSpool = TFlap[];
72
158
  ```
73
159
 
74
- The library ships built-in spools:
160
+ The library ships with built-in spools:
75
161
 
76
162
  ```ts
77
- import { charSpool, numericSpool } from 'split-flap-board';
163
+ import { charSpool, colorSpool, numericSpool } from 'split-flap-board';
78
164
 
79
- // charSpool [' ', A-Z, 0-9, . - / :]
80
- // numericSpool [' ', 0-9]
165
+ // charSpool -> [' ', A-Z, 0-9, . - / : ]
166
+ // numericSpool -> [' ', 0-9]
167
+ // colorSpool -> named color keys such as 'red', 'green', 'blue'
81
168
  ```
82
169
 
83
170
  Custom spools are just arrays:
@@ -91,7 +178,7 @@ const statusSpool: TSpool = [
91
178
  ];
92
179
  ```
93
180
 
94
- You can mix flap types within a spool:
181
+ You can also mix flap types within a single spool:
95
182
 
96
183
  ```ts
97
184
  const mixedSpool: TSpool = [
@@ -102,12 +189,17 @@ const mixedSpool: TSpool = [
102
189
  ];
103
190
  ```
104
191
 
105
- ### Spools grid vs. target grid
192
+ ### Spools Grid vs. Target Grid
106
193
 
107
194
  A board has two separate grids:
108
195
 
109
- - **`spools`** - a `TSpool[][]` defining what each spool unit CAN show. Typically set once at init.
110
- - **`grid`** - a `string[][]` of target keys defining what each unit SHOWS right now. Updated freely at runtime.
196
+ - **`spools`**: a `TSpool[][]` that defines what each cell can show. This is usually set once.
197
+ - **`grid`**: a `string[][]` of target keys that defines what each cell should show right now. This is the part you usually update at runtime.
198
+
199
+ If you are unsure which one to change, use this rule of thumb:
200
+
201
+ - change `spools` when the available flap set changes
202
+ - change `grid` when the displayed content changes
111
203
 
112
204
  ```ts
113
205
  import { charSpool, spoolGrid } from 'split-flap-board';
@@ -115,10 +207,10 @@ import { charSpool, spoolGrid } from 'split-flap-board';
115
207
  // spoolGrid(spool, cols, rows) fills a uniform TSpool[][]
116
208
  board.spools = spoolGrid(charSpool, 10, 3);
117
209
 
118
- // per-column: pass an array of spools, one per column (shorter arrays repeat)
210
+ // Per-column: pass an array of spools, one per column. Shorter arrays repeat.
119
211
  board.spools = spoolGrid([charSpool, charSpool, statusSpool], 3, 2);
120
212
 
121
- // fully custom: build the 2D array directly
213
+ // Fully custom: build the 2D array directly.
122
214
  board.spools = [
123
215
  [charSpool, charSpool, statusSpool],
124
216
  [charSpool, charSpool, statusSpool]
@@ -135,65 +227,16 @@ Board dimensions are inferred from `spools`.
135
227
 
136
228
  ## Usage
137
229
 
138
- ### Single spool
139
-
140
- ```html
141
- <script type="module">
142
- import 'split-flap-board';
143
- </script>
144
-
145
- <split-flap-spool value="A"></split-flap-spool>
146
- ```
147
-
148
- ```js
149
- const spool = document.querySelector('split-flap-spool');
150
- spool.value = 'Z'; // steps forward: A → B → ... → Z
151
- ```
152
-
153
- Switch to the realistic look:
154
-
155
- ```html
156
- <split-flap-spool variant="realistic" value="A"></split-flap-spool>
157
- ```
230
+ ### Updating at Runtime
158
231
 
159
- Custom spool:
232
+ For content changes, only `grid` needs to change. Assign a new array reference:
160
233
 
161
234
  ```js
162
- import { statusSpool } from './my-spools';
163
-
164
- spool.flaps = statusSpool;
165
- spool.value = 'green';
166
- ```
167
-
168
- ### Board
169
-
170
- ```html
171
- <script type="module">
172
- import 'split-flap-board';
173
- </script>
174
-
175
- <split-flap-board></split-flap-board>
176
- ```
177
-
178
- ```js
179
- import { fromLines } from 'split-flap-board';
180
-
181
- const board = document.querySelector('split-flap-board');
182
- const { spools, grid } = fromLines(['HELLO WORLD'], 11);
183
- board.spools = spools;
184
- board.grid = grid;
185
- ```
186
-
187
- ### Updating at runtime
188
-
189
- Only `grid` needs to change for content updates. Assign a new array reference:
190
-
191
- ```js
192
- // refresh content - spools stay the same
235
+ // Refresh content, spools stay the same.
193
236
  board.grid = [['G', 'O', 'O', 'D', 'B', 'Y', 'E', ' ', ' ', ' ', ' ']];
194
237
  ```
195
238
 
196
- ### Multi-row board
239
+ ### Multi-Row Board
197
240
 
198
241
  ```js
199
242
  import { fromLines } from 'split-flap-board';
@@ -207,7 +250,20 @@ board.spools = spools;
207
250
  board.grid = grid;
208
251
  ```
209
252
 
210
- ### Mixed spools per column
253
+ ### Custom Spool
254
+
255
+ ```js
256
+ const statusSpool = [
257
+ { type: 'color', value: '#111', key: 'off' },
258
+ { type: 'color', value: '#16a34a', key: 'green' },
259
+ { type: 'color', value: '#dc2626', key: 'red' }
260
+ ];
261
+
262
+ spool.flaps = statusSpool;
263
+ spool.value = 'green';
264
+ ```
265
+
266
+ ### Mixed Spools per Column
211
267
 
212
268
  ```js
213
269
  import { charSpool, spoolGrid } from 'split-flap-board';
@@ -226,7 +282,7 @@ board.grid = [
226
282
  ];
227
283
  ```
228
284
 
229
- ### Colored rows
285
+ ### Colored Rows
230
286
 
231
287
  ```js
232
288
  import { fromLines } from 'split-flap-board';
@@ -244,6 +300,8 @@ board.spools = spools;
244
300
  board.grid = grid;
245
301
  ```
246
302
 
303
+ Use row colors when you want text-style boards with a highlighted row, for example boarding status or delays.
304
+
247
305
  ### React
248
306
 
249
307
  ```tsx
@@ -264,6 +322,7 @@ export function DeparturesBoard() {
264
322
 
265
323
  useEffect(() => {
266
324
  if (ref.current == null) return;
325
+
267
326
  const { spools, grid } = fromLines(['DEPARTURES'], 10);
268
327
  (ref.current as any).spools = spools;
269
328
  (ref.current as any).grid = grid;
@@ -273,6 +332,8 @@ export function DeparturesBoard() {
273
332
  }
274
333
  ```
275
334
 
335
+ For richer framework integrations, the simplest approach is usually to keep `spools` stable and only update `grid`.
336
+
276
337
  ## API Reference
277
338
 
278
339
  ### `<split-flap-spool>`
@@ -298,7 +359,7 @@ The variant elements can also be used directly if you prefer not to use the wrap
298
359
 
299
360
  | Property | Type | Default | Description |
300
361
  | ------------------ | -------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------- |
301
- | `spools` | `TSpool[][]` | | 2D spool configuration, one per cell. Board dimensions are inferred from this. Always assign a new array reference to update. |
362
+ | `spools` | `TSpool[][]` | `[]` | 2D spool configuration, one per cell. Board dimensions are inferred from this. Always assign a new array reference to update. |
302
363
  | `grid` | `string[][]` | `[]` | 2D array of target keys. Always assign a new array reference to trigger a re-render. |
303
364
  | `speed` | `number` | `60` | Flip speed in milliseconds forwarded to every child spool. |
304
365
  | `variant` | `'minimal' \| 'realistic'` | `'minimal'` | Visual variant forwarded to every child spool. |
@@ -333,10 +394,10 @@ function spoolGrid(spool: TSpool | TSpool[], cols: number, rows: number): TSpool
333
394
  Creates a `TSpool[][]` for use with `board.spools`.
334
395
 
335
396
  ```ts
336
- // Uniform - same spool for every cell
397
+ // Uniform, same spool for every cell
337
398
  spoolGrid(charSpool, 10, 3);
338
399
 
339
- // Per-column - pass an array where index = column; shorter arrays repeat
400
+ // Per-column, pass an array where index = column. Shorter arrays repeat.
340
401
  spoolGrid([charSpool, charSpool, statusSpool], 3, 2);
341
402
  ```
342
403
 
@@ -349,7 +410,7 @@ function fromLines(
349
410
  ): { spools: TSpool[][]; grid: string[][] };
350
411
  ```
351
412
 
352
- Creates a char `spools` grid and a `grid` of target keys from an array of strings. Each string is one row, padded with spaces or truncated to `cols`. Rows with `bg`/`color` get those values baked into their char flaps.
413
+ Creates a char `spools` grid and a `grid` of target keys from an array of lines. Each line is uppercased, padded with spaces, or truncated to `cols`. Rows with `bg` or `color` get those values baked into their char flaps.
353
414
 
354
415
  ```ts
355
416
  const { spools, grid } = fromLines(
@@ -363,7 +424,7 @@ board.grid = grid;
363
424
 
364
425
  ### CSS Custom Properties
365
426
 
366
- Set these on the board to theme all spools at once, or override on individual spools via CSS selectors.
427
+ Set these on the board to theme all spools at once, or override them on individual spools via CSS selectors.
367
428
 
368
429
  ```css
369
430
  /* Board panel */
@@ -374,7 +435,7 @@ split-flap-board {
374
435
  --sfb-gap: 3px; /* gap between spool cells */
375
436
  }
376
437
 
377
- /* Shared (flap) */
438
+ /* Shared flap styles */
378
439
  split-flap-board {
379
440
  --sfb-flap-bg: #111; /* flap background */
380
441
  --sfb-flap-color: #f5f0e0; /* flap text color */
@@ -390,14 +451,14 @@ split-flap-board {
390
451
 
391
452
  /* Realistic variant */
392
453
  split-flap-board {
393
- --sfb-spool-width: 1em; /* flap width; defaults to font-size */
394
- --sfb-spool-height: 2em; /* flap height; defaults to font-size */
395
- --sfb-drum-radius: 0px; /* cylinder radius; 0 keeps the flip flat */
454
+ --sfb-spool-width: 1em; /* flap width, defaults to 1x font-size */
455
+ --sfb-spool-height: 2em; /* flap height, defaults to 2x font-size */
456
+ --sfb-drum-radius: 0px; /* cylinder radius, 0 keeps the flip flat */
396
457
  --sfb-crease: 1px; /* gap between the two flap halves */
397
458
  --sfb-perspective: 400px; /* CSS perspective depth */
398
459
  --sfb-view-transform: none; /* e.g. rotateY(-30deg) */
399
- --sfb-max-step-angle: 1turn; /* per-step angle cap; 8deg tightens small spools */
400
- --sfb-flap-border: #2a2a2a; /* border on each flap card; separates adjacent cells */
460
+ --sfb-max-step-angle: 1turn; /* per-step angle cap, 8deg tightens small spools */
461
+ --sfb-flap-border: #2a2a2a; /* border on each flap card */
401
462
  }
402
463
 
403
464
  /* Per-spool override */
@@ -409,27 +470,27 @@ split-flap-spool.highlight {
409
470
 
410
471
  ## Behavior
411
472
 
412
- ### Initial state
473
+ ### Initial State
413
474
 
414
- Before `value` is set, a `<split-flap-spool>` shows the first flap in its sequence (index 0). For `charSpool` that is a space. This mirrors the physical home position the hall effect sensor establishes on power-up.
475
+ Before `value` is set, a `<split-flap-spool>` shows the first flap in its sequence. For `charSpool`, that is a space. This mirrors the physical home position a real board establishes on startup.
415
476
 
416
477
  ### Animation
417
478
 
418
- Each flap step plays a fold animation where the top half falls away, revealing the next card underneath. The animation duration is derived from `speed` so it always fits within one step interval. No separate property is needed.
479
+ Each flap step plays a fold animation where the top half falls away and reveals the next card underneath. The animation duration is derived from `speed`, so it always fits inside one step interval.
419
480
 
420
- ### Unknown key
481
+ ### Unknown Key
421
482
 
422
- If `value` is set to a key that does not exist in `flaps`, the spool does not start a new search and no error is thrown. If that happens during an in-flight animation, the current flip finishes and `settled` reports the flap the spool actually landed on.
483
+ If `value` is set to a key that does not exist in `flaps`, the spool does not start a new search and no error is thrown. If this happens during an in-flight animation, the current flip finishes and `settled` reports the flap the spool actually landed on.
423
484
 
424
- ### Retargeting during motion
485
+ ### Retargeting During Motion
425
486
 
426
487
  If `value` changes to another valid key while the spool is already moving, the spool keeps its current forward motion and retargets to the newest valid key. It does not snap backward or restart from the beginning.
427
488
 
428
- ### Spool changes during motion
489
+ ### Spool Changes During Motion
429
490
 
430
491
  If `flaps` changes while the spool is moving, the component remaps the currently visible flap by key into the new spool, clears stale animation bookkeeping, and continues from the new coherent state.
431
492
 
432
- ### Grid size mismatch
493
+ ### Grid Size Mismatch
433
494
 
434
495
  If `grid` has more rows or columns than `spools`, the extra entries are ignored. If `grid` is smaller than `spools`, spools without a matching target key stay on their current flap. No errors are thrown.
435
496
 
@@ -438,8 +499,8 @@ If `grid` has more rows or columns than `spools`, the extra entries are ignored.
438
499
  Because a spool only rotates forward, the number of steps depends on the distance ahead in the spool, wrapping around if needed.
439
500
 
440
501
  ```
441
- 'A' 'C' = 2 steps
442
- 'Z' 'B' = 3 steps (wraps: Z ' ' A B)
502
+ 'A' -> 'C' = 2 steps
503
+ 'Z' -> 'B' = 3 steps (wraps: Z -> ' ' -> A -> B)
443
504
  ```
444
505
 
445
506
  This applies to all flap types. Keep the order of your spool in mind when designing update sequences. The closer two keys are in the spool, the faster the transition.
@@ -449,4 +510,3 @@ This applies to all flap types. Keep the order of your spool in mind when design
449
510
  - [How a Split-Flap Display Works (YouTube)](https://www.youtube.com/watch?v=UAQJJAQSg_g)
450
511
  - [Lit](https://lit.dev/)
451
512
  - [Scott Bezek's open-source split-flap hardware](https://github.com/scottbez1/splitflap)
452
- - [@ybhrdwj on X](https://x.com/ybhrdwj/status/2037110274696896687) - the tweet that started this :)
@@ -1,10 +1,10 @@
1
- "use strict";var p=require("lit"),i=require("lit/decorators.js");require("./spools/SplitFlapSpool.js");var n=Object.defineProperty,b=Object.getOwnPropertyDescriptor,l=(d,e,r,s)=>{for(var t=s>1?void 0:s?b(e,r):e,o=d.length-1,a;o>=0;o--)(a=d[o])&&(t=(s?a(e,r,t):a(t))||t);return s&&t&&n(e,r,t),t};exports.SplitFlapBoard=class extends p.LitElement{constructor(){super(...arguments),this.spools=[],this.grid=[],this.speed=60,this.variant="minimal",this.visibleSideCount=-1,this._pendingSettle=!1}updated(e){super.updated(e),(e.has("spools")||e.has("grid"))&&(this._pendingSettle=!0,Promise.resolve().then(()=>this._checkAllSettled()))}_getSpoolEls(){return Array.from(this.renderRoot.querySelectorAll("split-flap-spool"))}_checkAllSettled(){if(!this._pendingSettle)return;const e=this._getSpoolEls();e.length!==0&&e.every(r=>r.isSettled)&&(this._pendingSettle=!1,this._dispatchBoardSettled(e))}_dispatchBoardSettled(e){let r=0;const s=this.spools.map(t=>t.map(()=>{var o,a;return(a=(o=e[r++])==null?void 0:o.currentValue)!=null?a:""}));this.dispatchEvent(new CustomEvent("board-settled",{detail:{grid:s},bubbles:!0,composed:!0}))}render(){return p.html`
2
- ${this.spools.map((e,r)=>p.html`
3
- <div class="board-row" style="z-index: ${r+1}">
4
- ${e.map((s,t)=>{var o,a;return p.html`
1
+ "use strict";var l=require("lit"),p=require("lit/decorators.js"),n=Object.defineProperty,u=Object.getOwnPropertyDescriptor,a=(d,t,e,s)=>{for(var r=s>1?void 0:s?u(t,e):t,o=d.length-1,i;o>=0;o--)(i=d[o])&&(r=(s?i(t,e,r):i(r))||r);return s&&r&&n(t,e,r),r};exports.SplitFlapBoard=class extends l.LitElement{constructor(){super(...arguments),this.spools=[],this.grid=[],this.speed=60,this.variant="minimal",this.visibleSideCount=-1,this._pendingSettle=!1}updated(t){super.updated(t),(t.has("spools")||t.has("grid"))&&(this._pendingSettle=!0,queueMicrotask(()=>this._checkAllSettled()))}_getSpoolEls(){return Array.from(this.renderRoot.querySelectorAll("split-flap-spool"))}_checkAllSettled(){if(!this._pendingSettle)return;const t=this._getSpoolEls();if(t.length===0){this._pendingSettle=!1,this._dispatchBoardSettled([]);return}t.every(e=>e.isSettled)&&(this._pendingSettle=!1,this._dispatchBoardSettled(t))}_dispatchBoardSettled(t){const e=this._getCurrentGrid(t);this.dispatchEvent(new CustomEvent("board-settled",{detail:{grid:e},bubbles:!0,composed:!0}))}_getCurrentGrid(t){let e=0;return this.spools.map(s=>s.map(()=>{var r,o;const i=(o=(r=t[e])==null?void 0:r.currentValue)!=null?o:"";return e+=1,i}))}render(){return l.html`
2
+ ${this.spools.map((t,e)=>l.html`
3
+ <div class="board-row" style="z-index: ${e+1}">
4
+ ${t.map((s,r)=>{var o,i;return l.html`
5
5
  <split-flap-spool
6
6
  .flaps=${s}
7
- .value=${(a=(o=this.grid[r])==null?void 0:o[t])!=null?a:""}
7
+ .value=${(i=(o=this.grid[e])==null?void 0:o[r])!=null?i:""}
8
8
  .speed=${this.speed}
9
9
  .variant=${this.variant}
10
10
  .visibleSideCount=${this.visibleSideCount}
@@ -13,14 +13,12 @@
13
13
  `})}
14
14
  </div>
15
15
  `)}
16
- `}},exports.SplitFlapBoard.styles=p.css`
16
+ `}},exports.SplitFlapBoard.styles=l.css`
17
17
  :host {
18
18
  display: inline-flex;
19
19
  position: relative;
20
20
  flex-direction: column;
21
21
  box-sizing: border-box;
22
- /* Inset shadows for side rails; render below all children so frame bars
23
- * appear at the same visual level. */
24
22
  box-shadow:
25
23
  inset 14px 0 18px rgba(0, 0, 0, 0.55),
26
24
  inset -14px 0 18px rgba(0, 0, 0, 0.55);
@@ -29,7 +27,6 @@
29
27
  padding: var(--sfb-board-padding, 10px);
30
28
  }
31
29
 
32
- /* Absolute overlay so the outer frame ring adds no layout space. */
33
30
  :host::after {
34
31
  position: absolute;
35
32
  z-index: 10000;
@@ -40,8 +37,6 @@
40
37
  content: '';
41
38
  }
42
39
 
43
- /* Frame bar caps the top padding of each row. z-index increases per row in
44
- * render() so each bar sits above the drum content of the row above it. */
45
40
  .board-row {
46
41
  display: flex;
47
42
  position: relative;
@@ -58,4 +53,4 @@
58
53
  height: 10px;
59
54
  content: '';
60
55
  }
61
- `,l([i.property({type:Array})],exports.SplitFlapBoard.prototype,"spools",2),l([i.property({type:Array})],exports.SplitFlapBoard.prototype,"grid",2),l([i.property({type:Number})],exports.SplitFlapBoard.prototype,"speed",2),l([i.property({type:String})],exports.SplitFlapBoard.prototype,"variant",2),l([i.property({type:Number})],exports.SplitFlapBoard.prototype,"visibleSideCount",2),exports.SplitFlapBoard=l([i.customElement("split-flap-board")],exports.SplitFlapBoard);
56
+ `,a([p.property({type:Array})],exports.SplitFlapBoard.prototype,"spools",2),a([p.property({type:Array})],exports.SplitFlapBoard.prototype,"grid",2),a([p.property({type:Number})],exports.SplitFlapBoard.prototype,"speed",2),a([p.property({type:String})],exports.SplitFlapBoard.prototype,"variant",2),a([p.property({type:Number})],exports.SplitFlapBoard.prototype,"visibleSideCount",2),exports.SplitFlapBoard=a([p.customElement("split-flap-board")],exports.SplitFlapBoard);
@@ -1 +1 @@
1
- "use strict";var i=require("../spools/presets.js"),g=Object.defineProperty,h=Object.getOwnPropertySymbols,m=Object.prototype.hasOwnProperty,v=Object.prototype.propertyIsEnumerable,y=(t,r,e)=>r in t?g(t,r,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[r]=e,u=(t,r)=>{for(var e in r||(r={}))m.call(r,e)&&y(t,e,r[e]);if(h)for(var e of h(r))v.call(r,e)&&y(t,e,r[e]);return t};function d(t,r,e){const o=t.length>0&&Array.isArray(t[0])?t:Array.from({length:r},()=>t);return Array.from({length:e},()=>Array.from({length:r},(l,a)=>{var p;return(p=o[a%o.length])!=null?p:i.charSpool}))}function A(t,r){const e=[],o=[];for(const l of t){const a=typeof l=="object",p=a?l.text:l,n=a?l.bg:void 0,s=a?l.color:void 0,f=p.toUpperCase().padEnd(r," ").slice(0,r).split("");if(n!=null||s!=null){const b=f.map(()=>i.charSpool.map(c=>c.type!=="char"?c:u(u(u({},c),n!=null?{bg:n}:{}),s!=null?{color:s}:{})));e.push(b)}else e.push(Array.from({length:r},()=>i.charSpool));o.push(f)}return{spools:e,grid:o}}exports.fromLines=A,exports.spoolGrid=d;
1
+ "use strict";var u=require("../spools/presets.js"),y=Object.defineProperty,i=Object.getOwnPropertySymbols,b=Object.prototype.hasOwnProperty,g=Object.prototype.propertyIsEnumerable,s=(r,t,e)=>t in r?y(r,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):r[t]=e,c=(r,t)=>{for(var e in t||(t={}))b.call(t,e)&&s(r,e,t[e]);if(i)for(var e of i(t))g.call(t,e)&&s(r,e,t[e]);return r};function h(r,t,e){const o=v(r)?r:[r];return Array.from({length:e},()=>Array.from({length:t},(a,l)=>{var n;return(n=o[l%o.length])!=null?n:u.charSpool}))}function m(r,t){const e=[],o=[];for(const a of r){const l=O(a),n=A(l.text,t).split(""),f=l.bg!=null||l.color!=null;e.push(f?S(t,l):p(u.charSpool,t)),o.push(n)}return{spools:e,grid:o}}function v(r){return Array.isArray(r[0])}function p(r,t){return Array.from({length:t},()=>r)}function d(r,t){return r.type!=="char"?r:c(c(c({},r),t.bg!=null?{bg:t.bg}:{}),t.color!=null?{color:t.color}:{})}function O(r){return typeof r=="string"?{text:r}:r}function A(r,t){return r.toUpperCase().padEnd(t," ").slice(0,t)}function S(r,t){const e=u.charSpool.map(o=>d(o,t));return p(e,r)}exports.fromLines=m,exports.spoolGrid=h;
@@ -1 +1 @@
1
- "use strict";function r(e){if(e.key!=null)return e.key;switch(e.type){case"char":case"color":return e.value;case"image":return e.src;case"custom":return e.key}}exports.getFlapKey=r;
1
+ "use strict";function c(e){var r,t;switch(e.type){case"char":case"color":return(r=e.key)!=null?r:e.value;case"image":return(t=e.key)!=null?t:e.src;case"custom":return e.key}}exports.getFlapKey=c;
@@ -1,4 +1,4 @@
1
- "use strict";var o=require("lit"),r=require("lit/decorators.js"),S=require("./presets.js"),n=require("../lib/flap.js");require("./SplitFlapSpoolMinimal.js"),require("./SplitFlapSpoolRealistic.js");var v=Object.defineProperty,h=Object.getOwnPropertyDescriptor,i=(s,t,e,p)=>{for(var l=p>1?void 0:p?h(t,e):t,a=s.length-1,u;a>=0;a--)(u=s[a])&&(l=(p?u(t,e,l):u(l))||l);return p&&l&&v(t,e,l),l};exports.SplitFlapSpool=class extends o.LitElement{constructor(){super(...arguments),this.variant="minimal",this.value=" ",this.flaps=S.charSpool,this.speed=60,this.visibleSideCount=-1}get currentValue(){var t,e;return(e=(t=this._getActiveSpool())==null?void 0:t.currentValue)!=null?e:this.flaps[0]!=null?n.getFlapKey(this.flaps[0]):void 0}get isSettled(){var t,e;return(e=(t=this._getActiveSpool())==null?void 0:t.isSettled)!=null?e:!0}hasKey(t){var e,p;return(p=(e=this._getActiveSpool())==null?void 0:e.hasKey(t))!=null?p:this.flaps.some(l=>n.getFlapKey(l)===t)}_getActiveSpool(){return this.renderRoot.querySelector("split-flap-spool-minimal, split-flap-spool-realistic")}render(){return this.variant==="realistic"?o.html`
1
+ "use strict";var o=require("lit"),r=require("lit/decorators.js"),S=require("./presets.js"),u=require("../lib/flap.js");require("./SplitFlapSpoolMinimal.js"),require("./SplitFlapSpoolRealistic.js");var v=Object.defineProperty,h=Object.getOwnPropertyDescriptor,i=(s,e,t,p)=>{for(var l=p>1?void 0:p?h(e,t):e,a=s.length-1,n;a>=0;a--)(n=s[a])&&(l=(p?n(e,t,l):n(l))||l);return p&&l&&v(e,t,l),l};exports.SplitFlapSpool=class extends o.LitElement{constructor(){super(...arguments),this.variant="minimal",this.value=" ",this.flaps=S.charSpool,this.speed=60,this.visibleSideCount=-1}get currentValue(){var e,t;return(t=(e=this._getActiveSpool())==null?void 0:e.currentValue)!=null?t:this.flaps[0]!=null?u.getFlapKey(this.flaps[0]):void 0}get isSettled(){var e,t;return(t=(e=this._getActiveSpool())==null?void 0:e.isSettled)!=null?t:!0}hasKey(e){var t,p;return(p=(t=this._getActiveSpool())==null?void 0:t.hasKey(e))!=null?p:this.flaps.some(l=>u.getFlapKey(l)===e)}_getActiveSpool(){return this.renderRoot.querySelector("split-flap-spool-minimal, split-flap-spool-realistic")}_renderVariant(){return this.variant==="realistic"?o.html`
2
2
  <split-flap-spool-realistic
3
3
  .value=${this.value}
4
4
  .flaps=${this.flaps}
@@ -11,7 +11,7 @@
11
11
  .flaps=${this.flaps}
12
12
  .speed=${this.speed}
13
13
  ></split-flap-spool-minimal>
14
- `}},exports.SplitFlapSpool.styles=o.css`
14
+ `}render(){return this._renderVariant()}},exports.SplitFlapSpool.styles=o.css`
15
15
  :host {
16
16
  display: contents;
17
17
  }
@@ -1,7 +1,7 @@
1
- "use strict";var n=require("lit"),u=require("lit/decorators.js"),y=require("lit/directives/style-map.js"),I=require("./presets.js"),l=require("../lib/flap.js"),f=Object.defineProperty,v=Object.getOwnPropertySymbols,T=Object.prototype.hasOwnProperty,F=Object.prototype.propertyIsEnumerable,x=(s,e,t)=>e in s?f(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t,o=(s,e)=>{for(var t in e||(e={}))T.call(e,t)&&x(s,t,e[t]);if(v)for(var t of v(e))F.call(e,t)&&x(s,t,e[t]);return s},_=(s,e,t,r)=>{for(var i=void 0,h=s.length-1,p;h>=0;h--)(p=s[h])&&(i=p(e,t,i)||i);return i&&f(e,t,i),i};class a extends n.LitElement{constructor(){super(...arguments),this.value=" ",this.flaps=I.charSpool,this.speed=60,this._currentIndex=0,this._prevIndex=0,this._stepping=!1,this._targetIndex=-1,this._stepTimer=null,this._animTimer=null,this._animEndsAt=0}get _animDur(){return Math.max(Math.floor(this.speed*.85),1)}updated(e){super.updated(e),e.has("flaps")&&this._syncIndicesToFlaps(e.get("flaps")),(e.has("value")||e.has("flaps"))&&this._startStepping()}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}get currentFlap(){var e;return(e=this.flaps[this._currentIndex])!=null?e:this.flaps[0]}get currentValue(){return this.currentFlap!=null?l.getFlapKey(this.currentFlap):void 0}get isSettled(){return!this._stepping&&this._targetIndex===-1&&this._stepTimer==null&&this._animTimer==null}hasKey(e){return this.flaps.some(t=>l.getFlapKey(t)===e)}_startStepping(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const e=this.flaps.findIndex(r=>l.getFlapKey(r)===this.value),t=this._stepping||this._stepTimer!=null;if(e===-1){this._targetIndex=-1,t&&this._scheduleAdvanceOrSettle(this._getRemainingAnimTime());return}if(this._targetIndex=e,this._currentIndex===this._targetIndex){t?this._scheduleAdvanceOrSettle(this._getRemainingAnimTime()):this._targetIndex=-1;return}t||this._doStep()}_doStep(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const e=(this._currentIndex+1)%this.flaps.length;this._prevIndex=this._currentIndex,this._currentIndex=e,this._stepping=!0,this._animEndsAt=Date.now()+this._animDur,this._animTimer!=null&&clearTimeout(this._animTimer),this._animTimer=setTimeout(()=>{this._stepping=!1,this._animTimer=null,this._animEndsAt=0},this._animDur);const t=this._currentIndex!==this._targetIndex?this.speed:this._getRemainingAnimTime();this._scheduleAdvanceOrSettle(t)}_scheduleAdvanceOrSettle(e){this._stepTimer!=null&&clearTimeout(this._stepTimer),this._stepTimer=setTimeout(()=>{if(this._stepTimer=null,this._targetIndex!==-1&&this._currentIndex!==this._targetIndex){this._doStep();return}this._finishSettling()},Math.max(e,0))}_finishSettling(){if(this._stepping){this._scheduleAdvanceOrSettle(this._getRemainingAnimTime());return}this._targetIndex=-1;const e=this.currentValue;e!=null&&this.dispatchEvent(new CustomEvent("settled",{detail:{value:e},bubbles:!0,composed:!0}))}_getRemainingAnimTime(){return this._stepping?Math.max(this._animEndsAt-Date.now(),1):0}_resetForEmptyFlaps(){this._clearTimers(),this._targetIndex=-1,this._currentIndex=0,this._prevIndex=0,this._stepping=!1}_syncIndicesToFlaps(e){var t,r;if(!this.flaps.length){this._resetForEmptyFlaps();return}if(e==null||!e.length){this._currentIndex=0,this._prevIndex=0,this._targetIndex=-1,this._clearTimers(),this._stepping=!1;return}const i=(t=e[this._currentIndex])!=null?t:e[0],h=(r=e[this._prevIndex])!=null?r:i,p=i!=null?l.getFlapKey(i):void 0,c=h!=null?l.getFlapKey(h):p;this._clearTimers(),this._stepping=!1,this._targetIndex=-1;const m=p!=null?this.flaps.findIndex(d=>l.getFlapKey(d)===p):-1,g=c!=null?this.flaps.findIndex(d=>l.getFlapKey(d)===c):-1;this._currentIndex=m>=0?m:0,this._prevIndex=g>=0?g:this._currentIndex}_clearTimers(){this._stepTimer!=null&&(clearTimeout(this._stepTimer),this._stepTimer=null),this._animTimer!=null&&(clearTimeout(this._animTimer),this._animTimer=null),this._animEndsAt=0}_renderHalf(e,t){var r;switch(e.type){case"char":return n.html`
1
+ "use strict";var n=require("lit"),p=require("lit/decorators.js"),x=require("lit/directives/style-map.js"),I=require("./presets.js"),o=require("../lib/flap.js"),g=Object.defineProperty,f=Object.getOwnPropertySymbols,y=Object.prototype.hasOwnProperty,T=Object.prototype.propertyIsEnumerable,v=(s,e,t)=>e in s?g(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t,_=(s,e)=>{for(var t in e||(e={}))y.call(e,t)&&v(s,t,e[t]);if(f)for(var t of f(e))T.call(e,t)&&v(s,t,e[t]);return s},u=(s,e,t,r)=>{for(var i=void 0,a=s.length-1,h;a>=0;a--)(h=s[a])&&(i=h(e,t,i)||i);return i&&g(e,t,i),i};class l extends n.LitElement{constructor(){super(...arguments),this.value=" ",this.flaps=I.charSpool,this.speed=60,this._currentIndex=0,this._prevIndex=0,this._stepping=!1,this._targetIndex=-1,this._stepTimer=null,this._animTimer=null,this._animEndsAt=0}get _animDur(){return Math.max(Math.floor(this.speed*.85),1)}updated(e){super.updated(e),e.has("flaps")&&this._syncIndicesToFlaps(e.get("flaps")),(e.has("value")||e.has("flaps"))&&this._startStepping()}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}get currentFlap(){var e;return(e=this.flaps[this._currentIndex])!=null?e:this.flaps[0]}get currentValue(){return this.currentFlap!=null?o.getFlapKey(this.currentFlap):void 0}get isSettled(){return!this._stepping&&this._targetIndex===-1&&this._stepTimer==null&&this._animTimer==null}hasKey(e){return this._findFlapIndex(e)>=0}_startStepping(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const e=this._findFlapIndex(this.value),t=this._isAnimating();if(e===-1){this._targetIndex=-1,t&&this._scheduleAdvanceOrSettle(this._getRemainingAnimationTime());return}if(this._targetIndex=e,this._currentIndex===this._targetIndex){t?this._scheduleAdvanceOrSettle(this._getRemainingAnimationTime()):this._targetIndex=-1;return}t||this._doStep()}_doStep(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const e=this._getNextIndex();this._prevIndex=this._currentIndex,this._currentIndex=e,this._stepping=!0,this._animEndsAt=Date.now()+this._animDur,this._animTimer!=null&&clearTimeout(this._animTimer),this._animTimer=setTimeout(()=>{this._stepping=!1,this._animTimer=null,this._animEndsAt=0},this._animDur);const t=this._currentIndex!==this._targetIndex?this.speed:this._getRemainingAnimationTime();this._scheduleAdvanceOrSettle(t)}_scheduleAdvanceOrSettle(e){this._stepTimer!=null&&clearTimeout(this._stepTimer),this._stepTimer=setTimeout(()=>{if(this._stepTimer=null,this._targetIndex!==-1&&this._currentIndex!==this._targetIndex){this._doStep();return}this._finishSettling()},Math.max(e,0))}_finishSettling(){if(this._stepping){this._scheduleAdvanceOrSettle(this._getRemainingAnimationTime());return}this._targetIndex=-1;const e=this.currentValue;e!=null&&this.dispatchEvent(new CustomEvent("settled",{detail:{value:e},bubbles:!0,composed:!0}))}_findFlapIndex(e){return this.flaps.findIndex(t=>o.getFlapKey(t)===e)}_getNextIndex(){return(this._currentIndex+1)%this.flaps.length}_isAnimating(){return this._stepping||this._stepTimer!=null}_getRemainingAnimationTime(){return this._stepping?Math.max(this._animEndsAt-Date.now(),1):0}_resetForEmptyFlaps(){this._clearTimers(),this._targetIndex=-1,this._currentIndex=0,this._prevIndex=0,this._stepping=!1}_syncIndicesToFlaps(e){var t,r;if(!this.flaps.length){this._resetForEmptyFlaps();return}if(e==null||!e.length){this._currentIndex=0,this._prevIndex=0,this._targetIndex=-1,this._clearTimers(),this._stepping=!1;return}const i=(t=e[this._currentIndex])!=null?t:e[0],a=(r=e[this._prevIndex])!=null?r:i,h=i!=null?o.getFlapKey(i):void 0,d=a!=null?o.getFlapKey(a):h;this._clearTimers(),this._stepping=!1,this._targetIndex=-1;const c=h!=null?this._findFlapIndex(h):-1,m=d!=null?this._findFlapIndex(d):-1;this._currentIndex=c>=0?c:0,this._prevIndex=m>=0?m:this._currentIndex}_clearTimers(){this._stepTimer!=null&&(clearTimeout(this._stepTimer),this._stepTimer=null),this._animTimer!=null&&(clearTimeout(this._animTimer),this._animTimer=null),this._animEndsAt=0}_renderHalf(e,t){var r;switch(e.type){case"char":return n.html`
2
2
  <div
3
3
  class="char-inner"
4
- style=${y.styleMap(o(o(o(o(o({},e.color!=null?{color:e.color}:{}),e.bg!=null?{background:e.bg}:{}),e.fontSize!=null?{fontSize:e.fontSize}:{}),e.fontFamily!=null?{fontFamily:e.fontFamily}:{}),e.fontWeight!=null?{fontWeight:e.fontWeight}:{}))}
4
+ style=${x.styleMap(_(_(_(_(_({},e.color!=null?{color:e.color}:{}),e.bg!=null?{background:e.bg}:{}),e.fontSize!=null?{fontSize:e.fontSize}:{}),e.fontFamily!=null?{fontFamily:e.fontFamily}:{}),e.fontWeight!=null?{fontWeight:e.fontWeight}:{}))}
5
5
  >
6
6
  ${e.value}
7
7
  </div>
@@ -13,4 +13,4 @@
13
13
  <div class="half top flipping">${this._renderHalf(r,"top")}</div>
14
14
  <div class="half bottom flipping">${this._renderHalf(t,"bottom")}</div>
15
15
  `:n.nothing}
16
- `}}_([u.property({type:String})],a.prototype,"value"),_([u.property({type:Array})],a.prototype,"flaps"),_([u.property({type:Number})],a.prototype,"speed"),_([u.state()],a.prototype,"_currentIndex"),_([u.state()],a.prototype,"_prevIndex"),_([u.state()],a.prototype,"_stepping"),exports.SplitFlapSpoolBase=a;
16
+ `}}u([p.property({type:String})],l.prototype,"value"),u([p.property({type:Array})],l.prototype,"flaps"),u([p.property({type:Number})],l.prototype,"speed"),u([p.state()],l.prototype,"_currentIndex"),u([p.state()],l.prototype,"_prevIndex"),u([p.state()],l.prototype,"_stepping"),exports.SplitFlapSpoolBase=l;
@@ -1,14 +1,14 @@
1
- "use strict";var p=require("lit"),f=require("lit/decorators.js"),d=require("lit/directives/ref.js"),u=require("lit/directives/style-map.js");require("./presets.js");var v=require("../lib/spool-layout.js"),m=require("./SplitFlapSpoolBase.js"),b=Object.defineProperty,g=Object.getOwnPropertyDescriptor,h=(n,e,a,t)=>{for(var r=t>1?void 0:t?g(e,a):e,s=n.length-1,i;s>=0;s--)(i=n[s])&&(r=(t?i(e,a,r):i(r))||r);return t&&r&&b(e,a,r),r},x=(n,e,a)=>new Promise((t,r)=>{var s=l=>{try{o(a.next(l))}catch(c){r(c)}},i=l=>{try{o(a.throw(l))}catch(c){r(c)}},o=l=>l.done?t(l.value):Promise.resolve(l.value).then(s,i);o((a=a.apply(n,e)).next())});exports.SplitFlapSpoolRealistic=class extends m.SplitFlapSpoolBase{constructor(){super(...arguments),this.visibleSideCount=-1,this._slotRef=d.createRef(),this._wrapResetTimer=null,this._skipNextIndexAnimation=!1}firstUpdated(){this._syncSlotState()}updated(e){var a;const t=this._currentIndex,r=this._prevIndex;if(super.updated(e),e.has("flaps")){this._clearWrapTimer();const s=this._currentIndex!==t||this._prevIndex!==r;this._skipNextIndexAnimation=s,this._syncSlotState()}if(e.has("_currentIndex")){if(this._skipNextIndexAnimation){this._skipNextIndexAnimation=!1,this._syncSlotState();return}const s=(a=e.get("_currentIndex"))!=null?a:0;this._animateStep(s,this._currentIndex)}}disconnectedCallback(){super.disconnectedCallback(),this._clearWrapTimer()}_clearWrapTimer(){this._wrapResetTimer!=null&&(clearTimeout(this._wrapResetTimer),this._wrapResetTimer=null)}_syncSlotState(){const e=this._slotRef.value;e!=null&&(e.style.setProperty("--current-character-index",String(this._currentIndex)),e.style.setProperty("--_flip-dur","0ms"))}_animateStep(e,a){return x(this,null,function*(){const t=this._slotRef.value;if(t==null)return;this._clearWrapTimer(),t.style.setProperty("--_flip-dur","0ms"),t.style.setProperty("--current-character-index",String(e)),yield new Promise(i=>requestAnimationFrame(()=>{requestAnimationFrame(()=>i())}));const r=e===this.flaps.length-1&&a===0,s=r?this.flaps.length:a;t.style.setProperty("--_flip-dur",`${this._animDur}ms`),t.style.setProperty("--current-character-index",String(s)),r&&(this._wrapResetTimer=setTimeout(()=>{t.style.setProperty("--_flip-dur","0ms"),t.style.setProperty("--current-character-index","0"),this._wrapResetTimer=null,this.requestUpdate()},this._animDur))})}_getRenderCenter(){return this.flaps.length>0&&this._prevIndex===this.flaps.length-1&&this._currentIndex===0&&(this._stepping||this._wrapResetTimer!=null)?this.flaps.length:this._currentIndex}render(){const e=this._getRenderCenter(),a=v.getRenderedFlaps(this.flaps,this._currentIndex,{visibleSideCount:this.visibleSideCount,renderCenter:e});return p.html`
1
+ "use strict";var p=require("lit"),f=require("lit/decorators.js"),d=require("lit/directives/ref.js"),u=require("lit/directives/style-map.js");require("./presets.js");var v=require("../lib/spool-layout.js"),m=require("./SplitFlapSpoolBase.js"),b=Object.defineProperty,g=Object.getOwnPropertyDescriptor,h=(n,e,r,t)=>{for(var a=t>1?void 0:t?g(e,r):e,s=n.length-1,i;s>=0;s--)(i=n[s])&&(a=(t?i(e,r,a):i(a))||a);return t&&a&&b(e,r,a),a},x=(n,e,r)=>new Promise((t,a)=>{var s=l=>{try{o(r.next(l))}catch(c){a(c)}},i=l=>{try{o(r.throw(l))}catch(c){a(c)}},o=l=>l.done?t(l.value):Promise.resolve(l.value).then(s,i);o((r=r.apply(n,e)).next())});exports.SplitFlapSpoolRealistic=class extends m.SplitFlapSpoolBase{constructor(){super(...arguments),this.visibleSideCount=-1,this._slotRef=d.createRef(),this._wrapResetTimer=null,this._skipNextIndexAnimation=!1}firstUpdated(){this._syncSlotState()}updated(e){var r;const t=this._currentIndex,a=this._prevIndex;super.updated(e),e.has("flaps")&&this._handleFlapChange(t,a),e.has("_currentIndex")&&this._handleIndexChange((r=e.get("_currentIndex"))!=null?r:0)}disconnectedCallback(){super.disconnectedCallback(),this._clearWrapTimer()}_clearWrapTimer(){this._wrapResetTimer!=null&&(clearTimeout(this._wrapResetTimer),this._wrapResetTimer=null)}_syncSlotState(){const e=this._slotRef.value;e!=null&&(e.style.setProperty("--current-character-index",String(this._currentIndex)),e.style.setProperty("--_flip-dur","0ms"))}_handleFlapChange(e,r){this._clearWrapTimer();const t=this._currentIndex!==e||this._prevIndex!==r;this._skipNextIndexAnimation=t,this._syncSlotState()}_handleIndexChange(e){if(this._skipNextIndexAnimation){this._skipNextIndexAnimation=!1,this._syncSlotState();return}this._animateStep(e,this._currentIndex)}_waitForPositionReset(){return new Promise(e=>{requestAnimationFrame(()=>{requestAnimationFrame(()=>e())})})}_isWrapForwardStep(e,r){return e===this.flaps.length-1&&r===0}_animateStep(e,r){return x(this,null,function*(){const t=this._slotRef.value;if(t==null)return;this._clearWrapTimer(),t.style.setProperty("--_flip-dur","0ms"),t.style.setProperty("--current-character-index",String(e)),yield this._waitForPositionReset();const a=this._isWrapForwardStep(e,r),s=a?this.flaps.length:r;t.style.setProperty("--_flip-dur",`${this._animDur}ms`),t.style.setProperty("--current-character-index",String(s)),a&&(this._wrapResetTimer=setTimeout(()=>{t.style.setProperty("--_flip-dur","0ms"),t.style.setProperty("--current-character-index","0"),this._wrapResetTimer=null,this.requestUpdate()},this._animDur))})}_getRenderCenter(){return this.flaps.length>0&&this._prevIndex===this.flaps.length-1&&this._currentIndex===0&&(this._stepping||this._wrapResetTimer!=null)?this.flaps.length:this._currentIndex}render(){const e=this._getRenderCenter(),r=v.getRenderedFlaps(this.flaps,this._currentIndex,{visibleSideCount:this.visibleSideCount,renderCenter:e});return p.html`
2
2
  <div
3
3
  ${d.ref(this._slotRef)}
4
4
  class="slot"
5
5
  style=${u.styleMap({"--total":String(this.flaps.length)})}
6
6
  >
7
- ${a.map(({flap:t,actualIndex:r,renderedIndex:s})=>{const i=this.flaps.length>2&&this.flaps.length%2===0&&Math.abs(s-e)===this.flaps.length/2;return p.html`
7
+ ${r.map(({flap:t,actualIndex:a,renderedIndex:s})=>{const i=this.flaps.length>2&&this.flaps.length%2===0&&Math.abs(s-e)===this.flaps.length/2;return p.html`
8
8
  <div
9
9
  class="character ${i?"is-background":""}"
10
10
  style="--index: ${s}"
11
- data-index=${r}
11
+ data-index=${a}
12
12
  >
13
13
  <div class="flap">${this._renderHalf(t,"top")}</div>
14
14
  <div class="flap" aria-hidden="true">${this._renderHalf(t,"bottom")}</div>
@@ -1,10 +1,10 @@
1
- import{css as b,LitElement as h,html as n}from"lit";import{property as p,customElement as c}from"lit/decorators.js";import"./spools/SplitFlapSpool.js";var u=Object.defineProperty,v=Object.getOwnPropertyDescriptor,l=(e,r,a,i)=>{for(var t=i>1?void 0:i?v(r,a):r,o=e.length-1,d;o>=0;o--)(d=e[o])&&(t=(i?d(r,a,t):d(t))||t);return i&&t&&u(r,a,t),t};let s=class extends h{constructor(){super(...arguments),this.spools=[],this.grid=[],this.speed=60,this.variant="minimal",this.visibleSideCount=-1,this._pendingSettle=!1}updated(e){super.updated(e),(e.has("spools")||e.has("grid"))&&(this._pendingSettle=!0,Promise.resolve().then(()=>this._checkAllSettled()))}_getSpoolEls(){return Array.from(this.renderRoot.querySelectorAll("split-flap-spool"))}_checkAllSettled(){if(!this._pendingSettle)return;const e=this._getSpoolEls();e.length!==0&&e.every(r=>r.isSettled)&&(this._pendingSettle=!1,this._dispatchBoardSettled(e))}_dispatchBoardSettled(e){let r=0;const a=this.spools.map(i=>i.map(()=>{var t,o;return(o=(t=e[r++])==null?void 0:t.currentValue)!=null?o:""}));this.dispatchEvent(new CustomEvent("board-settled",{detail:{grid:a},bubbles:!0,composed:!0}))}render(){return n`
2
- ${this.spools.map((e,r)=>n`
3
- <div class="board-row" style="z-index: ${r+1}">
4
- ${e.map((a,i)=>{var t,o;return n`
1
+ import{css as b,LitElement as u,html as n}from"lit";import{property as d,customElement as c}from"lit/decorators.js";var h=Object.defineProperty,g=Object.getOwnPropertyDescriptor,a=(t,e,p,s)=>{for(var r=s>1?void 0:s?g(e,p):e,i=t.length-1,l;i>=0;i--)(l=t[i])&&(r=(s?l(e,p,r):l(r))||r);return s&&r&&h(e,p,r),r};let o=class extends u{constructor(){super(...arguments),this.spools=[],this.grid=[],this.speed=60,this.variant="minimal",this.visibleSideCount=-1,this._pendingSettle=!1}updated(t){super.updated(t),(t.has("spools")||t.has("grid"))&&(this._pendingSettle=!0,queueMicrotask(()=>this._checkAllSettled()))}_getSpoolEls(){return Array.from(this.renderRoot.querySelectorAll("split-flap-spool"))}_checkAllSettled(){if(!this._pendingSettle)return;const t=this._getSpoolEls();if(t.length===0){this._pendingSettle=!1,this._dispatchBoardSettled([]);return}t.every(e=>e.isSettled)&&(this._pendingSettle=!1,this._dispatchBoardSettled(t))}_dispatchBoardSettled(t){const e=this._getCurrentGrid(t);this.dispatchEvent(new CustomEvent("board-settled",{detail:{grid:e},bubbles:!0,composed:!0}))}_getCurrentGrid(t){let e=0;return this.spools.map(p=>p.map(()=>{var s,r;const i=(r=(s=t[e])==null?void 0:s.currentValue)!=null?r:"";return e+=1,i}))}render(){return n`
2
+ ${this.spools.map((t,e)=>n`
3
+ <div class="board-row" style="z-index: ${e+1}">
4
+ ${t.map((p,s)=>{var r,i;return n`
5
5
  <split-flap-spool
6
- .flaps=${a}
7
- .value=${(o=(t=this.grid[r])==null?void 0:t[i])!=null?o:""}
6
+ .flaps=${p}
7
+ .value=${(i=(r=this.grid[e])==null?void 0:r[s])!=null?i:""}
8
8
  .speed=${this.speed}
9
9
  .variant=${this.variant}
10
10
  .visibleSideCount=${this.visibleSideCount}
@@ -13,14 +13,12 @@ import{css as b,LitElement as h,html as n}from"lit";import{property as p,customE
13
13
  `})}
14
14
  </div>
15
15
  `)}
16
- `}};s.styles=b`
16
+ `}};o.styles=b`
17
17
  :host {
18
18
  display: inline-flex;
19
19
  position: relative;
20
20
  flex-direction: column;
21
21
  box-sizing: border-box;
22
- /* Inset shadows for side rails; render below all children so frame bars
23
- * appear at the same visual level. */
24
22
  box-shadow:
25
23
  inset 14px 0 18px rgba(0, 0, 0, 0.55),
26
24
  inset -14px 0 18px rgba(0, 0, 0, 0.55);
@@ -29,7 +27,6 @@ import{css as b,LitElement as h,html as n}from"lit";import{property as p,customE
29
27
  padding: var(--sfb-board-padding, 10px);
30
28
  }
31
29
 
32
- /* Absolute overlay so the outer frame ring adds no layout space. */
33
30
  :host::after {
34
31
  position: absolute;
35
32
  z-index: 10000;
@@ -40,8 +37,6 @@ import{css as b,LitElement as h,html as n}from"lit";import{property as p,customE
40
37
  content: '';
41
38
  }
42
39
 
43
- /* Frame bar caps the top padding of each row. z-index increases per row in
44
- * render() so each bar sits above the drum content of the row above it. */
45
40
  .board-row {
46
41
  display: flex;
47
42
  position: relative;
@@ -58,4 +53,4 @@ import{css as b,LitElement as h,html as n}from"lit";import{property as p,customE
58
53
  height: 10px;
59
54
  content: '';
60
55
  }
61
- `,l([p({type:Array})],s.prototype,"spools",2),l([p({type:Array})],s.prototype,"grid",2),l([p({type:Number})],s.prototype,"speed",2),l([p({type:String})],s.prototype,"variant",2),l([p({type:Number})],s.prototype,"visibleSideCount",2),s=l([c("split-flap-board")],s);export{s as SplitFlapBoard};
56
+ `,a([d({type:Array})],o.prototype,"spools",2),a([d({type:Array})],o.prototype,"grid",2),a([d({type:Number})],o.prototype,"speed",2),a([d({type:String})],o.prototype,"variant",2),a([d({type:Number})],o.prototype,"visibleSideCount",2),o=a([c("split-flap-board")],o);export{o as SplitFlapBoard};
@@ -1 +1 @@
1
- import{charSpool as f}from"../spools/presets.js";var b=Object.defineProperty,y=Object.getOwnPropertySymbols,g=Object.prototype.hasOwnProperty,v=Object.prototype.propertyIsEnumerable,h=(t,r,e)=>r in t?b(t,r,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[r]=e,i=(t,r)=>{for(var e in r||(r={}))g.call(r,e)&&h(t,e,r[e]);if(y)for(var e of y(r))v.call(r,e)&&h(t,e,r[e]);return t};function d(t,r,e){const o=t.length>0&&Array.isArray(t[0])?t:Array.from({length:r},()=>t);return Array.from({length:e},()=>Array.from({length:r},(l,a)=>{var n;return(n=o[a%o.length])!=null?n:f}))}function A(t,r){const e=[],o=[];for(const l of t){const a=typeof l=="object",n=a?l.text:l,p=a?l.bg:void 0,s=a?l.color:void 0,u=n.toUpperCase().padEnd(r," ").slice(0,r).split("");if(p!=null||s!=null){const m=u.map(()=>f.map(c=>c.type!=="char"?c:i(i(i({},c),p!=null?{bg:p}:{}),s!=null?{color:s}:{})));e.push(m)}else e.push(Array.from({length:r},()=>f));o.push(u)}return{spools:e,grid:o}}export{A as fromLines,d as spoolGrid};
1
+ import{charSpool as u}from"../spools/presets.js";var y=Object.defineProperty,f=Object.getOwnPropertySymbols,b=Object.prototype.hasOwnProperty,g=Object.prototype.propertyIsEnumerable,i=(r,t,e)=>t in r?y(r,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):r[t]=e,c=(r,t)=>{for(var e in t||(t={}))b.call(t,e)&&i(r,e,t[e]);if(f)for(var e of f(t))g.call(t,e)&&i(r,e,t[e]);return r};function m(r,t,e){const o=v(r)?r:[r];return Array.from({length:e},()=>Array.from({length:t},(a,n)=>{var l;return(l=o[n%o.length])!=null?l:u}))}function h(r,t){const e=[],o=[];for(const a of r){const n=d(a),l=A(n.text,t).split(""),s=n.bg!=null||n.color!=null;e.push(s?j(t,n):p(u,t)),o.push(l)}return{spools:e,grid:o}}function v(r){return Array.isArray(r[0])}function p(r,t){return Array.from({length:t},()=>r)}function O(r,t){return r.type!=="char"?r:c(c(c({},r),t.bg!=null?{bg:t.bg}:{}),t.color!=null?{color:t.color}:{})}function d(r){return typeof r=="string"?{text:r}:r}function A(r,t){return r.toUpperCase().padEnd(t," ").slice(0,t)}function j(r,t){const e=u.map(o=>O(o,t));return p(e,r)}export{h as fromLines,m as spoolGrid};
@@ -1 +1 @@
1
- function r(e){if(e.key!=null)return e.key;switch(e.type){case"char":case"color":return e.value;case"image":return e.src;case"custom":return e.key}}export{r as getFlapKey};
1
+ function t(r){var e,c;switch(r.type){case"char":case"color":return(e=r.key)!=null?e:r.value;case"image":return(c=r.key)!=null?c:r.src;case"custom":return r.key}}export{t as getFlapKey};
@@ -1,4 +1,4 @@
1
- import{css as m,LitElement as h,html as u}from"lit";import{property as o,customElement as c}from"lit/decorators.js";import{charSpool as d}from"./presets.js";import{getFlapKey as v}from"../lib/flap.js";import"./SplitFlapSpoolMinimal.js";import"./SplitFlapSpoolRealistic.js";var f=Object.defineProperty,y=Object.getOwnPropertyDescriptor,p=(e,t,s,r)=>{for(var i=r>1?void 0:r?y(t,s):t,a=e.length-1,n;a>=0;a--)(n=e[a])&&(i=(r?n(t,s,i):n(i))||i);return r&&i&&f(t,s,i),i};let l=class extends h{constructor(){super(...arguments),this.variant="minimal",this.value=" ",this.flaps=d,this.speed=60,this.visibleSideCount=-1}get currentValue(){var e,t;return(t=(e=this._getActiveSpool())==null?void 0:e.currentValue)!=null?t:this.flaps[0]!=null?v(this.flaps[0]):void 0}get isSettled(){var e,t;return(t=(e=this._getActiveSpool())==null?void 0:e.isSettled)!=null?t:!0}hasKey(e){var t,s;return(s=(t=this._getActiveSpool())==null?void 0:t.hasKey(e))!=null?s:this.flaps.some(r=>v(r)===e)}_getActiveSpool(){return this.renderRoot.querySelector("split-flap-spool-minimal, split-flap-spool-realistic")}render(){return this.variant==="realistic"?u`
1
+ import{css as h,LitElement as m,html as u}from"lit";import{property as o,customElement as d}from"lit/decorators.js";import{charSpool as c}from"./presets.js";import{getFlapKey as v}from"../lib/flap.js";import"./SplitFlapSpoolMinimal.js";import"./SplitFlapSpoolRealistic.js";var f=Object.defineProperty,y=Object.getOwnPropertyDescriptor,p=(e,t,i,s)=>{for(var r=s>1?void 0:s?y(t,i):t,a=e.length-1,n;a>=0;a--)(n=e[a])&&(r=(s?n(t,i,r):n(r))||r);return s&&r&&f(t,i,r),r};let l=class extends m{constructor(){super(...arguments),this.variant="minimal",this.value=" ",this.flaps=c,this.speed=60,this.visibleSideCount=-1}get currentValue(){var e,t;return(t=(e=this._getActiveSpool())==null?void 0:e.currentValue)!=null?t:this.flaps[0]!=null?v(this.flaps[0]):void 0}get isSettled(){var e,t;return(t=(e=this._getActiveSpool())==null?void 0:e.isSettled)!=null?t:!0}hasKey(e){var t,i;return(i=(t=this._getActiveSpool())==null?void 0:t.hasKey(e))!=null?i:this.flaps.some(s=>v(s)===e)}_getActiveSpool(){return this.renderRoot.querySelector("split-flap-spool-minimal, split-flap-spool-realistic")}_renderVariant(){return this.variant==="realistic"?u`
2
2
  <split-flap-spool-realistic
3
3
  .value=${this.value}
4
4
  .flaps=${this.flaps}
@@ -11,8 +11,8 @@ import{css as m,LitElement as h,html as u}from"lit";import{property as o,customE
11
11
  .flaps=${this.flaps}
12
12
  .speed=${this.speed}
13
13
  ></split-flap-spool-minimal>
14
- `}};l.styles=m`
14
+ `}render(){return this._renderVariant()}};l.styles=h`
15
15
  :host {
16
16
  display: contents;
17
17
  }
18
- `,p([o({type:String})],l.prototype,"variant",2),p([o({type:String})],l.prototype,"value",2),p([o({type:Array})],l.prototype,"flaps",2),p([o({type:Number})],l.prototype,"speed",2),p([o({type:Number})],l.prototype,"visibleSideCount",2),l=p([c("split-flap-spool")],l);export{l as SplitFlapSpool};
18
+ `,p([o({type:String})],l.prototype,"variant",2),p([o({type:String})],l.prototype,"value",2),p([o({type:Array})],l.prototype,"flaps",2),p([o({type:Number})],l.prototype,"speed",2),p([o({type:Number})],l.prototype,"visibleSideCount",2),l=p([d("split-flap-spool")],l);export{l as SplitFlapSpool};
@@ -1,16 +1,16 @@
1
- import{LitElement as y,html as p,nothing as v}from"lit";import{property as d,state as m}from"lit/decorators.js";import{styleMap as b}from"lit/directives/style-map.js";import{charSpool as F}from"./presets.js";import{getFlapKey as n}from"../lib/flap.js";var x=Object.defineProperty,I=Object.getOwnPropertySymbols,S=Object.prototype.hasOwnProperty,A=Object.prototype.propertyIsEnumerable,T=(s,t,e)=>t in s?x(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e,o=(s,t)=>{for(var e in t||(t={}))S.call(t,e)&&T(s,e,t[e]);if(I)for(var e of I(t))A.call(t,e)&&T(s,e,t[e]);return s},u=(s,t,e,r)=>{for(var i=void 0,a=s.length-1,h;a>=0;a--)(h=s[a])&&(i=h(t,e,i)||i);return i&&x(t,e,i),i};class l extends y{constructor(){super(...arguments),this.value=" ",this.flaps=F,this.speed=60,this._currentIndex=0,this._prevIndex=0,this._stepping=!1,this._targetIndex=-1,this._stepTimer=null,this._animTimer=null,this._animEndsAt=0}get _animDur(){return Math.max(Math.floor(this.speed*.85),1)}updated(t){super.updated(t),t.has("flaps")&&this._syncIndicesToFlaps(t.get("flaps")),(t.has("value")||t.has("flaps"))&&this._startStepping()}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}get currentFlap(){var t;return(t=this.flaps[this._currentIndex])!=null?t:this.flaps[0]}get currentValue(){return this.currentFlap!=null?n(this.currentFlap):void 0}get isSettled(){return!this._stepping&&this._targetIndex===-1&&this._stepTimer==null&&this._animTimer==null}hasKey(t){return this.flaps.some(e=>n(e)===t)}_startStepping(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const t=this.flaps.findIndex(r=>n(r)===this.value),e=this._stepping||this._stepTimer!=null;if(t===-1){this._targetIndex=-1,e&&this._scheduleAdvanceOrSettle(this._getRemainingAnimTime());return}if(this._targetIndex=t,this._currentIndex===this._targetIndex){e?this._scheduleAdvanceOrSettle(this._getRemainingAnimTime()):this._targetIndex=-1;return}e||this._doStep()}_doStep(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const t=(this._currentIndex+1)%this.flaps.length;this._prevIndex=this._currentIndex,this._currentIndex=t,this._stepping=!0,this._animEndsAt=Date.now()+this._animDur,this._animTimer!=null&&clearTimeout(this._animTimer),this._animTimer=setTimeout(()=>{this._stepping=!1,this._animTimer=null,this._animEndsAt=0},this._animDur);const e=this._currentIndex!==this._targetIndex?this.speed:this._getRemainingAnimTime();this._scheduleAdvanceOrSettle(e)}_scheduleAdvanceOrSettle(t){this._stepTimer!=null&&clearTimeout(this._stepTimer),this._stepTimer=setTimeout(()=>{if(this._stepTimer=null,this._targetIndex!==-1&&this._currentIndex!==this._targetIndex){this._doStep();return}this._finishSettling()},Math.max(t,0))}_finishSettling(){if(this._stepping){this._scheduleAdvanceOrSettle(this._getRemainingAnimTime());return}this._targetIndex=-1;const t=this.currentValue;t!=null&&this.dispatchEvent(new CustomEvent("settled",{detail:{value:t},bubbles:!0,composed:!0}))}_getRemainingAnimTime(){return this._stepping?Math.max(this._animEndsAt-Date.now(),1):0}_resetForEmptyFlaps(){this._clearTimers(),this._targetIndex=-1,this._currentIndex=0,this._prevIndex=0,this._stepping=!1}_syncIndicesToFlaps(t){var e,r;if(!this.flaps.length){this._resetForEmptyFlaps();return}if(t==null||!t.length){this._currentIndex=0,this._prevIndex=0,this._targetIndex=-1,this._clearTimers(),this._stepping=!1;return}const i=(e=t[this._currentIndex])!=null?e:t[0],a=(r=t[this._prevIndex])!=null?r:i,h=i!=null?n(i):void 0,c=a!=null?n(a):h;this._clearTimers(),this._stepping=!1,this._targetIndex=-1;const f=h!=null?this.flaps.findIndex(_=>n(_)===h):-1,g=c!=null?this.flaps.findIndex(_=>n(_)===c):-1;this._currentIndex=f>=0?f:0,this._prevIndex=g>=0?g:this._currentIndex}_clearTimers(){this._stepTimer!=null&&(clearTimeout(this._stepTimer),this._stepTimer=null),this._animTimer!=null&&(clearTimeout(this._animTimer),this._animTimer=null),this._animEndsAt=0}_renderHalf(t,e){var r;switch(t.type){case"char":return p`
1
+ import{LitElement as T,html as h,nothing as f}from"lit";import{property as _,state as d}from"lit/decorators.js";import{styleMap as y}from"lit/directives/style-map.js";import{charSpool as F}from"./presets.js";import{getFlapKey as o}from"../lib/flap.js";var v=Object.defineProperty,x=Object.getOwnPropertySymbols,b=Object.prototype.hasOwnProperty,S=Object.prototype.propertyIsEnumerable,I=(s,t,e)=>t in s?v(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e,u=(s,t)=>{for(var e in t||(t={}))b.call(t,e)&&I(s,e,t[e]);if(x)for(var e of x(t))S.call(t,e)&&I(s,e,t[e]);return s},p=(s,t,e,n)=>{for(var i=void 0,l=s.length-1,a;l>=0;l--)(a=s[l])&&(i=a(t,e,i)||i);return i&&v(t,e,i),i};class r extends T{constructor(){super(...arguments),this.value=" ",this.flaps=F,this.speed=60,this._currentIndex=0,this._prevIndex=0,this._stepping=!1,this._targetIndex=-1,this._stepTimer=null,this._animTimer=null,this._animEndsAt=0}get _animDur(){return Math.max(Math.floor(this.speed*.85),1)}updated(t){super.updated(t),t.has("flaps")&&this._syncIndicesToFlaps(t.get("flaps")),(t.has("value")||t.has("flaps"))&&this._startStepping()}disconnectedCallback(){super.disconnectedCallback(),this._clearTimers()}get currentFlap(){var t;return(t=this.flaps[this._currentIndex])!=null?t:this.flaps[0]}get currentValue(){return this.currentFlap!=null?o(this.currentFlap):void 0}get isSettled(){return!this._stepping&&this._targetIndex===-1&&this._stepTimer==null&&this._animTimer==null}hasKey(t){return this._findFlapIndex(t)>=0}_startStepping(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const t=this._findFlapIndex(this.value),e=this._isAnimating();if(t===-1){this._targetIndex=-1,e&&this._scheduleAdvanceOrSettle(this._getRemainingAnimationTime());return}if(this._targetIndex=t,this._currentIndex===this._targetIndex){e?this._scheduleAdvanceOrSettle(this._getRemainingAnimationTime()):this._targetIndex=-1;return}e||this._doStep()}_doStep(){if(!this.flaps.length){this._resetForEmptyFlaps();return}const t=this._getNextIndex();this._prevIndex=this._currentIndex,this._currentIndex=t,this._stepping=!0,this._animEndsAt=Date.now()+this._animDur,this._animTimer!=null&&clearTimeout(this._animTimer),this._animTimer=setTimeout(()=>{this._stepping=!1,this._animTimer=null,this._animEndsAt=0},this._animDur);const e=this._currentIndex!==this._targetIndex?this.speed:this._getRemainingAnimationTime();this._scheduleAdvanceOrSettle(e)}_scheduleAdvanceOrSettle(t){this._stepTimer!=null&&clearTimeout(this._stepTimer),this._stepTimer=setTimeout(()=>{if(this._stepTimer=null,this._targetIndex!==-1&&this._currentIndex!==this._targetIndex){this._doStep();return}this._finishSettling()},Math.max(t,0))}_finishSettling(){if(this._stepping){this._scheduleAdvanceOrSettle(this._getRemainingAnimationTime());return}this._targetIndex=-1;const t=this.currentValue;t!=null&&this.dispatchEvent(new CustomEvent("settled",{detail:{value:t},bubbles:!0,composed:!0}))}_findFlapIndex(t){return this.flaps.findIndex(e=>o(e)===t)}_getNextIndex(){return(this._currentIndex+1)%this.flaps.length}_isAnimating(){return this._stepping||this._stepTimer!=null}_getRemainingAnimationTime(){return this._stepping?Math.max(this._animEndsAt-Date.now(),1):0}_resetForEmptyFlaps(){this._clearTimers(),this._targetIndex=-1,this._currentIndex=0,this._prevIndex=0,this._stepping=!1}_syncIndicesToFlaps(t){var e,n;if(!this.flaps.length){this._resetForEmptyFlaps();return}if(t==null||!t.length){this._currentIndex=0,this._prevIndex=0,this._targetIndex=-1,this._clearTimers(),this._stepping=!1;return}const i=(e=t[this._currentIndex])!=null?e:t[0],l=(n=t[this._prevIndex])!=null?n:i,a=i!=null?o(i):void 0,m=l!=null?o(l):a;this._clearTimers(),this._stepping=!1,this._targetIndex=-1;const c=a!=null?this._findFlapIndex(a):-1,g=m!=null?this._findFlapIndex(m):-1;this._currentIndex=c>=0?c:0,this._prevIndex=g>=0?g:this._currentIndex}_clearTimers(){this._stepTimer!=null&&(clearTimeout(this._stepTimer),this._stepTimer=null),this._animTimer!=null&&(clearTimeout(this._animTimer),this._animTimer=null),this._animEndsAt=0}_renderHalf(t,e){var n;switch(t.type){case"char":return h`
2
2
  <div
3
3
  class="char-inner"
4
- style=${b(o(o(o(o(o({},t.color!=null?{color:t.color}:{}),t.bg!=null?{background:t.bg}:{}),t.fontSize!=null?{fontSize:t.fontSize}:{}),t.fontFamily!=null?{fontFamily:t.fontFamily}:{}),t.fontWeight!=null?{fontWeight:t.fontWeight}:{}))}
4
+ style=${y(u(u(u(u(u({},t.color!=null?{color:t.color}:{}),t.bg!=null?{background:t.bg}:{}),t.fontSize!=null?{fontSize:t.fontSize}:{}),t.fontFamily!=null?{fontFamily:t.fontFamily}:{}),t.fontWeight!=null?{fontWeight:t.fontWeight}:{}))}
5
5
  >
6
6
  ${t.value}
7
7
  </div>
8
- `;case"color":return p`<div class="color-fill" style="background: ${t.value}"></div>`;case"image":return p`<img class="image-fill" src=${t.src} alt=${(r=t.alt)!=null?r:""} />`;case"custom":return p`<div class="custom-fill">${t[e]}</div>`}}_getFlaps(){var t;const e=this.currentFlap;return e==null?null:{current:e,prev:(t=this.flaps[this._prevIndex])!=null?t:e}}_renderCard(){const t=this._getFlaps();if(t==null)return v;const{current:e,prev:r}=t,i=this._stepping?r:e;return p`
8
+ `;case"color":return h`<div class="color-fill" style="background: ${t.value}"></div>`;case"image":return h`<img class="image-fill" src=${t.src} alt=${(n=t.alt)!=null?n:""} />`;case"custom":return h`<div class="custom-fill">${t[e]}</div>`}}_getFlaps(){var t;const e=this.currentFlap;return e==null?null:{current:e,prev:(t=this.flaps[this._prevIndex])!=null?t:e}}_renderCard(){const t=this._getFlaps();if(t==null)return f;const{current:e,prev:n}=t,i=this._stepping?n:e;return h`
9
9
  <div class="half top">${this._renderHalf(e,"top")}</div>
10
10
  <div class="half bottom">${this._renderHalf(i,"bottom")}</div>
11
11
 
12
- ${this._stepping?p`
13
- <div class="half top flipping">${this._renderHalf(r,"top")}</div>
12
+ ${this._stepping?h`
13
+ <div class="half top flipping">${this._renderHalf(n,"top")}</div>
14
14
  <div class="half bottom flipping">${this._renderHalf(e,"bottom")}</div>
15
- `:v}
16
- `}}u([d({type:String})],l.prototype,"value"),u([d({type:Array})],l.prototype,"flaps"),u([d({type:Number})],l.prototype,"speed"),u([m()],l.prototype,"_currentIndex"),u([m()],l.prototype,"_prevIndex"),u([m()],l.prototype,"_stepping");export{l as SplitFlapSpoolBase};
15
+ `:f}
16
+ `}}p([_({type:String})],r.prototype,"value"),p([_({type:Array})],r.prototype,"flaps"),p([_({type:Number})],r.prototype,"speed"),p([d()],r.prototype,"_currentIndex"),p([d()],r.prototype,"_prevIndex"),p([d()],r.prototype,"_stepping");export{r as SplitFlapSpoolBase};
@@ -1,17 +1,17 @@
1
- import{css as h,html as p}from"lit";import{property as u,customElement as m}from"lit/decorators.js";import{createRef as v,ref as b}from"lit/directives/ref.js";import{styleMap as x}from"lit/directives/style-map.js";import"./presets.js";import{getRenderedFlaps as g}from"../lib/spool-layout.js";import{SplitFlapSpoolBase as y}from"./SplitFlapSpoolBase.js";var _=Object.defineProperty,w=Object.getOwnPropertyDescriptor,d=(t,a,e,s)=>{for(var r=s>1?void 0:s?w(a,e):a,i=t.length-1,l;i>=0;i--)(l=t[i])&&(r=(s?l(a,e,r):l(r))||r);return s&&r&&_(a,e,r),r},S=(t,a,e)=>new Promise((s,r)=>{var i=n=>{try{c(e.next(n))}catch(f){r(f)}},l=n=>{try{c(e.throw(n))}catch(f){r(f)}},c=n=>n.done?s(n.value):Promise.resolve(n.value).then(i,l);c((e=e.apply(t,a)).next())});let o=class extends y{constructor(){super(...arguments),this.visibleSideCount=-1,this._slotRef=v(),this._wrapResetTimer=null,this._skipNextIndexAnimation=!1}firstUpdated(){this._syncSlotState()}updated(t){var a;const e=this._currentIndex,s=this._prevIndex;if(super.updated(t),t.has("flaps")){this._clearWrapTimer();const r=this._currentIndex!==e||this._prevIndex!==s;this._skipNextIndexAnimation=r,this._syncSlotState()}if(t.has("_currentIndex")){if(this._skipNextIndexAnimation){this._skipNextIndexAnimation=!1,this._syncSlotState();return}const r=(a=t.get("_currentIndex"))!=null?a:0;this._animateStep(r,this._currentIndex)}}disconnectedCallback(){super.disconnectedCallback(),this._clearWrapTimer()}_clearWrapTimer(){this._wrapResetTimer!=null&&(clearTimeout(this._wrapResetTimer),this._wrapResetTimer=null)}_syncSlotState(){const t=this._slotRef.value;t!=null&&(t.style.setProperty("--current-character-index",String(this._currentIndex)),t.style.setProperty("--_flip-dur","0ms"))}_animateStep(t,a){return S(this,null,function*(){const e=this._slotRef.value;if(e==null)return;this._clearWrapTimer(),e.style.setProperty("--_flip-dur","0ms"),e.style.setProperty("--current-character-index",String(t)),yield new Promise(i=>requestAnimationFrame(()=>{requestAnimationFrame(()=>i())}));const s=t===this.flaps.length-1&&a===0,r=s?this.flaps.length:a;e.style.setProperty("--_flip-dur",`${this._animDur}ms`),e.style.setProperty("--current-character-index",String(r)),s&&(this._wrapResetTimer=setTimeout(()=>{e.style.setProperty("--_flip-dur","0ms"),e.style.setProperty("--current-character-index","0"),this._wrapResetTimer=null,this.requestUpdate()},this._animDur))})}_getRenderCenter(){return this.flaps.length>0&&this._prevIndex===this.flaps.length-1&&this._currentIndex===0&&(this._stepping||this._wrapResetTimer!=null)?this.flaps.length:this._currentIndex}render(){const t=this._getRenderCenter(),a=g(this.flaps,this._currentIndex,{visibleSideCount:this.visibleSideCount,renderCenter:t});return p`
1
+ import{css as h,html as p}from"lit";import{property as u,customElement as m}from"lit/decorators.js";import{createRef as v,ref as b}from"lit/directives/ref.js";import{styleMap as g}from"lit/directives/style-map.js";import"./presets.js";import{getRenderedFlaps as x}from"../lib/spool-layout.js";import{SplitFlapSpoolBase as _}from"./SplitFlapSpoolBase.js";var y=Object.defineProperty,w=Object.getOwnPropertyDescriptor,d=(e,r,t,s)=>{for(var a=s>1?void 0:s?w(r,t):r,i=e.length-1,l;i>=0;i--)(l=e[i])&&(a=(s?l(r,t,a):l(a))||a);return s&&a&&y(r,t,a),a},S=(e,r,t)=>new Promise((s,a)=>{var i=n=>{try{c(t.next(n))}catch(f){a(f)}},l=n=>{try{c(t.throw(n))}catch(f){a(f)}},c=n=>n.done?s(n.value):Promise.resolve(n.value).then(i,l);c((t=t.apply(e,r)).next())});let o=class extends _{constructor(){super(...arguments),this.visibleSideCount=-1,this._slotRef=v(),this._wrapResetTimer=null,this._skipNextIndexAnimation=!1}firstUpdated(){this._syncSlotState()}updated(e){var r;const t=this._currentIndex,s=this._prevIndex;super.updated(e),e.has("flaps")&&this._handleFlapChange(t,s),e.has("_currentIndex")&&this._handleIndexChange((r=e.get("_currentIndex"))!=null?r:0)}disconnectedCallback(){super.disconnectedCallback(),this._clearWrapTimer()}_clearWrapTimer(){this._wrapResetTimer!=null&&(clearTimeout(this._wrapResetTimer),this._wrapResetTimer=null)}_syncSlotState(){const e=this._slotRef.value;e!=null&&(e.style.setProperty("--current-character-index",String(this._currentIndex)),e.style.setProperty("--_flip-dur","0ms"))}_handleFlapChange(e,r){this._clearWrapTimer();const t=this._currentIndex!==e||this._prevIndex!==r;this._skipNextIndexAnimation=t,this._syncSlotState()}_handleIndexChange(e){if(this._skipNextIndexAnimation){this._skipNextIndexAnimation=!1,this._syncSlotState();return}this._animateStep(e,this._currentIndex)}_waitForPositionReset(){return new Promise(e=>{requestAnimationFrame(()=>{requestAnimationFrame(()=>e())})})}_isWrapForwardStep(e,r){return e===this.flaps.length-1&&r===0}_animateStep(e,r){return S(this,null,function*(){const t=this._slotRef.value;if(t==null)return;this._clearWrapTimer(),t.style.setProperty("--_flip-dur","0ms"),t.style.setProperty("--current-character-index",String(e)),yield this._waitForPositionReset();const s=this._isWrapForwardStep(e,r),a=s?this.flaps.length:r;t.style.setProperty("--_flip-dur",`${this._animDur}ms`),t.style.setProperty("--current-character-index",String(a)),s&&(this._wrapResetTimer=setTimeout(()=>{t.style.setProperty("--_flip-dur","0ms"),t.style.setProperty("--current-character-index","0"),this._wrapResetTimer=null,this.requestUpdate()},this._animDur))})}_getRenderCenter(){return this.flaps.length>0&&this._prevIndex===this.flaps.length-1&&this._currentIndex===0&&(this._stepping||this._wrapResetTimer!=null)?this.flaps.length:this._currentIndex}render(){const e=this._getRenderCenter(),r=x(this.flaps,this._currentIndex,{visibleSideCount:this.visibleSideCount,renderCenter:e});return p`
2
2
  <div
3
3
  ${b(this._slotRef)}
4
4
  class="slot"
5
- style=${x({"--total":String(this.flaps.length)})}
5
+ style=${g({"--total":String(this.flaps.length)})}
6
6
  >
7
- ${a.map(({flap:e,actualIndex:s,renderedIndex:r})=>{const i=this.flaps.length>2&&this.flaps.length%2===0&&Math.abs(r-t)===this.flaps.length/2;return p`
7
+ ${r.map(({flap:t,actualIndex:s,renderedIndex:a})=>{const i=this.flaps.length>2&&this.flaps.length%2===0&&Math.abs(a-e)===this.flaps.length/2;return p`
8
8
  <div
9
9
  class="character ${i?"is-background":""}"
10
- style="--index: ${r}"
10
+ style="--index: ${a}"
11
11
  data-index=${s}
12
12
  >
13
- <div class="flap">${this._renderHalf(e,"top")}</div>
14
- <div class="flap" aria-hidden="true">${this._renderHalf(e,"bottom")}</div>
13
+ <div class="flap">${this._renderHalf(t,"top")}</div>
14
+ <div class="flap" aria-hidden="true">${this._renderHalf(t,"bottom")}</div>
15
15
  </div>
16
16
  `})}
17
17
  </div>
@@ -1,6 +1,5 @@
1
1
  import { LitElement, type TemplateResult } from 'lit';
2
- import './spools/SplitFlapSpool';
3
- import type { TSpool } from './types';
2
+ import type { TSplitFlapVariant, TSpool } from './types';
4
3
  export declare class SplitFlapBoard extends LitElement {
5
4
  static readonly styles: import("lit").CSSResult;
6
5
  /** 2-D grid of spool configs; defines what each position can show. Row-major order. */
@@ -10,7 +9,7 @@ export declare class SplitFlapBoard extends LitElement {
10
9
  /** Flip speed in ms, forwarded to every child spool. */
11
10
  speed: number;
12
11
  /** Visual variant forwarded to every child spool. */
13
- variant: 'minimal' | 'realistic';
12
+ variant: TSplitFlapVariant;
14
13
  /** Number of visible drum sides forwarded to every child spool (-1 = default). */
15
14
  visibleSideCount: number;
16
15
  /** True while at least one spool is still animating toward its target. */
@@ -19,6 +18,7 @@ export declare class SplitFlapBoard extends LitElement {
19
18
  private _getSpoolEls;
20
19
  private _checkAllSettled;
21
20
  private _dispatchBoardSettled;
21
+ private _getCurrentGrid;
22
22
  render(): TemplateResult;
23
23
  }
24
24
  declare global {
@@ -1,4 +1,4 @@
1
- import type { TSpool } from '../types';
1
+ import type { TBoardData, TLineInput, TSpool } from '../types';
2
2
  /**
3
3
  * Fill a uniform 2-D spool grid.
4
4
  *
@@ -19,12 +19,4 @@ export declare function spoolGrid(spool: TSpool | TSpool[], cols: number, rows:
19
19
  * @param lines - Text lines to display.
20
20
  * @param cols - Board width in columns.
21
21
  */
22
- export declare function fromLines(lines: TLineInput[], cols: number): {
23
- spools: TSpool[][];
24
- grid: string[][];
25
- };
26
- export type TLineInput = string | {
27
- text: string;
28
- bg?: string;
29
- color?: string;
30
- };
22
+ export declare function fromLines(lines: TLineInput[], cols: number): TBoardData;
@@ -1,3 +1,3 @@
1
1
  import type { TFlap } from '../types';
2
- /** Returns the public key used to address a flap inside a spool. */
2
+ /** Returns the key used to address a flap inside a spool. */
3
3
  export declare function getFlapKey(flap: TFlap): string;
@@ -1,10 +1,10 @@
1
1
  import { LitElement, type TemplateResult } from 'lit';
2
- import type { TSpool } from '../types';
2
+ import type { TSplitFlapVariant, TSpool } from '../types';
3
3
  import './SplitFlapSpoolMinimal';
4
4
  import './SplitFlapSpoolRealistic';
5
5
  export declare class SplitFlapSpool extends LitElement {
6
6
  static readonly styles: import("lit").CSSResult;
7
- variant: 'minimal' | 'realistic';
7
+ variant: TSplitFlapVariant;
8
8
  value: string;
9
9
  flaps: TSpool;
10
10
  speed: number;
@@ -16,6 +16,7 @@ export declare class SplitFlapSpool extends LitElement {
16
16
  /** Returns true when the given key exists in the loaded flaps. */
17
17
  hasKey(value: string): boolean;
18
18
  private _getActiveSpool;
19
+ private _renderVariant;
19
20
  render(): TemplateResult;
20
21
  }
21
22
  declare global {
@@ -36,13 +36,16 @@ export declare abstract class SplitFlapSpoolBase extends LitElement {
36
36
  private _doStep;
37
37
  private _scheduleAdvanceOrSettle;
38
38
  private _finishSettling;
39
- private _getRemainingAnimTime;
39
+ private _findFlapIndex;
40
+ private _getNextIndex;
41
+ private _isAnimating;
42
+ private _getRemainingAnimationTime;
40
43
  private _resetForEmptyFlaps;
41
44
  private _syncIndicesToFlaps;
42
45
  private _clearTimers;
43
- /** Renders the content of one flap half. Expects `.char-inner`, `.color-fill`, `.image-fill`, `.custom-fill` class names in the subclass stylesheet. */
46
+ /** Renders one flap half. Subclass stylesheets must define `.char-inner`, `.color-fill`, `.image-fill`, `.custom-fill`. */
44
47
  protected _renderHalf(flap: TFlap, half: 'top' | 'bottom'): TemplateResult;
45
- /** Returns current and previous flap for use in render. Returns null when flaps is empty. */
48
+ /** Returns current and previous flap for render. Null when flaps is empty. */
46
49
  protected _getFlaps(): {
47
50
  current: TFlap;
48
51
  prev: TFlap;
@@ -24,10 +24,10 @@ export declare class SplitFlapSpoolRealistic extends SplitFlapSpoolBase {
24
24
  disconnectedCallback(): void;
25
25
  private _clearWrapTimer;
26
26
  private _syncSlotState;
27
- /**
28
- * Snap back to prevIdx (instant), then transition to nextIdx.
29
- * For forward-only adjacent steps the drum advances by exactly one --angle.
30
- */
27
+ private _handleFlapChange;
28
+ private _handleIndexChange;
29
+ private _waitForPositionReset;
30
+ private _isWrapForwardStep;
31
31
  private _animateStep;
32
32
  private _getRenderCenter;
33
33
  render(): TemplateResult;
@@ -1,5 +1,7 @@
1
1
  import type { TemplateResult } from 'lit';
2
+ export type TSplitFlapVariant = 'minimal' | 'realistic';
2
3
  export type TSpool = TFlap[];
4
+ export type TGrid = string[][];
3
5
  export type TFlap = TFlapChar | TFlapColor | TFlapImage | TFlapCustom;
4
6
  export interface TFlapChar {
5
7
  type: 'char';
@@ -28,3 +30,19 @@ export interface TFlapCustom {
28
30
  top: TemplateResult;
29
31
  bottom: TemplateResult;
30
32
  }
33
+ export interface TBoardData {
34
+ spools: TSpool[][];
35
+ grid: TGrid;
36
+ }
37
+ export interface TLineConfig {
38
+ text: string;
39
+ bg?: string;
40
+ color?: string;
41
+ }
42
+ export type TLineInput = string | TLineConfig;
43
+ export interface TSpoolSettledDetail {
44
+ value: string;
45
+ }
46
+ export interface TBoardSettledDetail {
47
+ grid: TGrid;
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "split-flap-board",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "private": false,
5
5
  "description": "Web component that simulates a split-flap display inspired by airport and train station boards",
6
6
  "keywords": [