@boba-cli/spinner 0.1.0-alpha.2
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 +105 -0
- package/dist/index.cjs +159 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +143 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +144 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# @boba-cli/spinner
|
|
2
|
+
|
|
3
|
+
Animated spinner component for Boba terminal UIs. Port of Charmbracelet Bubbles spinner.
|
|
4
|
+
|
|
5
|
+
<img src="../../examples/spinner-demo.gif" width="950" alt="Spinner component demo" />
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @boba-cli/spinner
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { SpinnerModel, TickMsg, dot } from '@boba-cli/spinner'
|
|
17
|
+
import { Style } from '@boba-cli/chapstick'
|
|
18
|
+
import type { Cmd, Msg, Model } from '@boba-cli/tea'
|
|
19
|
+
|
|
20
|
+
// Create a spinner with custom style
|
|
21
|
+
const spinner = new SpinnerModel({
|
|
22
|
+
spinner: dot,
|
|
23
|
+
style: new Style().foreground('#7c3aed'),
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
// In your model's init, start the spinner
|
|
27
|
+
function init(): Cmd<Msg> {
|
|
28
|
+
return spinner.tick()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// In your update function, handle TickMsg
|
|
32
|
+
function update(msg: Msg): [MyModel, Cmd<Msg>] {
|
|
33
|
+
if (msg instanceof TickMsg) {
|
|
34
|
+
const [nextSpinner, cmd] = spinner.update(msg)
|
|
35
|
+
return [{ ...model, spinner: nextSpinner }, cmd]
|
|
36
|
+
}
|
|
37
|
+
return [model, null]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// In your view, render the spinner
|
|
41
|
+
function view(): string {
|
|
42
|
+
return `Loading ${spinner.view()}`
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Built-in Spinners
|
|
47
|
+
|
|
48
|
+
| Spinner | Preview |
|
|
49
|
+
| ----------- | ------------------------- |
|
|
50
|
+
| `line` | `\| / - \` |
|
|
51
|
+
| `dot` | `⣾ ⣽ ⣻ ⢿ ⡿ ⣟ ⣯ ⣷` |
|
|
52
|
+
| `miniDot` | `⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏` |
|
|
53
|
+
| `jump` | `⢄ ⢂ ⢁ ⡁ ⡈ ⡐ ⡠` |
|
|
54
|
+
| `pulse` | `█ ▓ ▒ ░` |
|
|
55
|
+
| `points` | `∙∙∙ ●∙∙ ∙●∙ ∙∙●` |
|
|
56
|
+
| `globe` | `🌍 🌎 🌏` |
|
|
57
|
+
| `moon` | `🌑 🌒 🌓 🌔 🌕 🌖 🌗 🌘` |
|
|
58
|
+
| `monkey` | `🙈 🙉 🙊` |
|
|
59
|
+
| `meter` | `▱▱▱ ▰▱▱ ▰▰▱ ▰▰▰` |
|
|
60
|
+
| `hamburger` | `☱ ☲ ☴` |
|
|
61
|
+
| `ellipsis` | `. .. ...` |
|
|
62
|
+
|
|
63
|
+
## Custom Spinners
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import type { Spinner } from '@boba-cli/spinner'
|
|
67
|
+
|
|
68
|
+
const customSpinner: Spinner = {
|
|
69
|
+
frames: ['◐', '◓', '◑', '◒'],
|
|
70
|
+
fps: 100, // milliseconds per frame
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const model = new SpinnerModel({ spinner: customSpinner })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## API
|
|
77
|
+
|
|
78
|
+
| Export | Description |
|
|
79
|
+
| ------------------- | --------------------------------- |
|
|
80
|
+
| `SpinnerModel` | Main component model |
|
|
81
|
+
| `Spinner` | Interface for spinner definitions |
|
|
82
|
+
| `TickMsg` | Message for animation ticks |
|
|
83
|
+
| `line`, `dot`, etc. | Built-in spinner animations |
|
|
84
|
+
|
|
85
|
+
### SpinnerModel Methods
|
|
86
|
+
|
|
87
|
+
| Method | Description |
|
|
88
|
+
| ---------------- | --------------------------------------- |
|
|
89
|
+
| `id()` | Unique ID for message routing |
|
|
90
|
+
| `tick()` | Command to start/continue animation |
|
|
91
|
+
| `update(msg)` | Handle messages, returns `[model, cmd]` |
|
|
92
|
+
| `view()` | Render current frame with style |
|
|
93
|
+
| `withSpinner(s)` | New model with different spinner |
|
|
94
|
+
| `withStyle(s)` | New model with different style |
|
|
95
|
+
|
|
96
|
+
## Scripts
|
|
97
|
+
|
|
98
|
+
- `pnpm -C packages/spinner build`
|
|
99
|
+
- `pnpm -C packages/spinner test`
|
|
100
|
+
- `pnpm -C packages/spinner lint`
|
|
101
|
+
- `pnpm -C packages/spinner generate:api-report`
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var tea = require('@boba-cli/tea');
|
|
4
|
+
var chapstick = require('@boba-cli/chapstick');
|
|
5
|
+
|
|
6
|
+
// src/spinner.ts
|
|
7
|
+
var line = {
|
|
8
|
+
frames: ["|", "/", "-", "\\"],
|
|
9
|
+
fps: 100
|
|
10
|
+
};
|
|
11
|
+
var dot = {
|
|
12
|
+
frames: ["\u28FE ", "\u28FD ", "\u28FB ", "\u28BF ", "\u287F ", "\u28DF ", "\u28EF ", "\u28F7 "],
|
|
13
|
+
fps: 100
|
|
14
|
+
};
|
|
15
|
+
var miniDot = {
|
|
16
|
+
frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
|
|
17
|
+
fps: 83
|
|
18
|
+
};
|
|
19
|
+
var jump = {
|
|
20
|
+
frames: ["\u2884", "\u2882", "\u2881", "\u2841", "\u2848", "\u2850", "\u2860"],
|
|
21
|
+
fps: 100
|
|
22
|
+
};
|
|
23
|
+
var pulse = {
|
|
24
|
+
frames: ["\u2588", "\u2593", "\u2592", "\u2591"],
|
|
25
|
+
fps: 125
|
|
26
|
+
};
|
|
27
|
+
var points = {
|
|
28
|
+
frames: ["\u2219\u2219\u2219", "\u25CF\u2219\u2219", "\u2219\u25CF\u2219", "\u2219\u2219\u25CF"],
|
|
29
|
+
fps: 143
|
|
30
|
+
};
|
|
31
|
+
var globe = {
|
|
32
|
+
frames: ["\u{1F30D}", "\u{1F30E}", "\u{1F30F}"],
|
|
33
|
+
fps: 250
|
|
34
|
+
};
|
|
35
|
+
var moon = {
|
|
36
|
+
frames: ["\u{1F311}", "\u{1F312}", "\u{1F313}", "\u{1F314}", "\u{1F315}", "\u{1F316}", "\u{1F317}", "\u{1F318}"],
|
|
37
|
+
fps: 125
|
|
38
|
+
};
|
|
39
|
+
var monkey = {
|
|
40
|
+
frames: ["\u{1F648}", "\u{1F649}", "\u{1F64A}"],
|
|
41
|
+
fps: 333
|
|
42
|
+
};
|
|
43
|
+
var meter = {
|
|
44
|
+
frames: ["\u25B1\u25B1\u25B1", "\u25B0\u25B1\u25B1", "\u25B0\u25B0\u25B1", "\u25B0\u25B0\u25B0", "\u25B0\u25B0\u25B1", "\u25B0\u25B1\u25B1", "\u25B1\u25B1\u25B1"],
|
|
45
|
+
fps: 143
|
|
46
|
+
};
|
|
47
|
+
var hamburger = {
|
|
48
|
+
frames: ["\u2631", "\u2632", "\u2634", "\u2632"],
|
|
49
|
+
fps: 333
|
|
50
|
+
};
|
|
51
|
+
var ellipsis = {
|
|
52
|
+
frames: ["", ".", "..", "..."],
|
|
53
|
+
fps: 333
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// src/messages.ts
|
|
57
|
+
var TickMsg = class {
|
|
58
|
+
constructor(time, id, tag) {
|
|
59
|
+
this.time = time;
|
|
60
|
+
this.id = id;
|
|
61
|
+
this.tag = tag;
|
|
62
|
+
}
|
|
63
|
+
_tag = "spinner:tick";
|
|
64
|
+
};
|
|
65
|
+
var lastId = 0;
|
|
66
|
+
function nextId() {
|
|
67
|
+
return ++lastId;
|
|
68
|
+
}
|
|
69
|
+
var SpinnerModel = class _SpinnerModel {
|
|
70
|
+
spinner;
|
|
71
|
+
style;
|
|
72
|
+
#frame;
|
|
73
|
+
#id;
|
|
74
|
+
#tag;
|
|
75
|
+
constructor(options = {}, state) {
|
|
76
|
+
this.spinner = options.spinner ?? line;
|
|
77
|
+
this.style = options.style ?? new chapstick.Style();
|
|
78
|
+
this.#frame = state?.frame ?? 0;
|
|
79
|
+
this.#id = state?.id ?? nextId();
|
|
80
|
+
this.#tag = state?.tag ?? 0;
|
|
81
|
+
}
|
|
82
|
+
/** Unique ID for this spinner instance (for message routing). */
|
|
83
|
+
id() {
|
|
84
|
+
return this.#id;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Command to start/continue the spinner animation.
|
|
88
|
+
* Call this in your model's init() or after handling a TickMsg.
|
|
89
|
+
*/
|
|
90
|
+
tick() {
|
|
91
|
+
const id = this.#id;
|
|
92
|
+
const tag = this.#tag;
|
|
93
|
+
return tea.tick(this.spinner.fps, (time) => new TickMsg(time, id, tag));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Update the model in response to messages.
|
|
97
|
+
* Returns a new model and an optional command.
|
|
98
|
+
*/
|
|
99
|
+
update(msg) {
|
|
100
|
+
if (!(msg instanceof TickMsg)) {
|
|
101
|
+
return [this, null];
|
|
102
|
+
}
|
|
103
|
+
if (msg.id > 0 && msg.id !== this.#id) {
|
|
104
|
+
return [this, null];
|
|
105
|
+
}
|
|
106
|
+
if (msg.tag > 0 && msg.tag !== this.#tag) {
|
|
107
|
+
return [this, null];
|
|
108
|
+
}
|
|
109
|
+
let nextFrame = this.#frame + 1;
|
|
110
|
+
if (nextFrame >= this.spinner.frames.length) {
|
|
111
|
+
nextFrame = 0;
|
|
112
|
+
}
|
|
113
|
+
const nextTag = this.#tag + 1;
|
|
114
|
+
const next = new _SpinnerModel(
|
|
115
|
+
{ spinner: this.spinner, style: this.style },
|
|
116
|
+
{ frame: nextFrame, id: this.#id, tag: nextTag }
|
|
117
|
+
);
|
|
118
|
+
return [next, next.tick()];
|
|
119
|
+
}
|
|
120
|
+
/** Render the current frame with styling. */
|
|
121
|
+
view() {
|
|
122
|
+
const frame = this.spinner.frames[this.#frame];
|
|
123
|
+
if (frame === void 0) {
|
|
124
|
+
return "(error)";
|
|
125
|
+
}
|
|
126
|
+
return this.style.render(frame);
|
|
127
|
+
}
|
|
128
|
+
/** Create a new model with a different spinner. */
|
|
129
|
+
withSpinner(spinner) {
|
|
130
|
+
return new _SpinnerModel(
|
|
131
|
+
{ spinner, style: this.style },
|
|
132
|
+
{ frame: 0, id: this.#id, tag: this.#tag }
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
/** Create a new model with a different style. */
|
|
136
|
+
withStyle(style) {
|
|
137
|
+
return new _SpinnerModel(
|
|
138
|
+
{ spinner: this.spinner, style },
|
|
139
|
+
{ frame: this.#frame, id: this.#id, tag: this.#tag }
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
exports.SpinnerModel = SpinnerModel;
|
|
145
|
+
exports.TickMsg = TickMsg;
|
|
146
|
+
exports.dot = dot;
|
|
147
|
+
exports.ellipsis = ellipsis;
|
|
148
|
+
exports.globe = globe;
|
|
149
|
+
exports.hamburger = hamburger;
|
|
150
|
+
exports.jump = jump;
|
|
151
|
+
exports.line = line;
|
|
152
|
+
exports.meter = meter;
|
|
153
|
+
exports.miniDot = miniDot;
|
|
154
|
+
exports.monkey = monkey;
|
|
155
|
+
exports.moon = moon;
|
|
156
|
+
exports.points = points;
|
|
157
|
+
exports.pulse = pulse;
|
|
158
|
+
//# sourceMappingURL=index.cjs.map
|
|
159
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/spinner.ts","../src/messages.ts","../src/model.ts"],"names":["Style","tick"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Spinner animation definition with frames and timing.\n * @public\n */\nexport interface Spinner {\n /** Animation frames to cycle through */\n readonly frames: readonly string[]\n /** Milliseconds per frame */\n readonly fps: number\n}\n\n/**\n * Classic line spinner\n * @public\n */\nexport const line: Spinner = {\n frames: ['|', '/', '-', '\\\\'],\n fps: 100,\n}\n\n/**\n * Braille dot spinner\n * @public\n */\nexport const dot: Spinner = {\n frames: ['⣾ ', '⣽ ', '⣻ ', '⢿ ', '⡿ ', '⣟ ', '⣯ ', '⣷ '],\n fps: 100,\n}\n\n/**\n * Mini braille dot spinner\n * @public\n */\nexport const miniDot: Spinner = {\n frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],\n fps: 83,\n}\n\n/**\n * Jumping dot spinner\n * @public\n */\nexport const jump: Spinner = {\n frames: ['⢄', '⢂', '⢁', '⡁', '⡈', '⡐', '⡠'],\n fps: 100,\n}\n\n/**\n * Pulsing block spinner\n * @public\n */\nexport const pulse: Spinner = {\n frames: ['█', '▓', '▒', '░'],\n fps: 125,\n}\n\n/**\n * Moving dot points\n * @public\n */\nexport const points: Spinner = {\n frames: ['∙∙∙', '●∙∙', '∙●∙', '∙∙●'],\n fps: 143,\n}\n\n/**\n * Rotating globe emoji\n * @public\n */\nexport const globe: Spinner = {\n frames: ['🌍', '🌎', '🌏'],\n fps: 250,\n}\n\n/**\n * Moon phases\n * @public\n */\nexport const moon: Spinner = {\n frames: ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'],\n fps: 125,\n}\n\n/**\n * See no evil, hear no evil, speak no evil\n * @public\n */\nexport const monkey: Spinner = {\n frames: ['🙈', '🙉', '🙊'],\n fps: 333,\n}\n\n/**\n * Progress meter style\n * @public\n */\nexport const meter: Spinner = {\n frames: ['▱▱▱', '▰▱▱', '▰▰▱', '▰▰▰', '▰▰▱', '▰▱▱', '▱▱▱'],\n fps: 143,\n}\n\n/**\n * Hamburger menu animation\n * @public\n */\nexport const hamburger: Spinner = {\n frames: ['☱', '☲', '☴', '☲'],\n fps: 333,\n}\n\n/**\n * Growing ellipsis\n * @public\n */\nexport const ellipsis: Spinner = {\n frames: ['', '.', '..', '...'],\n fps: 333,\n}\n","/**\n * Message indicating the spinner should advance one frame.\n * @public\n */\nexport class TickMsg {\n readonly _tag = 'spinner:tick'\n\n constructor(\n /** The time at which the tick occurred */\n public readonly time: Date,\n /** The ID of the spinner this message belongs to */\n public readonly id: number,\n /** Internal tag for deduplication */\n public readonly tag: number,\n ) {}\n}\n","import { tick, type Cmd, type Msg } from '@boba-cli/tea'\nimport { Style } from '@boba-cli/chapstick'\nimport { type Spinner, line } from './spinner.js'\nimport { TickMsg } from './messages.js'\n\n// Module-level ID counter for unique spinner identification\nlet lastId = 0\nfunction nextId(): number {\n return ++lastId\n}\n\n/**\n * Options for creating a SpinnerModel.\n * @public\n */\nexport interface SpinnerOptions {\n /** Spinner animation to use (default: line) */\n spinner?: Spinner\n /** Style for rendering the spinner */\n style?: Style\n}\n\n/**\n * Spinner component model.\n *\n * Use `tick()` to start the animation, then handle `TickMsg` in your\n * update function by calling `model.update(msg)`.\n *\n * @public\n */\nexport class SpinnerModel {\n readonly spinner: Spinner\n readonly style: Style\n readonly #frame: number\n readonly #id: number\n readonly #tag: number\n\n constructor(\n options: SpinnerOptions = {},\n state?: { frame: number; id: number; tag: number },\n ) {\n this.spinner = options.spinner ?? line\n this.style = options.style ?? new Style()\n this.#frame = state?.frame ?? 0\n this.#id = state?.id ?? nextId()\n this.#tag = state?.tag ?? 0\n }\n\n /** Unique ID for this spinner instance (for message routing). */\n id(): number {\n return this.#id\n }\n\n /**\n * Command to start/continue the spinner animation.\n * Call this in your model's init() or after handling a TickMsg.\n */\n tick(): Cmd<TickMsg> {\n const id = this.#id\n const tag = this.#tag\n return tick(this.spinner.fps, (time) => new TickMsg(time, id, tag))\n }\n\n /**\n * Update the model in response to messages.\n * Returns a new model and an optional command.\n */\n update(msg: Msg): [SpinnerModel, Cmd<Msg>] {\n if (!(msg instanceof TickMsg)) {\n return [this, null]\n }\n\n // If an ID is set and doesn't match, reject the message\n if (msg.id > 0 && msg.id !== this.#id) {\n return [this, null]\n }\n\n // If a tag is set and doesn't match, reject the message\n // This prevents duplicate ticks from causing too-fast animation\n if (msg.tag > 0 && msg.tag !== this.#tag) {\n return [this, null]\n }\n\n // Advance frame\n let nextFrame = this.#frame + 1\n if (nextFrame >= this.spinner.frames.length) {\n nextFrame = 0\n }\n\n const nextTag = this.#tag + 1\n\n const next = new SpinnerModel(\n { spinner: this.spinner, style: this.style },\n { frame: nextFrame, id: this.#id, tag: nextTag },\n )\n\n return [next, next.tick()]\n }\n\n /** Render the current frame with styling. */\n view(): string {\n const frame = this.spinner.frames[this.#frame]\n if (frame === undefined) {\n return '(error)'\n }\n return this.style.render(frame)\n }\n\n /** Create a new model with a different spinner. */\n withSpinner(spinner: Spinner): SpinnerModel {\n return new SpinnerModel(\n { spinner, style: this.style },\n { frame: 0, id: this.#id, tag: this.#tag },\n )\n }\n\n /** Create a new model with a different style. */\n withStyle(style: Style): SpinnerModel {\n return new SpinnerModel(\n { spinner: this.spinner, style },\n { frame: this.#frame, id: this.#id, tag: this.#tag },\n )\n }\n}\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Cmd, Msg } from '@boba-cli/tea';
|
|
2
|
+
import { Style } from '@boba-cli/chapstick';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Spinner animation definition with frames and timing.
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
interface Spinner {
|
|
9
|
+
/** Animation frames to cycle through */
|
|
10
|
+
readonly frames: readonly string[];
|
|
11
|
+
/** Milliseconds per frame */
|
|
12
|
+
readonly fps: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Classic line spinner
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
declare const line: Spinner;
|
|
19
|
+
/**
|
|
20
|
+
* Braille dot spinner
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
declare const dot: Spinner;
|
|
24
|
+
/**
|
|
25
|
+
* Mini braille dot spinner
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
declare const miniDot: Spinner;
|
|
29
|
+
/**
|
|
30
|
+
* Jumping dot spinner
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
declare const jump: Spinner;
|
|
34
|
+
/**
|
|
35
|
+
* Pulsing block spinner
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
declare const pulse: Spinner;
|
|
39
|
+
/**
|
|
40
|
+
* Moving dot points
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
declare const points: Spinner;
|
|
44
|
+
/**
|
|
45
|
+
* Rotating globe emoji
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
declare const globe: Spinner;
|
|
49
|
+
/**
|
|
50
|
+
* Moon phases
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
declare const moon: Spinner;
|
|
54
|
+
/**
|
|
55
|
+
* See no evil, hear no evil, speak no evil
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
declare const monkey: Spinner;
|
|
59
|
+
/**
|
|
60
|
+
* Progress meter style
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
declare const meter: Spinner;
|
|
64
|
+
/**
|
|
65
|
+
* Hamburger menu animation
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
declare const hamburger: Spinner;
|
|
69
|
+
/**
|
|
70
|
+
* Growing ellipsis
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
73
|
+
declare const ellipsis: Spinner;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Message indicating the spinner should advance one frame.
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
declare class TickMsg {
|
|
80
|
+
/** The time at which the tick occurred */
|
|
81
|
+
readonly time: Date;
|
|
82
|
+
/** The ID of the spinner this message belongs to */
|
|
83
|
+
readonly id: number;
|
|
84
|
+
/** Internal tag for deduplication */
|
|
85
|
+
readonly tag: number;
|
|
86
|
+
readonly _tag = "spinner:tick";
|
|
87
|
+
constructor(
|
|
88
|
+
/** The time at which the tick occurred */
|
|
89
|
+
time: Date,
|
|
90
|
+
/** The ID of the spinner this message belongs to */
|
|
91
|
+
id: number,
|
|
92
|
+
/** Internal tag for deduplication */
|
|
93
|
+
tag: number);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Options for creating a SpinnerModel.
|
|
98
|
+
* @public
|
|
99
|
+
*/
|
|
100
|
+
interface SpinnerOptions {
|
|
101
|
+
/** Spinner animation to use (default: line) */
|
|
102
|
+
spinner?: Spinner;
|
|
103
|
+
/** Style for rendering the spinner */
|
|
104
|
+
style?: Style;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Spinner component model.
|
|
108
|
+
*
|
|
109
|
+
* Use `tick()` to start the animation, then handle `TickMsg` in your
|
|
110
|
+
* update function by calling `model.update(msg)`.
|
|
111
|
+
*
|
|
112
|
+
* @public
|
|
113
|
+
*/
|
|
114
|
+
declare class SpinnerModel {
|
|
115
|
+
#private;
|
|
116
|
+
readonly spinner: Spinner;
|
|
117
|
+
readonly style: Style;
|
|
118
|
+
constructor(options?: SpinnerOptions, state?: {
|
|
119
|
+
frame: number;
|
|
120
|
+
id: number;
|
|
121
|
+
tag: number;
|
|
122
|
+
});
|
|
123
|
+
/** Unique ID for this spinner instance (for message routing). */
|
|
124
|
+
id(): number;
|
|
125
|
+
/**
|
|
126
|
+
* Command to start/continue the spinner animation.
|
|
127
|
+
* Call this in your model's init() or after handling a TickMsg.
|
|
128
|
+
*/
|
|
129
|
+
tick(): Cmd<TickMsg>;
|
|
130
|
+
/**
|
|
131
|
+
* Update the model in response to messages.
|
|
132
|
+
* Returns a new model and an optional command.
|
|
133
|
+
*/
|
|
134
|
+
update(msg: Msg): [SpinnerModel, Cmd<Msg>];
|
|
135
|
+
/** Render the current frame with styling. */
|
|
136
|
+
view(): string;
|
|
137
|
+
/** Create a new model with a different spinner. */
|
|
138
|
+
withSpinner(spinner: Spinner): SpinnerModel;
|
|
139
|
+
/** Create a new model with a different style. */
|
|
140
|
+
withStyle(style: Style): SpinnerModel;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { type Spinner, SpinnerModel, type SpinnerOptions, TickMsg, dot, ellipsis, globe, hamburger, jump, line, meter, miniDot, monkey, moon, points, pulse };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { Cmd, Msg } from '@boba-cli/tea';
|
|
2
|
+
import { Style } from '@boba-cli/chapstick';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Spinner animation definition with frames and timing.
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
interface Spinner {
|
|
9
|
+
/** Animation frames to cycle through */
|
|
10
|
+
readonly frames: readonly string[];
|
|
11
|
+
/** Milliseconds per frame */
|
|
12
|
+
readonly fps: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Classic line spinner
|
|
16
|
+
* @public
|
|
17
|
+
*/
|
|
18
|
+
declare const line: Spinner;
|
|
19
|
+
/**
|
|
20
|
+
* Braille dot spinner
|
|
21
|
+
* @public
|
|
22
|
+
*/
|
|
23
|
+
declare const dot: Spinner;
|
|
24
|
+
/**
|
|
25
|
+
* Mini braille dot spinner
|
|
26
|
+
* @public
|
|
27
|
+
*/
|
|
28
|
+
declare const miniDot: Spinner;
|
|
29
|
+
/**
|
|
30
|
+
* Jumping dot spinner
|
|
31
|
+
* @public
|
|
32
|
+
*/
|
|
33
|
+
declare const jump: Spinner;
|
|
34
|
+
/**
|
|
35
|
+
* Pulsing block spinner
|
|
36
|
+
* @public
|
|
37
|
+
*/
|
|
38
|
+
declare const pulse: Spinner;
|
|
39
|
+
/**
|
|
40
|
+
* Moving dot points
|
|
41
|
+
* @public
|
|
42
|
+
*/
|
|
43
|
+
declare const points: Spinner;
|
|
44
|
+
/**
|
|
45
|
+
* Rotating globe emoji
|
|
46
|
+
* @public
|
|
47
|
+
*/
|
|
48
|
+
declare const globe: Spinner;
|
|
49
|
+
/**
|
|
50
|
+
* Moon phases
|
|
51
|
+
* @public
|
|
52
|
+
*/
|
|
53
|
+
declare const moon: Spinner;
|
|
54
|
+
/**
|
|
55
|
+
* See no evil, hear no evil, speak no evil
|
|
56
|
+
* @public
|
|
57
|
+
*/
|
|
58
|
+
declare const monkey: Spinner;
|
|
59
|
+
/**
|
|
60
|
+
* Progress meter style
|
|
61
|
+
* @public
|
|
62
|
+
*/
|
|
63
|
+
declare const meter: Spinner;
|
|
64
|
+
/**
|
|
65
|
+
* Hamburger menu animation
|
|
66
|
+
* @public
|
|
67
|
+
*/
|
|
68
|
+
declare const hamburger: Spinner;
|
|
69
|
+
/**
|
|
70
|
+
* Growing ellipsis
|
|
71
|
+
* @public
|
|
72
|
+
*/
|
|
73
|
+
declare const ellipsis: Spinner;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Message indicating the spinner should advance one frame.
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
declare class TickMsg {
|
|
80
|
+
/** The time at which the tick occurred */
|
|
81
|
+
readonly time: Date;
|
|
82
|
+
/** The ID of the spinner this message belongs to */
|
|
83
|
+
readonly id: number;
|
|
84
|
+
/** Internal tag for deduplication */
|
|
85
|
+
readonly tag: number;
|
|
86
|
+
readonly _tag = "spinner:tick";
|
|
87
|
+
constructor(
|
|
88
|
+
/** The time at which the tick occurred */
|
|
89
|
+
time: Date,
|
|
90
|
+
/** The ID of the spinner this message belongs to */
|
|
91
|
+
id: number,
|
|
92
|
+
/** Internal tag for deduplication */
|
|
93
|
+
tag: number);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Options for creating a SpinnerModel.
|
|
98
|
+
* @public
|
|
99
|
+
*/
|
|
100
|
+
interface SpinnerOptions {
|
|
101
|
+
/** Spinner animation to use (default: line) */
|
|
102
|
+
spinner?: Spinner;
|
|
103
|
+
/** Style for rendering the spinner */
|
|
104
|
+
style?: Style;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Spinner component model.
|
|
108
|
+
*
|
|
109
|
+
* Use `tick()` to start the animation, then handle `TickMsg` in your
|
|
110
|
+
* update function by calling `model.update(msg)`.
|
|
111
|
+
*
|
|
112
|
+
* @public
|
|
113
|
+
*/
|
|
114
|
+
declare class SpinnerModel {
|
|
115
|
+
#private;
|
|
116
|
+
readonly spinner: Spinner;
|
|
117
|
+
readonly style: Style;
|
|
118
|
+
constructor(options?: SpinnerOptions, state?: {
|
|
119
|
+
frame: number;
|
|
120
|
+
id: number;
|
|
121
|
+
tag: number;
|
|
122
|
+
});
|
|
123
|
+
/** Unique ID for this spinner instance (for message routing). */
|
|
124
|
+
id(): number;
|
|
125
|
+
/**
|
|
126
|
+
* Command to start/continue the spinner animation.
|
|
127
|
+
* Call this in your model's init() or after handling a TickMsg.
|
|
128
|
+
*/
|
|
129
|
+
tick(): Cmd<TickMsg>;
|
|
130
|
+
/**
|
|
131
|
+
* Update the model in response to messages.
|
|
132
|
+
* Returns a new model and an optional command.
|
|
133
|
+
*/
|
|
134
|
+
update(msg: Msg): [SpinnerModel, Cmd<Msg>];
|
|
135
|
+
/** Render the current frame with styling. */
|
|
136
|
+
view(): string;
|
|
137
|
+
/** Create a new model with a different spinner. */
|
|
138
|
+
withSpinner(spinner: Spinner): SpinnerModel;
|
|
139
|
+
/** Create a new model with a different style. */
|
|
140
|
+
withStyle(style: Style): SpinnerModel;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { type Spinner, SpinnerModel, type SpinnerOptions, TickMsg, dot, ellipsis, globe, hamburger, jump, line, meter, miniDot, monkey, moon, points, pulse };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { tick } from '@boba-cli/tea';
|
|
2
|
+
import { Style } from '@boba-cli/chapstick';
|
|
3
|
+
|
|
4
|
+
// src/spinner.ts
|
|
5
|
+
var line = {
|
|
6
|
+
frames: ["|", "/", "-", "\\"],
|
|
7
|
+
fps: 100
|
|
8
|
+
};
|
|
9
|
+
var dot = {
|
|
10
|
+
frames: ["\u28FE ", "\u28FD ", "\u28FB ", "\u28BF ", "\u287F ", "\u28DF ", "\u28EF ", "\u28F7 "],
|
|
11
|
+
fps: 100
|
|
12
|
+
};
|
|
13
|
+
var miniDot = {
|
|
14
|
+
frames: ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"],
|
|
15
|
+
fps: 83
|
|
16
|
+
};
|
|
17
|
+
var jump = {
|
|
18
|
+
frames: ["\u2884", "\u2882", "\u2881", "\u2841", "\u2848", "\u2850", "\u2860"],
|
|
19
|
+
fps: 100
|
|
20
|
+
};
|
|
21
|
+
var pulse = {
|
|
22
|
+
frames: ["\u2588", "\u2593", "\u2592", "\u2591"],
|
|
23
|
+
fps: 125
|
|
24
|
+
};
|
|
25
|
+
var points = {
|
|
26
|
+
frames: ["\u2219\u2219\u2219", "\u25CF\u2219\u2219", "\u2219\u25CF\u2219", "\u2219\u2219\u25CF"],
|
|
27
|
+
fps: 143
|
|
28
|
+
};
|
|
29
|
+
var globe = {
|
|
30
|
+
frames: ["\u{1F30D}", "\u{1F30E}", "\u{1F30F}"],
|
|
31
|
+
fps: 250
|
|
32
|
+
};
|
|
33
|
+
var moon = {
|
|
34
|
+
frames: ["\u{1F311}", "\u{1F312}", "\u{1F313}", "\u{1F314}", "\u{1F315}", "\u{1F316}", "\u{1F317}", "\u{1F318}"],
|
|
35
|
+
fps: 125
|
|
36
|
+
};
|
|
37
|
+
var monkey = {
|
|
38
|
+
frames: ["\u{1F648}", "\u{1F649}", "\u{1F64A}"],
|
|
39
|
+
fps: 333
|
|
40
|
+
};
|
|
41
|
+
var meter = {
|
|
42
|
+
frames: ["\u25B1\u25B1\u25B1", "\u25B0\u25B1\u25B1", "\u25B0\u25B0\u25B1", "\u25B0\u25B0\u25B0", "\u25B0\u25B0\u25B1", "\u25B0\u25B1\u25B1", "\u25B1\u25B1\u25B1"],
|
|
43
|
+
fps: 143
|
|
44
|
+
};
|
|
45
|
+
var hamburger = {
|
|
46
|
+
frames: ["\u2631", "\u2632", "\u2634", "\u2632"],
|
|
47
|
+
fps: 333
|
|
48
|
+
};
|
|
49
|
+
var ellipsis = {
|
|
50
|
+
frames: ["", ".", "..", "..."],
|
|
51
|
+
fps: 333
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/messages.ts
|
|
55
|
+
var TickMsg = class {
|
|
56
|
+
constructor(time, id, tag) {
|
|
57
|
+
this.time = time;
|
|
58
|
+
this.id = id;
|
|
59
|
+
this.tag = tag;
|
|
60
|
+
}
|
|
61
|
+
_tag = "spinner:tick";
|
|
62
|
+
};
|
|
63
|
+
var lastId = 0;
|
|
64
|
+
function nextId() {
|
|
65
|
+
return ++lastId;
|
|
66
|
+
}
|
|
67
|
+
var SpinnerModel = class _SpinnerModel {
|
|
68
|
+
spinner;
|
|
69
|
+
style;
|
|
70
|
+
#frame;
|
|
71
|
+
#id;
|
|
72
|
+
#tag;
|
|
73
|
+
constructor(options = {}, state) {
|
|
74
|
+
this.spinner = options.spinner ?? line;
|
|
75
|
+
this.style = options.style ?? new Style();
|
|
76
|
+
this.#frame = state?.frame ?? 0;
|
|
77
|
+
this.#id = state?.id ?? nextId();
|
|
78
|
+
this.#tag = state?.tag ?? 0;
|
|
79
|
+
}
|
|
80
|
+
/** Unique ID for this spinner instance (for message routing). */
|
|
81
|
+
id() {
|
|
82
|
+
return this.#id;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Command to start/continue the spinner animation.
|
|
86
|
+
* Call this in your model's init() or after handling a TickMsg.
|
|
87
|
+
*/
|
|
88
|
+
tick() {
|
|
89
|
+
const id = this.#id;
|
|
90
|
+
const tag = this.#tag;
|
|
91
|
+
return tick(this.spinner.fps, (time) => new TickMsg(time, id, tag));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Update the model in response to messages.
|
|
95
|
+
* Returns a new model and an optional command.
|
|
96
|
+
*/
|
|
97
|
+
update(msg) {
|
|
98
|
+
if (!(msg instanceof TickMsg)) {
|
|
99
|
+
return [this, null];
|
|
100
|
+
}
|
|
101
|
+
if (msg.id > 0 && msg.id !== this.#id) {
|
|
102
|
+
return [this, null];
|
|
103
|
+
}
|
|
104
|
+
if (msg.tag > 0 && msg.tag !== this.#tag) {
|
|
105
|
+
return [this, null];
|
|
106
|
+
}
|
|
107
|
+
let nextFrame = this.#frame + 1;
|
|
108
|
+
if (nextFrame >= this.spinner.frames.length) {
|
|
109
|
+
nextFrame = 0;
|
|
110
|
+
}
|
|
111
|
+
const nextTag = this.#tag + 1;
|
|
112
|
+
const next = new _SpinnerModel(
|
|
113
|
+
{ spinner: this.spinner, style: this.style },
|
|
114
|
+
{ frame: nextFrame, id: this.#id, tag: nextTag }
|
|
115
|
+
);
|
|
116
|
+
return [next, next.tick()];
|
|
117
|
+
}
|
|
118
|
+
/** Render the current frame with styling. */
|
|
119
|
+
view() {
|
|
120
|
+
const frame = this.spinner.frames[this.#frame];
|
|
121
|
+
if (frame === void 0) {
|
|
122
|
+
return "(error)";
|
|
123
|
+
}
|
|
124
|
+
return this.style.render(frame);
|
|
125
|
+
}
|
|
126
|
+
/** Create a new model with a different spinner. */
|
|
127
|
+
withSpinner(spinner) {
|
|
128
|
+
return new _SpinnerModel(
|
|
129
|
+
{ spinner, style: this.style },
|
|
130
|
+
{ frame: 0, id: this.#id, tag: this.#tag }
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
/** Create a new model with a different style. */
|
|
134
|
+
withStyle(style) {
|
|
135
|
+
return new _SpinnerModel(
|
|
136
|
+
{ spinner: this.spinner, style },
|
|
137
|
+
{ frame: this.#frame, id: this.#id, tag: this.#tag }
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export { SpinnerModel, TickMsg, dot, ellipsis, globe, hamburger, jump, line, meter, miniDot, monkey, moon, points, pulse };
|
|
143
|
+
//# sourceMappingURL=index.js.map
|
|
144
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/spinner.ts","../src/messages.ts","../src/model.ts"],"names":[],"mappings":";;;;AAeO,IAAM,IAAA,GAAgB;AAAA,EAC3B,MAAA,EAAQ,CAAC,GAAA,EAAK,GAAA,EAAK,KAAK,IAAI,CAAA;AAAA,EAC5B,GAAA,EAAK;AACP;AAMO,IAAM,GAAA,GAAe;AAAA,EAC1B,MAAA,EAAQ,CAAC,SAAA,EAAM,SAAA,EAAM,WAAM,SAAA,EAAM,SAAA,EAAM,SAAA,EAAM,SAAA,EAAM,SAAI,CAAA;AAAA,EACvD,GAAA,EAAK;AACP;AAMO,IAAM,OAAA,GAAmB;AAAA,EAC9B,MAAA,EAAQ,CAAC,QAAA,EAAK,QAAA,EAAK,QAAA,EAAK,QAAA,EAAK,QAAA,EAAK,QAAA,EAAK,QAAA,EAAK,QAAA,EAAK,QAAA,EAAK,QAAG,CAAA;AAAA,EACzD,GAAA,EAAK;AACP;AAMO,IAAM,IAAA,GAAgB;AAAA,EAC3B,MAAA,EAAQ,CAAC,QAAA,EAAK,QAAA,EAAK,UAAK,QAAA,EAAK,QAAA,EAAK,UAAK,QAAG,CAAA;AAAA,EAC1C,GAAA,EAAK;AACP;AAMO,IAAM,KAAA,GAAiB;AAAA,EAC5B,MAAA,EAAQ,CAAC,QAAA,EAAK,QAAA,EAAK,UAAK,QAAG,CAAA;AAAA,EAC3B,GAAA,EAAK;AACP;AAMO,IAAM,MAAA,GAAkB;AAAA,EAC7B,MAAA,EAAQ,CAAC,oBAAA,EAAO,oBAAA,EAAO,sBAAO,oBAAK,CAAA;AAAA,EACnC,GAAA,EAAK;AACP;AAMO,IAAM,KAAA,GAAiB;AAAA,EAC5B,MAAA,EAAQ,CAAC,WAAA,EAAM,WAAA,EAAM,WAAI,CAAA;AAAA,EACzB,GAAA,EAAK;AACP;AAMO,IAAM,IAAA,GAAgB;AAAA,EAC3B,MAAA,EAAQ,CAAC,WAAA,EAAM,WAAA,EAAM,aAAM,WAAA,EAAM,WAAA,EAAM,WAAA,EAAM,WAAA,EAAM,WAAI,CAAA;AAAA,EACvD,GAAA,EAAK;AACP;AAMO,IAAM,MAAA,GAAkB;AAAA,EAC7B,MAAA,EAAQ,CAAC,WAAA,EAAM,WAAA,EAAM,WAAI,CAAA;AAAA,EACzB,GAAA,EAAK;AACP;AAMO,IAAM,KAAA,GAAiB;AAAA,EAC5B,MAAA,EAAQ,CAAC,oBAAA,EAAO,oBAAA,EAAO,sBAAO,oBAAA,EAAO,oBAAA,EAAO,sBAAO,oBAAK,CAAA;AAAA,EACxD,GAAA,EAAK;AACP;AAMO,IAAM,SAAA,GAAqB;AAAA,EAChC,MAAA,EAAQ,CAAC,QAAA,EAAK,QAAA,EAAK,UAAK,QAAG,CAAA;AAAA,EAC3B,GAAA,EAAK;AACP;AAMO,IAAM,QAAA,GAAoB;AAAA,EAC/B,MAAA,EAAQ,CAAC,EAAA,EAAI,GAAA,EAAK,MAAM,KAAK,CAAA;AAAA,EAC7B,GAAA,EAAK;AACP;;;ACjHO,IAAM,UAAN,MAAc;AAAA,EAGnB,WAAA,CAEkB,IAAA,EAEA,EAAA,EAEA,GAAA,EAChB;AALgB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAEA,IAAA,IAAA,CAAA,EAAA,GAAA,EAAA;AAEA,IAAA,IAAA,CAAA,GAAA,GAAA,GAAA;AAAA,EACf;AAAA,EATM,IAAA,GAAO,cAAA;AAUlB;ACTA,IAAI,MAAA,GAAS,CAAA;AACb,SAAS,MAAA,GAAiB;AACxB,EAAA,OAAO,EAAE,MAAA;AACX;AAqBO,IAAM,YAAA,GAAN,MAAM,aAAA,CAAa;AAAA,EACf,OAAA;AAAA,EACA,KAAA;AAAA,EACA,MAAA;AAAA,EACA,GAAA;AAAA,EACA,IAAA;AAAA,EAET,WAAA,CACE,OAAA,GAA0B,EAAC,EAC3B,KAAA,EACA;AACA,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,OAAA,IAAW,IAAA;AAClC,IAAA,IAAA,CAAK,KAAA,GAAQ,OAAA,CAAQ,KAAA,IAAS,IAAI,KAAA,EAAM;AACxC,IAAA,IAAA,CAAK,MAAA,GAAS,OAAO,KAAA,IAAS,CAAA;AAC9B,IAAA,IAAA,CAAK,GAAA,GAAM,KAAA,EAAO,EAAA,IAAM,MAAA,EAAO;AAC/B,IAAA,IAAA,CAAK,IAAA,GAAO,OAAO,GAAA,IAAO,CAAA;AAAA,EAC5B;AAAA;AAAA,EAGA,EAAA,GAAa;AACX,IAAA,OAAO,IAAA,CAAK,GAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,IAAA,GAAqB;AACnB,IAAA,MAAM,KAAK,IAAA,CAAK,GAAA;AAChB,IAAA,MAAM,MAAM,IAAA,CAAK,IAAA;AACjB,IAAA,OAAO,IAAA,CAAK,IAAA,CAAK,OAAA,CAAQ,GAAA,EAAK,CAAC,IAAA,KAAS,IAAI,OAAA,CAAQ,IAAA,EAAM,EAAA,EAAI,GAAG,CAAC,CAAA;AAAA,EACpE;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,GAAA,EAAoC;AACzC,IAAA,IAAI,EAAE,eAAe,OAAA,CAAA,EAAU;AAC7B,MAAA,OAAO,CAAC,MAAM,IAAI,CAAA;AAAA,IACpB;AAGA,IAAA,IAAI,IAAI,EAAA,GAAK,CAAA,IAAK,GAAA,CAAI,EAAA,KAAO,KAAK,GAAA,EAAK;AACrC,MAAA,OAAO,CAAC,MAAM,IAAI,CAAA;AAAA,IACpB;AAIA,IAAA,IAAI,IAAI,GAAA,GAAM,CAAA,IAAK,GAAA,CAAI,GAAA,KAAQ,KAAK,IAAA,EAAM;AACxC,MAAA,OAAO,CAAC,MAAM,IAAI,CAAA;AAAA,IACpB;AAGA,IAAA,IAAI,SAAA,GAAY,KAAK,MAAA,GAAS,CAAA;AAC9B,IAAA,IAAI,SAAA,IAAa,IAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,MAAA,EAAQ;AAC3C,MAAA,SAAA,GAAY,CAAA;AAAA,IACd;AAEA,IAAA,MAAM,OAAA,GAAU,KAAK,IAAA,GAAO,CAAA;AAE5B,IAAA,MAAM,OAAO,IAAI,aAAA;AAAA,MACf,EAAE,OAAA,EAAS,IAAA,CAAK,OAAA,EAAS,KAAA,EAAO,KAAK,KAAA,EAAM;AAAA,MAC3C,EAAE,KAAA,EAAO,SAAA,EAAW,IAAI,IAAA,CAAK,GAAA,EAAK,KAAK,OAAA;AAAQ,KACjD;AAEA,IAAA,OAAO,CAAC,IAAA,EAAM,IAAA,CAAK,IAAA,EAAM,CAAA;AAAA,EAC3B;AAAA;AAAA,EAGA,IAAA,GAAe;AACb,IAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,OAAA,CAAQ,MAAA,CAAO,KAAK,MAAM,CAAA;AAC7C,IAAA,IAAI,UAAU,MAAA,EAAW;AACvB,MAAA,OAAO,SAAA;AAAA,IACT;AACA,IAAA,OAAO,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,KAAK,CAAA;AAAA,EAChC;AAAA;AAAA,EAGA,YAAY,OAAA,EAAgC;AAC1C,IAAA,OAAO,IAAI,aAAA;AAAA,MACT,EAAE,OAAA,EAAS,KAAA,EAAO,IAAA,CAAK,KAAA,EAAM;AAAA,MAC7B,EAAE,OAAO,CAAA,EAAG,EAAA,EAAI,KAAK,GAAA,EAAK,GAAA,EAAK,KAAK,IAAA;AAAK,KAC3C;AAAA,EACF;AAAA;AAAA,EAGA,UAAU,KAAA,EAA4B;AACpC,IAAA,OAAO,IAAI,aAAA;AAAA,MACT,EAAE,OAAA,EAAS,IAAA,CAAK,OAAA,EAAS,KAAA,EAAM;AAAA,MAC/B,EAAE,OAAO,IAAA,CAAK,MAAA,EAAQ,IAAI,IAAA,CAAK,GAAA,EAAK,GAAA,EAAK,IAAA,CAAK,IAAA;AAAK,KACrD;AAAA,EACF;AACF","file":"index.js","sourcesContent":["/**\n * Spinner animation definition with frames and timing.\n * @public\n */\nexport interface Spinner {\n /** Animation frames to cycle through */\n readonly frames: readonly string[]\n /** Milliseconds per frame */\n readonly fps: number\n}\n\n/**\n * Classic line spinner\n * @public\n */\nexport const line: Spinner = {\n frames: ['|', '/', '-', '\\\\'],\n fps: 100,\n}\n\n/**\n * Braille dot spinner\n * @public\n */\nexport const dot: Spinner = {\n frames: ['⣾ ', '⣽ ', '⣻ ', '⢿ ', '⡿ ', '⣟ ', '⣯ ', '⣷ '],\n fps: 100,\n}\n\n/**\n * Mini braille dot spinner\n * @public\n */\nexport const miniDot: Spinner = {\n frames: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],\n fps: 83,\n}\n\n/**\n * Jumping dot spinner\n * @public\n */\nexport const jump: Spinner = {\n frames: ['⢄', '⢂', '⢁', '⡁', '⡈', '⡐', '⡠'],\n fps: 100,\n}\n\n/**\n * Pulsing block spinner\n * @public\n */\nexport const pulse: Spinner = {\n frames: ['█', '▓', '▒', '░'],\n fps: 125,\n}\n\n/**\n * Moving dot points\n * @public\n */\nexport const points: Spinner = {\n frames: ['∙∙∙', '●∙∙', '∙●∙', '∙∙●'],\n fps: 143,\n}\n\n/**\n * Rotating globe emoji\n * @public\n */\nexport const globe: Spinner = {\n frames: ['🌍', '🌎', '🌏'],\n fps: 250,\n}\n\n/**\n * Moon phases\n * @public\n */\nexport const moon: Spinner = {\n frames: ['🌑', '🌒', '🌓', '🌔', '🌕', '🌖', '🌗', '🌘'],\n fps: 125,\n}\n\n/**\n * See no evil, hear no evil, speak no evil\n * @public\n */\nexport const monkey: Spinner = {\n frames: ['🙈', '🙉', '🙊'],\n fps: 333,\n}\n\n/**\n * Progress meter style\n * @public\n */\nexport const meter: Spinner = {\n frames: ['▱▱▱', '▰▱▱', '▰▰▱', '▰▰▰', '▰▰▱', '▰▱▱', '▱▱▱'],\n fps: 143,\n}\n\n/**\n * Hamburger menu animation\n * @public\n */\nexport const hamburger: Spinner = {\n frames: ['☱', '☲', '☴', '☲'],\n fps: 333,\n}\n\n/**\n * Growing ellipsis\n * @public\n */\nexport const ellipsis: Spinner = {\n frames: ['', '.', '..', '...'],\n fps: 333,\n}\n","/**\n * Message indicating the spinner should advance one frame.\n * @public\n */\nexport class TickMsg {\n readonly _tag = 'spinner:tick'\n\n constructor(\n /** The time at which the tick occurred */\n public readonly time: Date,\n /** The ID of the spinner this message belongs to */\n public readonly id: number,\n /** Internal tag for deduplication */\n public readonly tag: number,\n ) {}\n}\n","import { tick, type Cmd, type Msg } from '@boba-cli/tea'\nimport { Style } from '@boba-cli/chapstick'\nimport { type Spinner, line } from './spinner.js'\nimport { TickMsg } from './messages.js'\n\n// Module-level ID counter for unique spinner identification\nlet lastId = 0\nfunction nextId(): number {\n return ++lastId\n}\n\n/**\n * Options for creating a SpinnerModel.\n * @public\n */\nexport interface SpinnerOptions {\n /** Spinner animation to use (default: line) */\n spinner?: Spinner\n /** Style for rendering the spinner */\n style?: Style\n}\n\n/**\n * Spinner component model.\n *\n * Use `tick()` to start the animation, then handle `TickMsg` in your\n * update function by calling `model.update(msg)`.\n *\n * @public\n */\nexport class SpinnerModel {\n readonly spinner: Spinner\n readonly style: Style\n readonly #frame: number\n readonly #id: number\n readonly #tag: number\n\n constructor(\n options: SpinnerOptions = {},\n state?: { frame: number; id: number; tag: number },\n ) {\n this.spinner = options.spinner ?? line\n this.style = options.style ?? new Style()\n this.#frame = state?.frame ?? 0\n this.#id = state?.id ?? nextId()\n this.#tag = state?.tag ?? 0\n }\n\n /** Unique ID for this spinner instance (for message routing). */\n id(): number {\n return this.#id\n }\n\n /**\n * Command to start/continue the spinner animation.\n * Call this in your model's init() or after handling a TickMsg.\n */\n tick(): Cmd<TickMsg> {\n const id = this.#id\n const tag = this.#tag\n return tick(this.spinner.fps, (time) => new TickMsg(time, id, tag))\n }\n\n /**\n * Update the model in response to messages.\n * Returns a new model and an optional command.\n */\n update(msg: Msg): [SpinnerModel, Cmd<Msg>] {\n if (!(msg instanceof TickMsg)) {\n return [this, null]\n }\n\n // If an ID is set and doesn't match, reject the message\n if (msg.id > 0 && msg.id !== this.#id) {\n return [this, null]\n }\n\n // If a tag is set and doesn't match, reject the message\n // This prevents duplicate ticks from causing too-fast animation\n if (msg.tag > 0 && msg.tag !== this.#tag) {\n return [this, null]\n }\n\n // Advance frame\n let nextFrame = this.#frame + 1\n if (nextFrame >= this.spinner.frames.length) {\n nextFrame = 0\n }\n\n const nextTag = this.#tag + 1\n\n const next = new SpinnerModel(\n { spinner: this.spinner, style: this.style },\n { frame: nextFrame, id: this.#id, tag: nextTag },\n )\n\n return [next, next.tick()]\n }\n\n /** Render the current frame with styling. */\n view(): string {\n const frame = this.spinner.frames[this.#frame]\n if (frame === undefined) {\n return '(error)'\n }\n return this.style.render(frame)\n }\n\n /** Create a new model with a different spinner. */\n withSpinner(spinner: Spinner): SpinnerModel {\n return new SpinnerModel(\n { spinner, style: this.style },\n { frame: 0, id: this.#id, tag: this.#tag },\n )\n }\n\n /** Create a new model with a different style. */\n withStyle(style: Style): SpinnerModel {\n return new SpinnerModel(\n { spinner: this.spinner, style },\n { frame: this.#frame, id: this.#id, tag: this.#tag },\n )\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@boba-cli/spinner",
|
|
3
|
+
"description": "Animated spinner component for Boba terminal UIs",
|
|
4
|
+
"version": "0.1.0-alpha.2",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"@boba-cli/chapstick": "0.1.0-alpha.2",
|
|
7
|
+
"@boba-cli/tea": "0.1.0-alpha.1"
|
|
8
|
+
},
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"typescript": "5.8.2",
|
|
11
|
+
"vitest": "^4.0.16"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.0.0"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"import": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"require": {
|
|
23
|
+
"types": "./dist/index.d.cts",
|
|
24
|
+
"default": "./dist/index.cjs"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"./package.json": "./package.json"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"main": "./dist/index.cjs",
|
|
33
|
+
"module": "./dist/index.js",
|
|
34
|
+
"type": "module",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"check:api-report": "pnpm run generate:api-report",
|
|
39
|
+
"check:eslint": "pnpm run lint",
|
|
40
|
+
"generate:api-report": "api-extractor run --local",
|
|
41
|
+
"lint": "eslint \"{src,test}/**/*.{ts,tsx}\"",
|
|
42
|
+
"test": "vitest run"
|
|
43
|
+
}
|
|
44
|
+
}
|