@interactive-inc/claude-funnel 0.7.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -133
- package/dist/bin.js +1417 -0
- package/dist/gateway/daemon.js +513 -0
- package/dist/highlights-eq9cgrbb.scm +604 -0
- package/dist/highlights-ghv9g403.scm +205 -0
- package/dist/highlights-hk7bwhj4.scm +284 -0
- package/dist/highlights-r812a2qc.scm +150 -0
- package/dist/highlights-x6tmsnaa.scm +115 -0
- package/dist/injections-73j83es3.scm +27 -0
- package/dist/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/lib/bin.ts +78 -0
- package/lib/{modules → cli}/router/to-request.ts +13 -20
- package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +27 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +40 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +41 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +22 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +23 -0
- package/lib/cli/routes/channels.$channel.connectors.$connector.ts +26 -0
- package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +92 -0
- package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +22 -0
- package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +63 -0
- package/lib/cli/routes/channels.$channel.connectors.ts +26 -0
- package/lib/cli/routes/channels.$channel.rename.$newName.ts +22 -0
- package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +34 -0
- package/lib/cli/routes/channels.$channel.ts +34 -0
- package/lib/cli/routes/channels.add.$channel.ts +33 -0
- package/lib/cli/routes/channels.remove.$channel.ts +20 -0
- package/lib/cli/routes/channels.ts +39 -0
- package/lib/cli/routes/claude.ts +69 -0
- package/lib/cli/routes/gateway.listeners.ts +41 -0
- package/lib/cli/routes/gateway.logs.ts +123 -0
- package/lib/{routes/gateway/restart.ts → cli/routes/gateway.restart.ts} +20 -5
- package/lib/cli/routes/gateway.run.ts +41 -0
- package/lib/cli/routes/gateway.start.ts +50 -0
- package/lib/cli/routes/gateway.status.ts +19 -0
- package/lib/cli/routes/gateway.stop.ts +32 -0
- package/lib/cli/routes/gateway.ts +55 -0
- package/lib/cli/routes/index.ts +202 -0
- package/lib/cli/routes/profiles.$profile.as-default.ts +22 -0
- package/lib/cli/routes/profiles.$profile.rename.$newName.ts +22 -0
- package/lib/cli/routes/profiles.$profile.run.ts +36 -0
- package/lib/cli/routes/profiles.add.$profile.ts +46 -0
- package/lib/cli/routes/profiles.remove.$profile.ts +20 -0
- package/lib/cli/routes/profiles.set.$profile.ts +46 -0
- package/lib/cli/routes/profiles.ts +40 -0
- package/lib/cli/routes/status.ts +93 -0
- package/lib/cli/routes/update.ts +27 -0
- package/lib/connectors/connector-config-schema.ts +16 -0
- package/lib/connectors/connector-factory.ts +94 -0
- package/lib/connectors/connector-listener.ts +20 -0
- package/lib/{modules/connectors/funnel-discord-adapter.ts → connectors/discord-adapter.ts} +6 -11
- package/lib/{modules/connectors → connectors}/discord-connector-schema.ts +4 -1
- package/lib/connectors/discord-listener.ts +111 -0
- package/lib/{modules/connectors/funnel-gh-adapter.ts → connectors/gh-adapter.ts} +3 -6
- package/lib/{modules/connectors → connectors}/gh-connector-schema.ts +4 -1
- package/lib/{modules/connectors/funnel-gh-listener.ts → connectors/gh-listener.ts} +45 -19
- package/lib/{modules/connectors → connectors}/match-cron.ts +10 -4
- package/lib/connectors/schedule-connector-schema.ts +33 -0
- package/lib/connectors/schedule-listener.ts +207 -0
- package/lib/connectors/schedule-state-store.ts +54 -0
- package/lib/connectors/slack-adapter.ts +36 -0
- package/lib/{modules/connectors → connectors}/slack-connector-schema.ts +4 -1
- package/lib/{modules/connectors/funnel-slack-event-processor.ts → connectors/slack-event-processor.ts} +15 -9
- package/lib/{modules/connectors/funnel-slack-listener.ts → connectors/slack-listener.ts} +33 -14
- package/lib/engine/channels/channels.ts +520 -0
- package/lib/{modules/claude/funnel-claude.ts → engine/claude/claude.ts} +28 -55
- package/lib/engine/claude/gateway-controller.ts +4 -0
- package/lib/{modules/fs/funnel-file-system.ts → engine/fs/file-system.ts} +4 -0
- package/lib/{modules/fs/memory-funnel-file-system.ts → engine/fs/memory-file-system.ts} +20 -3
- package/lib/{modules/fs/node-funnel-file-system.ts → engine/fs/node-file-system.ts} +14 -2
- package/lib/{modules/http/memory-funnel-http-client.ts → engine/http/memory-http-client.ts} +1 -5
- package/lib/{modules/http/node-funnel-http-client.ts → engine/http/node-http-client.ts} +1 -5
- package/lib/{modules/id/memory-funnel-id-generator.ts → engine/id/memory-id-generator.ts} +1 -1
- package/lib/{modules/id/node-funnel-id-generator.ts → engine/id/node-id-generator.ts} +1 -1
- package/lib/{modules/logger/memory-funnel-logger.ts → engine/logger/memory-logger.ts} +1 -1
- package/lib/{modules/logger/node-funnel-logger.ts → engine/logger/node-logger.ts} +1 -1
- package/lib/{modules/logger/noop-funnel-logger.ts → engine/logger/noop-logger.ts} +1 -1
- package/lib/engine/mcp/channel-server.ts +204 -0
- package/lib/{modules/mcp/funnel-mcp.ts → engine/mcp/mcp.ts} +24 -10
- package/lib/{modules/process/memory-funnel-process-runner.ts → engine/process/memory-process-runner.ts} +1 -1
- package/lib/{modules/process/node-funnel-process-runner.ts → engine/process/node-process-runner.ts} +12 -21
- package/lib/engine/profiles/profile-channel-checker.ts +7 -0
- package/lib/{modules/profiles/funnel-profiles.ts → engine/profiles/profiles.ts} +41 -43
- package/lib/{modules/settings/mock-funnel-settings-reader.ts → engine/settings/mock-settings-reader.ts} +4 -3
- package/lib/{modules/settings/funnel-settings-reader.ts → engine/settings/settings-reader.ts} +1 -1
- package/lib/engine/settings/settings-schema.ts +46 -0
- package/lib/engine/settings/settings-store.ts +110 -0
- package/lib/{modules/time/memory-funnel-clock.ts → engine/time/memory-clock.ts} +1 -1
- package/lib/{modules/time/node-funnel-clock.ts → engine/time/node-clock.ts} +1 -1
- package/lib/funnel.ts +83 -78
- package/lib/gateway/auth-middleware.ts +44 -0
- package/lib/gateway/broadcaster.ts +319 -0
- package/lib/gateway/daemon.ts +47 -0
- package/lib/gateway/factory.ts +10 -0
- package/lib/gateway/funnel-event-store.ts +155 -0
- package/lib/gateway/gateway-server.ts +414 -0
- package/lib/gateway/gateway-token.ts +79 -0
- package/lib/{modules/gateway/funnel-gateway.ts → gateway/gateway.ts} +27 -13
- package/lib/{modules/gateway → gateway}/kill-competing-slack-gateways.ts +4 -4
- package/lib/gateway/listener-supervisor.ts +339 -0
- package/lib/gateway/listeners-client.ts +128 -0
- package/lib/gateway/resolve-daemon-script.ts +26 -0
- package/lib/gateway/routes/channels.connectors.call.ts +39 -0
- package/lib/gateway/routes/health.ts +13 -0
- package/lib/gateway/routes/index.ts +24 -0
- package/lib/gateway/routes/listeners.list.ts +6 -0
- package/lib/gateway/routes/listeners.restart.ts +15 -0
- package/lib/gateway/routes/listeners.start.ts +15 -0
- package/lib/gateway/routes/listeners.stop.ts +15 -0
- package/lib/gateway/routes/route-deps.ts +11 -0
- package/lib/gateway/routes/status.ts +15 -0
- package/lib/gateway/routes/validator.ts +17 -0
- package/lib/index.ts +50 -92
- package/lib/logger/leuco-human-file-writer.ts +65 -0
- package/lib/logger/leuco-human-logger.ts +98 -0
- package/lib/logger/leuco-human-record.ts +16 -0
- package/lib/logger/leuco-human-stdout-writer.ts +26 -0
- package/lib/logger/leuco-human-writer.ts +14 -0
- package/lib/logger/leuco-logger-memory-sink.ts +67 -0
- package/lib/logger/leuco-logger-record.ts +13 -0
- package/lib/logger/leuco-logger-sink.ts +33 -0
- package/lib/logger/leuco-logger-sqlite-sink.ts +355 -0
- package/lib/logger/leuco-logger.ts +135 -0
- package/lib/tui/app.tsx +357 -0
- package/lib/tui/components/add-row.tsx +18 -0
- package/lib/tui/components/brand.tsx +27 -0
- package/lib/tui/components/card.tsx +44 -0
- package/lib/tui/components/detail-bar.tsx +46 -0
- package/lib/tui/components/editable-field.tsx +33 -0
- package/lib/tui/components/empty-state.tsx +11 -0
- package/lib/tui/components/gateway-status.tsx +66 -0
- package/lib/tui/components/keymap.tsx +29 -0
- package/lib/tui/components/menu-item.tsx +73 -0
- package/lib/tui/components/menu.tsx +26 -0
- package/lib/tui/components/panel-header.tsx +22 -0
- package/lib/tui/components/readonly-field.tsx +18 -0
- package/lib/tui/components/section-header.tsx +25 -0
- package/lib/tui/components/selection-accent.tsx +32 -0
- package/lib/tui/components/session-item.tsx +33 -0
- package/lib/tui/components/session-list.tsx +33 -0
- package/lib/tui/components/ui/hascii/accordion-item.tsx +88 -0
- package/lib/tui/components/ui/hascii/accordion.tsx +96 -0
- package/lib/tui/components/ui/hascii/alert-dialog.tsx +43 -0
- package/lib/tui/components/ui/hascii/badge.tsx +51 -0
- package/lib/tui/components/ui/hascii/breadcrumb.tsx +58 -0
- package/lib/tui/components/ui/hascii/button.tsx +194 -0
- package/lib/tui/components/ui/hascii/card-content.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-description.tsx +13 -0
- package/lib/tui/components/ui/hascii/card-footer.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/card-title.tsx +13 -0
- package/lib/tui/components/ui/hascii/card.tsx +27 -0
- package/lib/tui/components/ui/hascii/checkbox.tsx +65 -0
- package/lib/tui/components/ui/hascii/command.tsx +159 -0
- package/lib/tui/components/ui/hascii/dialog-content.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-description.tsx +13 -0
- package/lib/tui/components/ui/hascii/dialog-footer.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/dialog-title.tsx +13 -0
- package/lib/tui/components/ui/hascii/dialog.tsx +27 -0
- package/lib/tui/components/ui/hascii/file-tree.tsx +142 -0
- package/lib/tui/components/ui/hascii/focus-group.tsx +62 -0
- package/lib/tui/components/ui/hascii/form-item.tsx +43 -0
- package/lib/tui/components/ui/hascii/input-otp.tsx +86 -0
- package/lib/tui/components/ui/hascii/input.tsx +130 -0
- package/lib/tui/components/ui/hascii/pagination.tsx +105 -0
- package/lib/tui/components/ui/hascii/progress.tsx +28 -0
- package/lib/tui/components/ui/hascii/select.tsx +131 -0
- package/lib/tui/components/ui/hascii/separator.tsx +35 -0
- package/lib/tui/components/ui/hascii/sidebar-content.tsx +23 -0
- package/lib/tui/components/ui/hascii/sidebar-header.tsx +14 -0
- package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +67 -0
- package/lib/tui/components/ui/hascii/sidebar.tsx +24 -0
- package/lib/tui/components/ui/hascii/skeleton.tsx +60 -0
- package/lib/tui/components/ui/hascii/slider.tsx +91 -0
- package/lib/tui/components/ui/hascii/snackbar.tsx +75 -0
- package/lib/tui/components/ui/hascii/sparkline.tsx +53 -0
- package/lib/tui/components/ui/hascii/spinner.tsx +47 -0
- package/lib/tui/components/ui/hascii/stepper.tsx +54 -0
- package/lib/tui/components/ui/hascii/switch.tsx +66 -0
- package/lib/tui/components/ui/hascii/table.tsx +95 -0
- package/lib/tui/components/ui/hascii/tabs.tsx +59 -0
- package/lib/tui/components/ui/hascii/toggle-group-item.tsx +45 -0
- package/lib/tui/components/ui/hascii/toggle-group.tsx +99 -0
- package/lib/tui/components/ui/hascii/tree.tsx +104 -0
- package/lib/tui/components/view-shell.tsx +44 -0
- package/lib/tui/filter-input.tsx +33 -0
- package/lib/tui/hooks/hascii/use-pressable.ts +54 -0
- package/lib/tui/parse-comma-list.ts +14 -0
- package/lib/tui/profile-launcher.tsx +61 -0
- package/lib/tui/scrollbar-options.ts +19 -0
- package/lib/tui/sidebar.tsx +50 -0
- package/lib/tui/theme.ts +40 -0
- package/lib/tui/tui.tsx +20 -0
- package/lib/tui/types.ts +38 -0
- package/lib/tui/unique-name.ts +18 -0
- package/lib/tui/use-event-stream.ts +133 -0
- package/lib/tui/use-snapshot.ts +99 -0
- package/lib/tui/utils/hascii/form-item-context.tsx +23 -0
- package/lib/tui/utils/hascii/input-focus-context.tsx +31 -0
- package/lib/tui/utils/hascii/theme-context.tsx +26 -0
- package/lib/tui/utils/hascii/theme.ts +176 -0
- package/lib/tui/views/channels-view.tsx +108 -0
- package/lib/tui/views/connectors-view.tsx +164 -0
- package/lib/tui/views/events-view.tsx +160 -0
- package/lib/tui/views/listeners-view.tsx +80 -0
- package/lib/tui/views/profiles-view.tsx +152 -0
- package/package.json +50 -44
- package/lib/api.ts +0 -54
- package/lib/modules/channels/channel-connector-ref-updater.ts +0 -4
- package/lib/modules/channels/funnel-channels.ts +0 -160
- package/lib/modules/connectors/connector-config-schema.ts +0 -16
- package/lib/modules/connectors/connector-existence-checker.ts +0 -3
- package/lib/modules/connectors/funnel-callable-connector-store.ts +0 -9
- package/lib/modules/connectors/funnel-connector-listener.ts +0 -5
- package/lib/modules/connectors/funnel-connector-stores.ts +0 -52
- package/lib/modules/connectors/funnel-connector-type-store.ts +0 -24
- package/lib/modules/connectors/funnel-connectors.ts +0 -151
- package/lib/modules/connectors/funnel-discord-listener.ts +0 -71
- package/lib/modules/connectors/funnel-discord-store.ts +0 -88
- package/lib/modules/connectors/funnel-gh-store.ts +0 -101
- package/lib/modules/connectors/funnel-json-connector-store.ts +0 -100
- package/lib/modules/connectors/funnel-schedule-listener.ts +0 -130
- package/lib/modules/connectors/funnel-schedule-store.ts +0 -195
- package/lib/modules/connectors/funnel-slack-adapter.ts +0 -31
- package/lib/modules/connectors/funnel-slack-store.ts +0 -90
- package/lib/modules/connectors/migrate-legacy-connectors.ts +0 -81
- package/lib/modules/connectors/schedule-connector-schema.ts +0 -18
- package/lib/modules/connectors/schedule-last-fired-store.ts +0 -48
- package/lib/modules/gateway/daemon.ts +0 -74
- package/lib/modules/gateway/funnel-broadcaster.ts +0 -37
- package/lib/modules/gateway/funnel-event-logger.ts +0 -59
- package/lib/modules/gateway/funnel-gateway-server.ts +0 -241
- package/lib/modules/mcp/channel-server.ts +0 -76
- package/lib/modules/profiles/profile-channel-checker.ts +0 -3
- package/lib/modules/profiles/profile-channel-ref-updater.ts +0 -3
- package/lib/modules/repos/funnel-repositories.ts +0 -112
- package/lib/modules/schedule/funnel-schedule.ts +0 -39
- package/lib/modules/settings/funnel-settings-store.ts +0 -56
- package/lib/modules/settings/settings-schema.ts +0 -33
- package/lib/modules/tui/app.tsx +0 -44
- package/lib/modules/tui/tui.tsx +0 -13
- package/lib/routes/channels/add.help.ts +0 -3
- package/lib/routes/channels/add.ts +0 -21
- package/lib/routes/channels/connectors-attach.help.ts +0 -3
- package/lib/routes/channels/connectors-attach.ts +0 -17
- package/lib/routes/channels/connectors-detach.help.ts +0 -3
- package/lib/routes/channels/connectors-detach.ts +0 -17
- package/lib/routes/channels/group.help.ts +0 -16
- package/lib/routes/channels/group.ts +0 -22
- package/lib/routes/channels/remove.help.ts +0 -3
- package/lib/routes/channels/remove.ts +0 -17
- package/lib/routes/channels/rename.help.ts +0 -5
- package/lib/routes/channels/rename.ts +0 -17
- package/lib/routes/channels/routes.ts +0 -19
- package/lib/routes/channels/show.help.ts +0 -1
- package/lib/routes/channels/show.ts +0 -26
- package/lib/routes/claude/claude.help.ts +0 -16
- package/lib/routes/claude/claude.ts +0 -76
- package/lib/routes/claude/routes.ts +0 -4
- package/lib/routes/connectors/add.help.ts +0 -28
- package/lib/routes/connectors/add.ts +0 -64
- package/lib/routes/connectors/group.help.ts +0 -14
- package/lib/routes/connectors/group.ts +0 -18
- package/lib/routes/connectors/remove.help.ts +0 -3
- package/lib/routes/connectors/remove.ts +0 -17
- package/lib/routes/connectors/rename.help.ts +0 -5
- package/lib/routes/connectors/rename.ts +0 -17
- package/lib/routes/connectors/routes.ts +0 -23
- package/lib/routes/connectors/schedules-add.help.ts +0 -11
- package/lib/routes/connectors/schedules-add.ts +0 -33
- package/lib/routes/connectors/schedules-group.help.ts +0 -1
- package/lib/routes/connectors/schedules-group.ts +0 -38
- package/lib/routes/connectors/schedules-remove.help.ts +0 -3
- package/lib/routes/connectors/schedules-remove.ts +0 -17
- package/lib/routes/connectors/set.help.ts +0 -8
- package/lib/routes/connectors/set.ts +0 -72
- package/lib/routes/connectors/show.help.ts +0 -1
- package/lib/routes/connectors/show.ts +0 -41
- package/lib/routes/gateway/group.help.ts +0 -15
- package/lib/routes/gateway/group.ts +0 -28
- package/lib/routes/gateway/logs.help.ts +0 -13
- package/lib/routes/gateway/logs.ts +0 -102
- package/lib/routes/gateway/restart.help.ts +0 -10
- package/lib/routes/gateway/routes.ts +0 -18
- package/lib/routes/gateway/run.help.ts +0 -12
- package/lib/routes/gateway/run.ts +0 -35
- package/lib/routes/gateway/start.help.ts +0 -15
- package/lib/routes/gateway/start.ts +0 -32
- package/lib/routes/gateway/status.help.ts +0 -9
- package/lib/routes/gateway/status.ts +0 -28
- package/lib/routes/gateway/stop.help.ts +0 -8
- package/lib/routes/gateway/stop.ts +0 -21
- package/lib/routes/profiles/add.help.ts +0 -3
- package/lib/routes/profiles/add.ts +0 -33
- package/lib/routes/profiles/group.help.ts +0 -16
- package/lib/routes/profiles/group.ts +0 -25
- package/lib/routes/profiles/launch.help.ts +0 -4
- package/lib/routes/profiles/launch.ts +0 -36
- package/lib/routes/profiles/remove.help.ts +0 -3
- package/lib/routes/profiles/remove.ts +0 -17
- package/lib/routes/profiles/rename.help.ts +0 -5
- package/lib/routes/profiles/rename.ts +0 -17
- package/lib/routes/profiles/routes.ts +0 -18
- package/lib/routes/profiles/set.help.ts +0 -5
- package/lib/routes/profiles/set.ts +0 -32
- package/lib/routes/repos/add.help.ts +0 -6
- package/lib/routes/repos/add.ts +0 -20
- package/lib/routes/repos/group.help.ts +0 -11
- package/lib/routes/repos/group.ts +0 -18
- package/lib/routes/repos/remove.help.ts +0 -3
- package/lib/routes/repos/remove.ts +0 -17
- package/lib/routes/repos/rename.help.ts +0 -5
- package/lib/routes/repos/rename.ts +0 -17
- package/lib/routes/repos/routes.ts +0 -17
- package/lib/routes/repos/set.help.ts +0 -5
- package/lib/routes/repos/set.ts +0 -21
- package/lib/routes/repos/show.help.ts +0 -1
- package/lib/routes/repos/show.ts +0 -19
- package/lib/routes/request/discord-help.ts +0 -9
- package/lib/routes/request/discord.help.ts +0 -19
- package/lib/routes/request/discord.ts +0 -65
- package/lib/routes/request/group.help.ts +0 -15
- package/lib/routes/request/group.ts +0 -9
- package/lib/routes/request/routes.ts +0 -14
- package/lib/routes/request/slack-help.ts +0 -9
- package/lib/routes/request/slack.help.ts +0 -19
- package/lib/routes/request/slack.ts +0 -61
- package/lib/routes/status/routes.ts +0 -4
- package/lib/routes/status/status.help.ts +0 -6
- package/lib/routes/status/status.ts +0 -77
- package/lib/routes/update/routes.ts +0 -4
- package/lib/routes/update/update.help.ts +0 -5
- package/lib/routes/update/update.ts +0 -21
- package/lib/routes.ts +0 -40
- /package/lib/{factory.ts → cli/factory.ts} +0 -0
- /package/lib/{modules → cli}/router/query-to-cli-args.ts +0 -0
- /package/lib/{modules → cli}/router/validator.ts +0 -0
- /package/lib/{modules/connectors/funnel-connector-adapter.ts → connectors/connector-adapter.ts} +0 -0
- /package/lib/{modules/connectors/funnel-discord-event-processor.ts → connectors/discord-event-processor.ts} +0 -0
- /package/lib/{modules/http/funnel-http-client.ts → engine/http/http-client.ts} +0 -0
- /package/lib/{modules/id/funnel-id-generator.ts → engine/id/id-generator.ts} +0 -0
- /package/lib/{modules/logger/funnel-logger.ts → engine/logger/logger.ts} +0 -0
- /package/lib/{modules/process/funnel-process-runner.ts → engine/process/process-runner.ts} +0 -0
- /package/lib/{modules/time/funnel-clock.ts → engine/time/clock.ts} +0 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { SelectOption } from "@opentui/core"
|
|
2
|
+
import { useKeyboard } from "@opentui/react"
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import type { HasciiTheme } from "@/tui/utils/hascii/theme"
|
|
5
|
+
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
6
|
+
import { usePressable } from "@/tui/hooks/hascii/use-pressable"
|
|
7
|
+
|
|
8
|
+
export type Props = {
|
|
9
|
+
options?: SelectOption[]
|
|
10
|
+
width?: number
|
|
11
|
+
height?: number
|
|
12
|
+
defaultIndex?: number
|
|
13
|
+
focusedIndex?: number
|
|
14
|
+
isFocused?: boolean
|
|
15
|
+
onChange?: (index: number, option: SelectOption | null) => void
|
|
16
|
+
onSelect?: (index: number, option: SelectOption | null) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const pickItemBg = (
|
|
20
|
+
isActive: boolean,
|
|
21
|
+
hovered: boolean,
|
|
22
|
+
pressed: boolean,
|
|
23
|
+
theme: HasciiTheme,
|
|
24
|
+
): string | undefined => {
|
|
25
|
+
if (pressed) return theme.color.secondaryActive
|
|
26
|
+
if (hovered && isActive) return theme.color.hoverActive
|
|
27
|
+
if (hovered) return theme.color.secondaryHover
|
|
28
|
+
if (isActive) return theme.color.secondaryActive
|
|
29
|
+
return undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type ItemProps = {
|
|
33
|
+
option: SelectOption
|
|
34
|
+
isActive: boolean
|
|
35
|
+
onPress: () => void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Internal row used by HasciiSelect. Tracks hover/press state and renders the active left bar. */
|
|
39
|
+
function HasciiSelectItem(props: ItemProps) {
|
|
40
|
+
const theme = useHasciiTheme()
|
|
41
|
+
const press = usePressable({ onPress: props.onPress })
|
|
42
|
+
|
|
43
|
+
const bg = pickItemBg(props.isActive, press.isHovered, press.isPressed, theme)
|
|
44
|
+
|
|
45
|
+
const nameColor =
|
|
46
|
+
props.isActive || press.isHovered ? theme.color.foreground : theme.color.mutedForeground
|
|
47
|
+
const descColor = theme.color.mutedForeground
|
|
48
|
+
|
|
49
|
+
const rowHeight = props.option.description ? 4 : 3
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<box flexDirection="row" backgroundColor={bg} {...press.bind}>
|
|
53
|
+
{props.isActive ? (
|
|
54
|
+
<box position="absolute" left={0} top={0} bottom={0} flexDirection="column">
|
|
55
|
+
{Array.from({ length: rowHeight }, (_, index) => (
|
|
56
|
+
<text key={index} fg={theme.color.primary}>
|
|
57
|
+
▏
|
|
58
|
+
</text>
|
|
59
|
+
))}
|
|
60
|
+
</box>
|
|
61
|
+
) : null}
|
|
62
|
+
<box
|
|
63
|
+
flexGrow={1}
|
|
64
|
+
flexDirection="column"
|
|
65
|
+
paddingTop={1}
|
|
66
|
+
paddingBottom={1}
|
|
67
|
+
paddingLeft={2}
|
|
68
|
+
paddingRight={2}
|
|
69
|
+
gap={0}
|
|
70
|
+
>
|
|
71
|
+
<text fg={nameColor}>{props.option.name}</text>
|
|
72
|
+
{props.option.description ? <text fg={descColor}>{props.option.description}</text> : null}
|
|
73
|
+
</box>
|
|
74
|
+
</box>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Vertical option list with a muted background, a flush left bar on the active row, and vertical scrolling when items overflow. */
|
|
79
|
+
export function HasciiSelect(props: Props) {
|
|
80
|
+
const width = props.width ?? 36
|
|
81
|
+
const height = props.height ?? 16
|
|
82
|
+
const isFocused = props.isFocused ?? true
|
|
83
|
+
const options = props.options ?? []
|
|
84
|
+
|
|
85
|
+
const theme = useHasciiTheme()
|
|
86
|
+
|
|
87
|
+
const internalState = useState(props.defaultIndex ?? 0)
|
|
88
|
+
const internal = internalState[0]
|
|
89
|
+
const setInternal = internalState[1]
|
|
90
|
+
|
|
91
|
+
const current = props.focusedIndex ?? internal
|
|
92
|
+
|
|
93
|
+
const moveTo = (next: number) => {
|
|
94
|
+
if (options.length === 0) return
|
|
95
|
+
|
|
96
|
+
const clamped = Math.max(0, Math.min(options.length - 1, next))
|
|
97
|
+
if (props.focusedIndex === undefined) setInternal(clamped)
|
|
98
|
+
props.onChange?.(clamped, options[clamped] ?? null)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
useKeyboard((key) => {
|
|
102
|
+
if (!isFocused || options.length === 0) return
|
|
103
|
+
|
|
104
|
+
if (key.name === "up") moveTo(current - 1)
|
|
105
|
+
if (key.name === "down") moveTo(current + 1)
|
|
106
|
+
if (key.name === "return" || key.name === "space") {
|
|
107
|
+
props.onSelect?.(current, options[current] ?? null)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<box flexDirection="column" width={width} height={height} backgroundColor={theme.color.muted}>
|
|
113
|
+
<scrollbox
|
|
114
|
+
flexGrow={1}
|
|
115
|
+
focused={isFocused}
|
|
116
|
+
scrollY
|
|
117
|
+
stickyScroll={false}
|
|
118
|
+
contentOptions={{ flexDirection: "column", gap: 0 }}
|
|
119
|
+
>
|
|
120
|
+
{options.map((option, index) => (
|
|
121
|
+
<HasciiSelectItem
|
|
122
|
+
key={`${option.value}-${index}`}
|
|
123
|
+
option={option}
|
|
124
|
+
isActive={index === current}
|
|
125
|
+
onPress={() => moveTo(index)}
|
|
126
|
+
/>
|
|
127
|
+
))}
|
|
128
|
+
</scrollbox>
|
|
129
|
+
</box>
|
|
130
|
+
)
|
|
131
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
2
|
+
|
|
3
|
+
type Orientation = "horizontal" | "vertical"
|
|
4
|
+
|
|
5
|
+
export type Props = {
|
|
6
|
+
orientation?: Orientation
|
|
7
|
+
color?: string
|
|
8
|
+
length?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Hairline divider drawn with box-drawing characters. Length is the run in the chosen orientation. */
|
|
12
|
+
export function HasciiSeparator(props: Props) {
|
|
13
|
+
const orientation = props.orientation ?? "horizontal"
|
|
14
|
+
const length = props.length ?? (orientation === "horizontal" ? 32 : 8)
|
|
15
|
+
|
|
16
|
+
const theme = useHasciiTheme()
|
|
17
|
+
const color = props.color ?? theme.color.border
|
|
18
|
+
|
|
19
|
+
if (orientation === "vertical") {
|
|
20
|
+
const lines: string[] = []
|
|
21
|
+
for (let index = 0; index < length; index++) lines.push("│")
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<box width={1} height={length} flexShrink={0}>
|
|
25
|
+
<text fg={color}>{lines.join("\n")}</text>
|
|
26
|
+
</box>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<box width={length} height={1} flexShrink={0}>
|
|
32
|
+
<text fg={color}>{"─".repeat(length)}</text>
|
|
33
|
+
</box>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
export type Props = {
|
|
4
|
+
isFocused?: boolean
|
|
5
|
+
children?: ReactNode
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** Scrollable middle region of a HasciiSidebar. Wheel + arrow keys scroll vertically. */
|
|
9
|
+
export function HasciiSidebarContent(props: Props) {
|
|
10
|
+
const isFocused = props.isFocused ?? true
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<scrollbox
|
|
14
|
+
flexGrow={1}
|
|
15
|
+
focused={isFocused}
|
|
16
|
+
scrollY
|
|
17
|
+
stickyScroll={false}
|
|
18
|
+
contentOptions={{ flexDirection: "column", gap: 0 }}
|
|
19
|
+
>
|
|
20
|
+
{props.children}
|
|
21
|
+
</scrollbox>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
export type Props = {
|
|
4
|
+
children?: ReactNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** Top section of a HasciiSidebar. Renders children at the sidebar's text padding. */
|
|
8
|
+
export function HasciiSidebarHeader(props: Props) {
|
|
9
|
+
return (
|
|
10
|
+
<box flexDirection="column" paddingLeft={2} paddingRight={2} paddingBottom={1} gap={0}>
|
|
11
|
+
{props.children}
|
|
12
|
+
</box>
|
|
13
|
+
)
|
|
14
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import type { HasciiTheme } from "@/tui/utils/hascii/theme"
|
|
3
|
+
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
4
|
+
import { usePressable } from "@/tui/hooks/hascii/use-pressable"
|
|
5
|
+
|
|
6
|
+
export type Props = {
|
|
7
|
+
isActive?: boolean
|
|
8
|
+
isDisabled?: boolean
|
|
9
|
+
onPress?: () => void
|
|
10
|
+
children?: ReactNode
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const pickBg = (
|
|
14
|
+
isDisabled: boolean,
|
|
15
|
+
isActive: boolean,
|
|
16
|
+
isHovered: boolean,
|
|
17
|
+
isPressed: boolean,
|
|
18
|
+
theme: HasciiTheme,
|
|
19
|
+
): string | undefined => {
|
|
20
|
+
if (isDisabled) return undefined
|
|
21
|
+
if (isPressed) return theme.color.secondaryActive
|
|
22
|
+
if (isHovered && isActive) return theme.color.hoverActive
|
|
23
|
+
if (isHovered) return theme.color.secondaryHover
|
|
24
|
+
if (isActive) return theme.color.secondaryActive
|
|
25
|
+
return undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ROW_HEIGHT = 3
|
|
29
|
+
|
|
30
|
+
/** Single pressable row inside HasciiSidebarContent. Active items show a thin left rule using ▏ glyphs. */
|
|
31
|
+
export function HasciiSidebarMenuItem(props: Props) {
|
|
32
|
+
const isActive = props.isActive ?? false
|
|
33
|
+
const isDisabled = props.isDisabled ?? false
|
|
34
|
+
const theme = useHasciiTheme()
|
|
35
|
+
|
|
36
|
+
const press = usePressable({ isDisabled, onPress: props.onPress })
|
|
37
|
+
|
|
38
|
+
const bg = pickBg(isDisabled, isActive, press.isHovered, press.isPressed, theme)
|
|
39
|
+
|
|
40
|
+
const fg = isDisabled
|
|
41
|
+
? theme.color.mutedForeground
|
|
42
|
+
: isActive || press.isHovered
|
|
43
|
+
? theme.color.foreground
|
|
44
|
+
: theme.color.mutedForeground
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<box
|
|
48
|
+
paddingTop={1}
|
|
49
|
+
paddingBottom={1}
|
|
50
|
+
paddingLeft={2}
|
|
51
|
+
paddingRight={2}
|
|
52
|
+
backgroundColor={bg}
|
|
53
|
+
{...press.bind}
|
|
54
|
+
>
|
|
55
|
+
{isActive ? (
|
|
56
|
+
<box position="absolute" left={0} top={0} bottom={0} flexDirection="column">
|
|
57
|
+
{Array.from({ length: ROW_HEIGHT }, (_, index) => (
|
|
58
|
+
<text key={index} fg={theme.color.primary}>
|
|
59
|
+
▏
|
|
60
|
+
</text>
|
|
61
|
+
))}
|
|
62
|
+
</box>
|
|
63
|
+
) : null}
|
|
64
|
+
<text fg={fg}>{props.children}</text>
|
|
65
|
+
</box>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from "react"
|
|
2
|
+
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
3
|
+
|
|
4
|
+
export type Props = {
|
|
5
|
+
width?: number
|
|
6
|
+
children?: ReactNode
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Fixed-width vertical sidebar. Compose with HasciiSidebarHeader, HasciiSidebarContent, HasciiSidebarMenuItem. */
|
|
10
|
+
export function HasciiSidebar(props: Props) {
|
|
11
|
+
const theme = useHasciiTheme()
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<box
|
|
15
|
+
flexDirection="column"
|
|
16
|
+
width={props.width ?? 24}
|
|
17
|
+
backgroundColor={theme.color.muted}
|
|
18
|
+
paddingTop={1}
|
|
19
|
+
gap={0}
|
|
20
|
+
>
|
|
21
|
+
{props.children}
|
|
22
|
+
</box>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
3
|
+
|
|
4
|
+
export type Props = {
|
|
5
|
+
width?: number
|
|
6
|
+
height?: number
|
|
7
|
+
intervalMs?: number
|
|
8
|
+
cycleMs?: number
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const lerpChannel = (a: number, b: number, t: number): number => Math.round(a + (b - a) * t)
|
|
12
|
+
|
|
13
|
+
const parseHex = (hex: string): [number, number, number] => {
|
|
14
|
+
const clean = hex.startsWith("#") ? hex.slice(1) : hex
|
|
15
|
+
const r = parseInt(clean.slice(0, 2), 16)
|
|
16
|
+
const g = parseInt(clean.slice(2, 4), 16)
|
|
17
|
+
const b = parseInt(clean.slice(4, 6), 16)
|
|
18
|
+
return [r, g, b]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const toHex = (channel: number): string => channel.toString(16).padStart(2, "0")
|
|
22
|
+
|
|
23
|
+
const lerpHex = (a: string, b: string, t: number): string => {
|
|
24
|
+
const colorA = parseHex(a)
|
|
25
|
+
const colorB = parseHex(b)
|
|
26
|
+
const r = lerpChannel(colorA[0], colorB[0], t)
|
|
27
|
+
const g = lerpChannel(colorA[1], colorB[1], t)
|
|
28
|
+
const blue = lerpChannel(colorA[2], colorB[2], t)
|
|
29
|
+
return `#${toHex(r)}${toHex(g)}${toHex(blue)}`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Placeholder block that pulses smoothly between two muted shades using cosine easing. */
|
|
33
|
+
export function HasciiSkeleton(props: Props) {
|
|
34
|
+
const intervalMs = props.intervalMs ?? 60
|
|
35
|
+
const cycleMs = props.cycleMs ?? 1800
|
|
36
|
+
const theme = useHasciiTheme()
|
|
37
|
+
|
|
38
|
+
const startState = useState<number>(performance.now())
|
|
39
|
+
const start = startState[0]
|
|
40
|
+
|
|
41
|
+
const elapsedState = useState(0)
|
|
42
|
+
const elapsed = elapsedState[0]
|
|
43
|
+
const setElapsed = elapsedState[1]
|
|
44
|
+
|
|
45
|
+
// useEffect drives the pulse — necessary for time-based color interpolation.
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const id = setInterval(() => {
|
|
48
|
+
setElapsed(performance.now() - start)
|
|
49
|
+
}, intervalMs)
|
|
50
|
+
|
|
51
|
+
return () => clearInterval(id)
|
|
52
|
+
}, [intervalMs, start, setElapsed])
|
|
53
|
+
|
|
54
|
+
const phase = ((elapsed % cycleMs) / cycleMs) * 2 * Math.PI
|
|
55
|
+
const t = (1 - Math.cos(phase)) / 2
|
|
56
|
+
|
|
57
|
+
const bg = lerpHex(theme.color.muted, theme.color.secondaryActive, t)
|
|
58
|
+
|
|
59
|
+
return <box width={props.width} height={props.height ?? 1} backgroundColor={bg} flexShrink={0} />
|
|
60
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { RGBA, SliderRenderable } from "@opentui/core"
|
|
2
|
+
import { extend } from "@opentui/react"
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
5
|
+
|
|
6
|
+
declare module "@opentui/react" {
|
|
7
|
+
interface OpenTUIComponents {
|
|
8
|
+
slider: typeof SliderRenderable
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
extend({ slider: SliderRenderable })
|
|
13
|
+
|
|
14
|
+
export type Props = {
|
|
15
|
+
value?: number
|
|
16
|
+
defaultValue?: number
|
|
17
|
+
min?: number
|
|
18
|
+
max?: number
|
|
19
|
+
width?: number
|
|
20
|
+
thumbSize?: number
|
|
21
|
+
onChange?: (next: number) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const TRANSPARENT = RGBA.fromValues(0, 0, 0, 0)
|
|
25
|
+
|
|
26
|
+
const viewPortSizeFor = (thumbCells: number, range: number, width: number): number => {
|
|
27
|
+
const virtualThumb = thumbCells * 2
|
|
28
|
+
const denominator = width * 2 - virtualThumb
|
|
29
|
+
|
|
30
|
+
if (denominator <= 0) return range
|
|
31
|
+
|
|
32
|
+
return Math.max(1, Math.round((virtualThumb * range) / denominator))
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Horizontal slider backed by OpenTUI's SliderRenderable. A ─ track sits behind a thumbSize-cell thumb that supports native click + drag. */
|
|
36
|
+
export function HasciiSlider(props: Props) {
|
|
37
|
+
const min = props.min ?? 0
|
|
38
|
+
const max = props.max ?? 100
|
|
39
|
+
const width = props.width ?? 32
|
|
40
|
+
const thumbSize = props.thumbSize ?? 3
|
|
41
|
+
|
|
42
|
+
const theme = useHasciiTheme()
|
|
43
|
+
|
|
44
|
+
const internalState = useState(props.defaultValue ?? min)
|
|
45
|
+
const internal = internalState[0]
|
|
46
|
+
const setInternal = internalState[1]
|
|
47
|
+
|
|
48
|
+
const value = props.value ?? internal
|
|
49
|
+
|
|
50
|
+
const onChange = (next: number) => {
|
|
51
|
+
if (props.value === undefined) setInternal(next)
|
|
52
|
+
props.onChange?.(next)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const hoveredState = useState(false)
|
|
56
|
+
const isHovered = hoveredState[0]
|
|
57
|
+
const setHovered = hoveredState[1]
|
|
58
|
+
|
|
59
|
+
const range = Math.max(1, max - min)
|
|
60
|
+
const viewPortSize = viewPortSizeFor(thumbSize, range, width)
|
|
61
|
+
|
|
62
|
+
const thumbFg = isHovered ? theme.color.primaryHover : theme.color.primary
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<box
|
|
66
|
+
width={width}
|
|
67
|
+
height={1}
|
|
68
|
+
onMouseOver={() => setHovered(true)}
|
|
69
|
+
onMouseOut={() => setHovered(false)}
|
|
70
|
+
>
|
|
71
|
+
<box position="absolute" left={0} top={0}>
|
|
72
|
+
<text fg={theme.color.border}>{"─".repeat(width)}</text>
|
|
73
|
+
</box>
|
|
74
|
+
<slider
|
|
75
|
+
position="absolute"
|
|
76
|
+
left={0}
|
|
77
|
+
top={0}
|
|
78
|
+
orientation="horizontal"
|
|
79
|
+
width={width}
|
|
80
|
+
height={1}
|
|
81
|
+
min={min}
|
|
82
|
+
max={max}
|
|
83
|
+
value={value}
|
|
84
|
+
viewPortSize={viewPortSize}
|
|
85
|
+
foregroundColor={thumbFg}
|
|
86
|
+
backgroundColor={TRANSPARENT}
|
|
87
|
+
onChange={onChange}
|
|
88
|
+
/>
|
|
89
|
+
</box>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import type { ReactNode } from "react"
|
|
3
|
+
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
4
|
+
|
|
5
|
+
type Variant = "default" | "secondary" | "destructive"
|
|
6
|
+
|
|
7
|
+
export type Props = {
|
|
8
|
+
variant?: Variant
|
|
9
|
+
width?: number
|
|
10
|
+
slideMs?: number
|
|
11
|
+
isOpen?: boolean
|
|
12
|
+
children?: ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Toast-like overlay that slides in from the right edge. Render inside an end-aligned column to anchor bottom-right. */
|
|
16
|
+
export function HasciiSnackbar(props: Props) {
|
|
17
|
+
const variant = props.variant ?? "default"
|
|
18
|
+
const width = props.width ?? 28
|
|
19
|
+
const slideMs = props.slideMs ?? 90
|
|
20
|
+
const isOpen = props.isOpen ?? true
|
|
21
|
+
|
|
22
|
+
const theme = useHasciiTheme()
|
|
23
|
+
|
|
24
|
+
const offsetState = useState(width)
|
|
25
|
+
const offset = offsetState[0]
|
|
26
|
+
const setOffset = offsetState[1]
|
|
27
|
+
|
|
28
|
+
// useEffect drives the slide animation by stepping marginRight from `width` to 0 (or back).
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
const target = isOpen ? 0 : width
|
|
31
|
+
if (offset === target) return
|
|
32
|
+
|
|
33
|
+
const start = performance.now()
|
|
34
|
+
const from = offset
|
|
35
|
+
let frame = 0
|
|
36
|
+
|
|
37
|
+
const tick = () => {
|
|
38
|
+
const progress = Math.min(1, (performance.now() - start) / slideMs)
|
|
39
|
+
const next = Math.round(from + (target - from) * progress)
|
|
40
|
+
setOffset(next)
|
|
41
|
+
|
|
42
|
+
if (progress < 1) {
|
|
43
|
+
frame = setTimeout(tick, 16) as unknown as number
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
tick()
|
|
48
|
+
|
|
49
|
+
return () => clearTimeout(frame)
|
|
50
|
+
}, [isOpen, width, slideMs, offset, setOffset])
|
|
51
|
+
|
|
52
|
+
const palette = {
|
|
53
|
+
default: { bg: theme.color.primary, fg: theme.color.primaryForeground },
|
|
54
|
+
secondary: { bg: theme.color.secondary, fg: theme.color.secondaryForeground },
|
|
55
|
+
destructive: {
|
|
56
|
+
bg: theme.color.destructive,
|
|
57
|
+
fg: theme.color.destructiveForeground,
|
|
58
|
+
},
|
|
59
|
+
}[variant]
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<box
|
|
63
|
+
flexDirection="row"
|
|
64
|
+
width={width}
|
|
65
|
+
paddingTop={1}
|
|
66
|
+
paddingBottom={1}
|
|
67
|
+
paddingLeft={2}
|
|
68
|
+
paddingRight={2}
|
|
69
|
+
marginRight={-offset}
|
|
70
|
+
backgroundColor={palette.bg}
|
|
71
|
+
>
|
|
72
|
+
<text fg={palette.fg}>{props.children}</text>
|
|
73
|
+
</box>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
2
|
+
|
|
3
|
+
export type Props = {
|
|
4
|
+
values: number[]
|
|
5
|
+
width?: number
|
|
6
|
+
color?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const BARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"] as const
|
|
10
|
+
|
|
11
|
+
/** Single-row Unicode bar chart. Maps each value to one of eight block heights. */
|
|
12
|
+
export function HasciiSparkline(props: Props) {
|
|
13
|
+
const theme = useHasciiTheme()
|
|
14
|
+
const color = props.color ?? theme.color.primary
|
|
15
|
+
|
|
16
|
+
const samples = props.width !== undefined ? takeSamples(props.values, props.width) : props.values
|
|
17
|
+
|
|
18
|
+
if (samples.length === 0) {
|
|
19
|
+
return <text fg={color}> </text>
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let min = Infinity
|
|
23
|
+
let max = -Infinity
|
|
24
|
+
|
|
25
|
+
for (const value of samples) {
|
|
26
|
+
if (value < min) min = value
|
|
27
|
+
if (value > max) max = value
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const range = max - min || 1
|
|
31
|
+
|
|
32
|
+
const glyphs = samples.map((value) => {
|
|
33
|
+
const ratio = (value - min) / range
|
|
34
|
+
const index = Math.min(BARS.length - 1, Math.round(ratio * (BARS.length - 1)))
|
|
35
|
+
return BARS[index]
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return <text fg={color}>{glyphs.join("")}</text>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const takeSamples = (values: number[], targetWidth: number): number[] => {
|
|
42
|
+
if (values.length <= targetWidth) return values
|
|
43
|
+
|
|
44
|
+
const samples: number[] = []
|
|
45
|
+
const stride = values.length / targetWidth
|
|
46
|
+
|
|
47
|
+
for (let index = 0; index < targetWidth; index++) {
|
|
48
|
+
const sourceIndex = Math.min(values.length - 1, Math.floor(index * stride))
|
|
49
|
+
samples.push(values[sourceIndex] ?? 0)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return samples
|
|
53
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useState } from "react"
|
|
2
|
+
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
3
|
+
|
|
4
|
+
export const SPINNER_KINDS = {
|
|
5
|
+
braille: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
6
|
+
dots: ["⠁", "⠂", "⠄", "⡀", "⢀", "⠠", "⠐", "⠈"],
|
|
7
|
+
line: ["|", "/", "-", "\\"],
|
|
8
|
+
noise: ["▓", "▒", "░", "▒"],
|
|
9
|
+
pipe: ["┤", "┘", "┴", "└", "├", "┌", "┬", "┐"],
|
|
10
|
+
block: ["▌", "▀", "▐", "▄"],
|
|
11
|
+
growVert: ["▁", "▃", "▄", "▅", "▆", "▇", "█", "▇", "▆", "▅", "▄", "▃"],
|
|
12
|
+
toggle: ["▢", "▣", "▤", "▥", "▦", "▧", "▨", "▩"],
|
|
13
|
+
} as const satisfies Record<string, readonly string[]>
|
|
14
|
+
|
|
15
|
+
export type SpinnerKind = keyof typeof SPINNER_KINDS
|
|
16
|
+
|
|
17
|
+
export type Props = {
|
|
18
|
+
variant?: SpinnerKind
|
|
19
|
+
intervalMs?: number
|
|
20
|
+
color?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Animated single-cell spinner. variant chooses the glyph cycle, intervalMs the cadence. */
|
|
24
|
+
export function HasciiSpinner(props: Props) {
|
|
25
|
+
const variant: SpinnerKind = props.variant ?? "braille"
|
|
26
|
+
const intervalMs = props.intervalMs ?? 80
|
|
27
|
+
|
|
28
|
+
const theme = useHasciiTheme()
|
|
29
|
+
const color = props.color ?? theme.color.foreground
|
|
30
|
+
|
|
31
|
+
const frames = SPINNER_KINDS[variant]
|
|
32
|
+
|
|
33
|
+
const frameState = useState(0)
|
|
34
|
+
const frame = frameState[0]
|
|
35
|
+
const setFrame = frameState[1]
|
|
36
|
+
|
|
37
|
+
// useEffect is necessary for time-based frame advancement in a TUI loop.
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const id = setInterval(() => {
|
|
40
|
+
setFrame((current) => (current + 1) % frames.length)
|
|
41
|
+
}, intervalMs)
|
|
42
|
+
|
|
43
|
+
return () => clearInterval(id)
|
|
44
|
+
}, [frames, intervalMs, setFrame])
|
|
45
|
+
|
|
46
|
+
return <text fg={color}>{frames[frame]}</text>
|
|
47
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
2
|
+
|
|
3
|
+
export type StepperItem = {
|
|
4
|
+
label: string
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type Props = {
|
|
8
|
+
steps: StepperItem[]
|
|
9
|
+
current: number
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Horizontal multi-step indicator. Past steps show ■, current shows ▣, future shows □. */
|
|
13
|
+
export function HasciiStepper(props: Props) {
|
|
14
|
+
const theme = useHasciiTheme()
|
|
15
|
+
|
|
16
|
+
const items: import("react").ReactNode[] = []
|
|
17
|
+
|
|
18
|
+
for (let index = 0; index < props.steps.length; index++) {
|
|
19
|
+
const step = props.steps[index]
|
|
20
|
+
if (step === undefined) continue
|
|
21
|
+
|
|
22
|
+
const isPast = index < props.current
|
|
23
|
+
const isCurrent = index === props.current
|
|
24
|
+
|
|
25
|
+
const markerFg = isPast || isCurrent ? theme.color.primary : theme.color.mutedForeground
|
|
26
|
+
const labelFg = isCurrent ? theme.color.foreground : theme.color.mutedForeground
|
|
27
|
+
const marker = isPast ? "■" : isCurrent ? "▣" : "□"
|
|
28
|
+
|
|
29
|
+
items.push(
|
|
30
|
+
<box key={`step-${index}`} flexDirection="row" alignItems="center">
|
|
31
|
+
<text fg={markerFg}>{marker}</text>
|
|
32
|
+
<box paddingLeft={2}>
|
|
33
|
+
<text fg={labelFg}>{step.label}</text>
|
|
34
|
+
</box>
|
|
35
|
+
</box>,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if (index < props.steps.length - 1) {
|
|
39
|
+
const lineFg = isPast ? theme.color.primary : theme.color.mutedForeground
|
|
40
|
+
|
|
41
|
+
items.push(
|
|
42
|
+
<box key={`line-${index}`} paddingLeft={1} paddingRight={1}>
|
|
43
|
+
<text fg={lineFg}>──</text>
|
|
44
|
+
</box>,
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<box flexDirection="row" alignItems="center">
|
|
51
|
+
{items}
|
|
52
|
+
</box>
|
|
53
|
+
)
|
|
54
|
+
}
|