@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 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":";;;;;;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,IAAIA,eAAA,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,OAAOC,QAAA,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.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"]}
@@ -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 };
@@ -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
+ }