@interactive-inc/claude-funnel 0.10.0 → 0.15.1
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 +106 -56
- package/dist/bin.js +557 -530
- package/dist/connectors/schedule.d.ts +2 -49
- package/dist/connectors/schedule.js +1 -1
- package/dist/connectors/slack.d.ts +4 -48
- package/dist/connectors/slack.js +1 -1
- package/dist/gateway/daemon.js +213 -211
- package/dist/index.d.ts +465 -173
- package/dist/index.js +692 -154
- package/dist/{schedule-connector-schema-CkuIQ0JQ.js → schedule-connector-schema-FxP7LPlx.js} +11 -0
- package/dist/{file-system-Co60LrmR.d.ts → schedule-listener-BPodvbld.d.ts} +56 -1
- package/dist/{slack-connector-schema-Cd22WiHB.js → slack-connector-schema-B4hsf3AY.js} +10 -1
- package/dist/slack-listener-CHj6uMY-.d.ts +74 -0
- package/package.json +2 -6
- package/schemas/funnel.schema.json +144 -0
- package/dist/slack-connector-schema-D7zAHN8k.d.ts +0 -15
- package/lib/bin.ts +0 -3
- package/lib/cli/factory.ts +0 -10
- package/lib/cli/index.ts +0 -85
- package/lib/cli/router/query-to-cli-args.ts +0 -20
- package/lib/cli/router/to-request.ts +0 -113
- package/lib/cli/router/validator.ts +0 -27
- package/lib/cli/routes/channels.$channel.connectors.$connector.rename.$newName.ts +0 -27
- package/lib/cli/routes/channels.$channel.connectors.$connector.request.ts +0 -40
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.add.$id.ts +0 -41
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.remove.$id.ts +0 -22
- package/lib/cli/routes/channels.$channel.connectors.$connector.schedules.ts +0 -23
- package/lib/cli/routes/channels.$channel.connectors.$connector.ts +0 -26
- package/lib/cli/routes/channels.$channel.connectors.add.$connector.ts +0 -92
- package/lib/cli/routes/channels.$channel.connectors.remove.$connector.ts +0 -22
- package/lib/cli/routes/channels.$channel.connectors.set.$connector.ts +0 -63
- package/lib/cli/routes/channels.$channel.connectors.ts +0 -26
- package/lib/cli/routes/channels.$channel.publish.ts +0 -52
- package/lib/cli/routes/channels.$channel.rename.$newName.ts +0 -22
- package/lib/cli/routes/channels.$channel.set.delivery.$mode.ts +0 -34
- package/lib/cli/routes/channels.$channel.ts +0 -34
- package/lib/cli/routes/channels.add.$channel.ts +0 -33
- package/lib/cli/routes/channels.remove.$channel.ts +0 -20
- package/lib/cli/routes/channels.ts +0 -39
- package/lib/cli/routes/claude.ts +0 -70
- package/lib/cli/routes/gateway.listeners.ts +0 -41
- package/lib/cli/routes/gateway.logs.ts +0 -123
- package/lib/cli/routes/gateway.restart.ts +0 -50
- package/lib/cli/routes/gateway.run.ts +0 -41
- package/lib/cli/routes/gateway.start.ts +0 -50
- package/lib/cli/routes/gateway.status.ts +0 -19
- package/lib/cli/routes/gateway.stop.ts +0 -32
- package/lib/cli/routes/gateway.ts +0 -55
- package/lib/cli/routes/index.ts +0 -219
- package/lib/cli/routes/profiles.$profile.as-default.ts +0 -22
- package/lib/cli/routes/profiles.$profile.rename.$newName.ts +0 -22
- package/lib/cli/routes/profiles.$profile.run.ts +0 -36
- package/lib/cli/routes/profiles.add.$profile.ts +0 -49
- package/lib/cli/routes/profiles.remove.$profile.ts +0 -20
- package/lib/cli/routes/profiles.set.$profile.ts +0 -45
- package/lib/cli/routes/profiles.ts +0 -40
- package/lib/cli/routes/status.ts +0 -93
- package/lib/cli/routes/update.ts +0 -27
- package/lib/connectors/connector-adapter.ts +0 -9
- package/lib/connectors/connector-config-schema.ts +0 -16
- package/lib/connectors/connector-factory.ts +0 -94
- package/lib/connectors/connector-listener.ts +0 -20
- package/lib/connectors/discord-adapter.ts +0 -51
- package/lib/connectors/discord-connector-schema.ts +0 -12
- package/lib/connectors/discord-event-processor.ts +0 -48
- package/lib/connectors/discord-listener.ts +0 -111
- package/lib/connectors/discord.ts +0 -4
- package/lib/connectors/gh-adapter.ts +0 -48
- package/lib/connectors/gh-connector-schema.ts +0 -12
- package/lib/connectors/gh-listener.ts +0 -137
- package/lib/connectors/gh.ts +0 -3
- package/lib/connectors/match-cron.ts +0 -78
- package/lib/connectors/schedule-connector-schema.ts +0 -33
- package/lib/connectors/schedule-listener.ts +0 -207
- package/lib/connectors/schedule-state-store.ts +0 -54
- package/lib/connectors/schedule.ts +0 -4
- package/lib/connectors/slack-adapter.ts +0 -36
- package/lib/connectors/slack-connector-schema.ts +0 -13
- package/lib/connectors/slack-event-processor.ts +0 -97
- package/lib/connectors/slack-listener.ts +0 -97
- package/lib/connectors/slack.ts +0 -4
- package/lib/engine/channels/channels.ts +0 -520
- package/lib/engine/claude/claude.ts +0 -205
- package/lib/engine/claude/gateway-controller.ts +0 -4
- package/lib/engine/fs/file-system.ts +0 -23
- package/lib/engine/fs/memory-file-system.ts +0 -102
- package/lib/engine/fs/node-file-system.ts +0 -68
- package/lib/engine/http/http-client.ts +0 -17
- package/lib/engine/http/memory-http-client.ts +0 -36
- package/lib/engine/http/node-http-client.ts +0 -23
- package/lib/engine/id/id-generator.ts +0 -7
- package/lib/engine/id/memory-id-generator.ts +0 -20
- package/lib/engine/id/node-id-generator.ts +0 -7
- package/lib/engine/logger/logger.ts +0 -11
- package/lib/engine/logger/memory-logger.ts +0 -28
- package/lib/engine/logger/node-logger.ts +0 -49
- package/lib/engine/logger/noop-logger.ts +0 -9
- package/lib/engine/mcp/channel-server.ts +0 -123
- package/lib/engine/mcp/channel-subscriber.ts +0 -82
- package/lib/engine/mcp/mcp.ts +0 -126
- package/lib/engine/mcp/read-channel-connectors.ts +0 -34
- package/lib/engine/mcp/read-gateway-token.ts +0 -16
- package/lib/engine/mcp/usage-hint-for-type.ts +0 -15
- package/lib/engine/process/memory-process-runner.ts +0 -88
- package/lib/engine/process/node-process-runner.ts +0 -91
- package/lib/engine/process/process-runner.ts +0 -33
- package/lib/engine/profiles/profile-channel-checker.ts +0 -7
- package/lib/engine/profiles/profiles.ts +0 -126
- package/lib/engine/settings/mock-settings-reader.ts +0 -27
- package/lib/engine/settings/settings-reader.ts +0 -6
- package/lib/engine/settings/settings-schema.ts +0 -48
- package/lib/engine/settings/settings-store.ts +0 -110
- package/lib/engine/time/clock.ts +0 -15
- package/lib/engine/time/memory-clock.ts +0 -26
- package/lib/engine/time/node-clock.ts +0 -7
- package/lib/funnel.ts +0 -294
- package/lib/gateway/auth-middleware.ts +0 -44
- package/lib/gateway/broadcaster.ts +0 -319
- package/lib/gateway/channel-publisher.ts +0 -67
- package/lib/gateway/daemon.ts +0 -47
- package/lib/gateway/factory.ts +0 -10
- package/lib/gateway/funnel-event-store.ts +0 -155
- package/lib/gateway/gateway-server.ts +0 -426
- package/lib/gateway/gateway-token.ts +0 -79
- package/lib/gateway/gateway.ts +0 -209
- package/lib/gateway/kill-competing-slack-gateways.ts +0 -56
- package/lib/gateway/listener-supervisor.ts +0 -339
- package/lib/gateway/listeners-client.ts +0 -128
- package/lib/gateway/publish-schema.ts +0 -27
- package/lib/gateway/resolve-daemon-script.ts +0 -26
- package/lib/gateway/routes/channels.connectors.call.ts +0 -39
- package/lib/gateway/routes/channels.publish.ts +0 -44
- package/lib/gateway/routes/health.ts +0 -13
- package/lib/gateway/routes/index.ts +0 -26
- package/lib/gateway/routes/listeners.list.ts +0 -6
- package/lib/gateway/routes/listeners.restart.ts +0 -15
- package/lib/gateway/routes/listeners.start.ts +0 -15
- package/lib/gateway/routes/listeners.stop.ts +0 -15
- package/lib/gateway/routes/route-deps.ts +0 -19
- package/lib/gateway/routes/status.ts +0 -15
- package/lib/gateway/routes/validator.ts +0 -17
- package/lib/index.ts +0 -67
- package/lib/logger/leuco-human-file-writer.ts +0 -65
- package/lib/logger/leuco-human-logger.ts +0 -98
- package/lib/logger/leuco-human-record.ts +0 -16
- package/lib/logger/leuco-human-stdout-writer.ts +0 -26
- package/lib/logger/leuco-human-writer.ts +0 -14
- package/lib/logger/leuco-logger-memory-sink.ts +0 -67
- package/lib/logger/leuco-logger-record.ts +0 -13
- package/lib/logger/leuco-logger-sink.ts +0 -33
- package/lib/logger/leuco-logger-sqlite-sink.ts +0 -355
- package/lib/logger/leuco-logger.ts +0 -135
- package/lib/tui/app.tsx +0 -357
- package/lib/tui/components/add-row.tsx +0 -18
- package/lib/tui/components/brand.tsx +0 -27
- package/lib/tui/components/card.tsx +0 -44
- package/lib/tui/components/detail-bar.tsx +0 -46
- package/lib/tui/components/editable-field.tsx +0 -33
- package/lib/tui/components/empty-state.tsx +0 -11
- package/lib/tui/components/gateway-status.tsx +0 -66
- package/lib/tui/components/keymap.tsx +0 -29
- package/lib/tui/components/menu-item.tsx +0 -73
- package/lib/tui/components/menu.tsx +0 -26
- package/lib/tui/components/panel-header.tsx +0 -22
- package/lib/tui/components/readonly-field.tsx +0 -18
- package/lib/tui/components/section-header.tsx +0 -25
- package/lib/tui/components/selection-accent.tsx +0 -32
- package/lib/tui/components/session-item.tsx +0 -33
- package/lib/tui/components/session-list.tsx +0 -33
- package/lib/tui/components/ui/hascii/accordion-item.tsx +0 -88
- package/lib/tui/components/ui/hascii/accordion.tsx +0 -96
- package/lib/tui/components/ui/hascii/alert-dialog.tsx +0 -43
- package/lib/tui/components/ui/hascii/badge.tsx +0 -51
- package/lib/tui/components/ui/hascii/breadcrumb.tsx +0 -58
- package/lib/tui/components/ui/hascii/button.tsx +0 -194
- package/lib/tui/components/ui/hascii/card-content.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-description.tsx +0 -13
- package/lib/tui/components/ui/hascii/card-footer.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/card-title.tsx +0 -13
- package/lib/tui/components/ui/hascii/card.tsx +0 -27
- package/lib/tui/components/ui/hascii/checkbox.tsx +0 -65
- package/lib/tui/components/ui/hascii/command.tsx +0 -159
- package/lib/tui/components/ui/hascii/dialog-content.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-description.tsx +0 -13
- package/lib/tui/components/ui/hascii/dialog-footer.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/dialog-title.tsx +0 -13
- package/lib/tui/components/ui/hascii/dialog.tsx +0 -27
- package/lib/tui/components/ui/hascii/file-tree.tsx +0 -142
- package/lib/tui/components/ui/hascii/focus-group.tsx +0 -62
- package/lib/tui/components/ui/hascii/form-item.tsx +0 -43
- package/lib/tui/components/ui/hascii/input-otp.tsx +0 -86
- package/lib/tui/components/ui/hascii/input.tsx +0 -130
- package/lib/tui/components/ui/hascii/pagination.tsx +0 -105
- package/lib/tui/components/ui/hascii/progress.tsx +0 -28
- package/lib/tui/components/ui/hascii/select.tsx +0 -131
- package/lib/tui/components/ui/hascii/separator.tsx +0 -35
- package/lib/tui/components/ui/hascii/sidebar-content.tsx +0 -23
- package/lib/tui/components/ui/hascii/sidebar-header.tsx +0 -14
- package/lib/tui/components/ui/hascii/sidebar-menu-item.tsx +0 -67
- package/lib/tui/components/ui/hascii/sidebar.tsx +0 -24
- package/lib/tui/components/ui/hascii/skeleton.tsx +0 -60
- package/lib/tui/components/ui/hascii/slider.tsx +0 -91
- package/lib/tui/components/ui/hascii/snackbar.tsx +0 -75
- package/lib/tui/components/ui/hascii/sparkline.tsx +0 -53
- package/lib/tui/components/ui/hascii/spinner.tsx +0 -47
- package/lib/tui/components/ui/hascii/stepper.tsx +0 -54
- package/lib/tui/components/ui/hascii/switch.tsx +0 -66
- package/lib/tui/components/ui/hascii/table.tsx +0 -95
- package/lib/tui/components/ui/hascii/tabs.tsx +0 -59
- package/lib/tui/components/ui/hascii/toggle-group-item.tsx +0 -45
- package/lib/tui/components/ui/hascii/toggle-group.tsx +0 -99
- package/lib/tui/components/ui/hascii/tree.tsx +0 -104
- package/lib/tui/components/view-shell.tsx +0 -44
- package/lib/tui/filter-input.tsx +0 -33
- package/lib/tui/hooks/hascii/use-pressable.ts +0 -54
- package/lib/tui/parse-comma-list.ts +0 -14
- package/lib/tui/profile-launcher.tsx +0 -61
- package/lib/tui/scrollbar-options.ts +0 -19
- package/lib/tui/sidebar.tsx +0 -50
- package/lib/tui/theme.ts +0 -40
- package/lib/tui/tui.tsx +0 -20
- package/lib/tui/types.ts +0 -38
- package/lib/tui/unique-name.ts +0 -18
- package/lib/tui/use-event-stream.ts +0 -133
- package/lib/tui/use-snapshot.ts +0 -99
- package/lib/tui/utils/hascii/form-item-context.tsx +0 -23
- package/lib/tui/utils/hascii/input-focus-context.tsx +0 -31
- package/lib/tui/utils/hascii/theme-context.tsx +0 -26
- package/lib/tui/utils/hascii/theme.ts +0 -176
- package/lib/tui/views/channels-view.tsx +0 -108
- package/lib/tui/views/connectors-view.tsx +0 -164
- package/lib/tui/views/events-view.tsx +0 -160
- package/lib/tui/views/listeners-view.tsx +0 -80
- package/lib/tui/views/profiles-view.tsx +0 -152
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { useState } 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 FileTreeNode = {
|
|
7
|
-
id: string
|
|
8
|
-
label: string
|
|
9
|
-
children?: FileTreeNode[]
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export type Props = {
|
|
13
|
-
nodes: FileTreeNode[]
|
|
14
|
-
indent?: number
|
|
15
|
-
defaultExpanded?: string[]
|
|
16
|
-
expanded?: string[]
|
|
17
|
-
onToggle?: (id: string, isOpen: boolean) => void
|
|
18
|
-
selectedId?: string | null
|
|
19
|
-
onSelect?: (id: string) => void
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
type Row = {
|
|
23
|
-
node: FileTreeNode
|
|
24
|
-
depth: number
|
|
25
|
-
hasChildren: boolean
|
|
26
|
-
isOpen: boolean
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type RowProps = {
|
|
30
|
-
row: Row
|
|
31
|
-
indent: number
|
|
32
|
-
isSelected: boolean
|
|
33
|
-
onPress: () => void
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const pickRowBg = (
|
|
37
|
-
isActive: boolean,
|
|
38
|
-
isHovered: boolean,
|
|
39
|
-
isPressed: boolean,
|
|
40
|
-
theme: HasciiTheme,
|
|
41
|
-
): string | undefined => {
|
|
42
|
-
if (isPressed) return theme.color.secondaryActive
|
|
43
|
-
if (isHovered && isActive) return theme.color.hoverActive
|
|
44
|
-
if (isHovered) return theme.color.secondaryHover
|
|
45
|
-
if (isActive) return theme.color.secondaryActive
|
|
46
|
-
return undefined
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Internal row used by HasciiFileTree. Tracks the standard hover/active palette and renders ▾/▸ for folders. */
|
|
50
|
-
function HasciiFileTreeRow(props: RowProps) {
|
|
51
|
-
const theme = useHasciiTheme()
|
|
52
|
-
const press = usePressable({ onPress: props.onPress })
|
|
53
|
-
|
|
54
|
-
const bg = pickRowBg(props.isSelected, press.isHovered, press.isPressed, theme)
|
|
55
|
-
|
|
56
|
-
const indentText = " ".repeat(props.row.depth * props.indent)
|
|
57
|
-
const marker = !props.row.hasChildren ? " " : props.row.isOpen ? "▾" : "▸"
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<box
|
|
61
|
-
flexDirection="row"
|
|
62
|
-
alignItems="center"
|
|
63
|
-
paddingLeft={1}
|
|
64
|
-
paddingRight={1}
|
|
65
|
-
height={1}
|
|
66
|
-
backgroundColor={bg}
|
|
67
|
-
{...press.bind}
|
|
68
|
-
>
|
|
69
|
-
<text fg={theme.color.mutedForeground}>{indentText}</text>
|
|
70
|
-
<text fg={theme.color.mutedForeground}>{marker} </text>
|
|
71
|
-
<text fg={theme.color.foreground}>{props.row.node.label}</text>
|
|
72
|
-
</box>
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const flatten = (nodes: FileTreeNode[], depth: number, openSet: Set<string>): Row[] => {
|
|
77
|
-
const rows: Row[] = []
|
|
78
|
-
|
|
79
|
-
for (const node of nodes) {
|
|
80
|
-
const hasChildren = (node.children?.length ?? 0) > 0
|
|
81
|
-
const isOpen = openSet.has(node.id)
|
|
82
|
-
|
|
83
|
-
rows.push({ node, depth, hasChildren, isOpen })
|
|
84
|
-
|
|
85
|
-
if (hasChildren && isOpen) {
|
|
86
|
-
const childRows = flatten(node.children ?? [], depth + 1, openSet)
|
|
87
|
-
for (const childRow of childRows) rows.push(childRow)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return rows
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** Indented IDE-style file tree. Folder rows show ▾/▸ and toggle on click; leaf rows just select. */
|
|
95
|
-
export function HasciiFileTree(props: Props) {
|
|
96
|
-
const indent = props.indent ?? 2
|
|
97
|
-
|
|
98
|
-
const internalState = useState<string[]>(props.defaultExpanded ?? [])
|
|
99
|
-
const internalOpen = internalState[0]
|
|
100
|
-
const setInternalOpen = internalState[1]
|
|
101
|
-
|
|
102
|
-
const expanded = props.expanded ?? internalOpen
|
|
103
|
-
const openSet = new Set(expanded)
|
|
104
|
-
|
|
105
|
-
const selectedState = useState<string | null>(props.selectedId ?? null)
|
|
106
|
-
const internalSelected = selectedState[0]
|
|
107
|
-
const setInternalSelected = selectedState[1]
|
|
108
|
-
|
|
109
|
-
const selected = props.selectedId !== undefined ? props.selectedId : internalSelected
|
|
110
|
-
|
|
111
|
-
const toggle = (id: string) => {
|
|
112
|
-
const isOpen = openSet.has(id)
|
|
113
|
-
const next = isOpen ? expanded.filter((entry) => entry !== id) : [...expanded, id]
|
|
114
|
-
|
|
115
|
-
if (props.expanded === undefined) setInternalOpen(next)
|
|
116
|
-
props.onToggle?.(id, !isOpen)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const select = (id: string) => {
|
|
120
|
-
if (props.selectedId === undefined) setInternalSelected(id)
|
|
121
|
-
props.onSelect?.(id)
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const rows = flatten(props.nodes, 0, openSet)
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<box flexDirection="column">
|
|
128
|
-
{rows.map((row) => (
|
|
129
|
-
<HasciiFileTreeRow
|
|
130
|
-
key={row.node.id}
|
|
131
|
-
row={row}
|
|
132
|
-
indent={indent}
|
|
133
|
-
isSelected={row.node.id === selected}
|
|
134
|
-
onPress={() => {
|
|
135
|
-
select(row.node.id)
|
|
136
|
-
if (row.hasChildren) toggle(row.node.id)
|
|
137
|
-
}}
|
|
138
|
-
/>
|
|
139
|
-
))}
|
|
140
|
-
</box>
|
|
141
|
-
)
|
|
142
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { useKeyboard } from "@opentui/react"
|
|
2
|
-
import { createContext, useContext, useState } from "react"
|
|
3
|
-
import type { ReactNode } from "react"
|
|
4
|
-
|
|
5
|
-
type ContextValue = {
|
|
6
|
-
currentId: string | null
|
|
7
|
-
setCurrentId: (id: string) => void
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const HasciiFocusContext = createContext<ContextValue | null>(null)
|
|
11
|
-
|
|
12
|
-
/** Returns whether the provided focusId matches the surrounding HasciiFocusGroup's current focus. */
|
|
13
|
-
export function useHasciiFocus(focusId: string | undefined): boolean {
|
|
14
|
-
const ctx = useContext(HasciiFocusContext)
|
|
15
|
-
|
|
16
|
-
if (!ctx || focusId === undefined) return false
|
|
17
|
-
return ctx.currentId === focusId
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Returns the imperative API of the surrounding HasciiFocusGroup. Null when used outside a group. */
|
|
21
|
-
export function useHasciiFocusController(): ContextValue | null {
|
|
22
|
-
return useContext(HasciiFocusContext)
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Computes the next focus index. Wraps at both ends. */
|
|
26
|
-
export function nextFocusIndex(currentIndex: number, length: number, isShift: boolean): number {
|
|
27
|
-
if (length === 0) return -1
|
|
28
|
-
|
|
29
|
-
return isShift ? (currentIndex - 1 + length) % length : (currentIndex + 1) % length
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type Props = {
|
|
33
|
-
ids: readonly string[]
|
|
34
|
-
defaultId?: string
|
|
35
|
-
children: ReactNode
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Manages keyboard focus across an ordered list of children. Tab cycles forward; Shift+Tab cycles back. */
|
|
39
|
-
export function HasciiFocusGroup(props: Props) {
|
|
40
|
-
const initialId = props.defaultId ?? props.ids[0] ?? null
|
|
41
|
-
|
|
42
|
-
const currentState = useState<string | null>(initialId)
|
|
43
|
-
const currentId = currentState[0]
|
|
44
|
-
const setCurrentId = currentState[1]
|
|
45
|
-
|
|
46
|
-
useKeyboard((key) => {
|
|
47
|
-
if (key.name !== "tab" || props.ids.length === 0) return
|
|
48
|
-
|
|
49
|
-
const currentIndex = currentId ? props.ids.indexOf(currentId) : -1
|
|
50
|
-
const next = nextFocusIndex(currentIndex, props.ids.length, key.shift ?? false)
|
|
51
|
-
|
|
52
|
-
const target = props.ids[next]
|
|
53
|
-
if (target !== undefined) setCurrentId(target)
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
const value: ContextValue = {
|
|
57
|
-
currentId,
|
|
58
|
-
setCurrentId: (id: string) => setCurrentId(id),
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return <HasciiFocusContext.Provider value={value}>{props.children}</HasciiFocusContext.Provider>
|
|
62
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { useId } from "react"
|
|
2
|
-
import type { ReactNode } from "react"
|
|
3
|
-
import { HasciiFormItemProvider } from "@/tui/utils/hascii/form-item-context"
|
|
4
|
-
import { useHasciiInputFocus } from "@/tui/utils/hascii/input-focus-context"
|
|
5
|
-
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
6
|
-
|
|
7
|
-
export type Props = {
|
|
8
|
-
label: string
|
|
9
|
-
labelWidth?: number
|
|
10
|
-
children: ReactNode
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/** Horizontal form row: a fixed-width label on the left, the field on the right. The label background brightens while the wrapped HasciiInput is focused (requires HasciiInputFocusProvider in the tree). */
|
|
14
|
-
export function HasciiFormItem(props: Props) {
|
|
15
|
-
const labelWidth = props.labelWidth ?? 12
|
|
16
|
-
const theme = useHasciiTheme()
|
|
17
|
-
|
|
18
|
-
const id = useId()
|
|
19
|
-
const inputFocus = useHasciiInputFocus()
|
|
20
|
-
const isInputFocused = inputFocus?.focusedId === id
|
|
21
|
-
|
|
22
|
-
const labelBg = isInputFocused ? theme.color.secondaryActive : theme.color.popover
|
|
23
|
-
const labelFg = isInputFocused ? theme.color.foreground : theme.color.mutedForeground
|
|
24
|
-
|
|
25
|
-
return (
|
|
26
|
-
<HasciiFormItemProvider value={{ focusId: id }}>
|
|
27
|
-
<box flexDirection="row" alignItems="center">
|
|
28
|
-
<box
|
|
29
|
-
width={labelWidth}
|
|
30
|
-
height={3}
|
|
31
|
-
paddingLeft={2}
|
|
32
|
-
paddingRight={2}
|
|
33
|
-
alignItems="flex-start"
|
|
34
|
-
justifyContent="center"
|
|
35
|
-
backgroundColor={labelBg}
|
|
36
|
-
>
|
|
37
|
-
<text fg={labelFg}>{props.label}</text>
|
|
38
|
-
</box>
|
|
39
|
-
{props.children}
|
|
40
|
-
</box>
|
|
41
|
-
</HasciiFormItemProvider>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { useKeyboard } from "@opentui/react"
|
|
2
|
-
import { useState } from "react"
|
|
3
|
-
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
4
|
-
|
|
5
|
-
export type Props = {
|
|
6
|
-
length?: number
|
|
7
|
-
value?: string
|
|
8
|
-
defaultValue?: string
|
|
9
|
-
isFocused?: boolean
|
|
10
|
-
onChange?: (value: string) => void
|
|
11
|
-
onComplete?: (value: string) => void
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const isDigit = (key: string): boolean => /^[0-9]$/.test(key)
|
|
15
|
-
|
|
16
|
-
/** OTP slot row. Uncontrolled by default — type digits to fill, backspace to erase. */
|
|
17
|
-
export function HasciiInputOtp(props: Props) {
|
|
18
|
-
const length = props.length ?? 6
|
|
19
|
-
const isFocused = props.isFocused ?? true
|
|
20
|
-
const theme = useHasciiTheme()
|
|
21
|
-
|
|
22
|
-
const internalState = useState(props.defaultValue ?? "")
|
|
23
|
-
const internal = internalState[0]
|
|
24
|
-
const setInternal = internalState[1]
|
|
25
|
-
|
|
26
|
-
const value = props.value ?? internal
|
|
27
|
-
|
|
28
|
-
const setValue = (next: string) => {
|
|
29
|
-
if (props.value === undefined) setInternal(next)
|
|
30
|
-
props.onChange?.(next)
|
|
31
|
-
|
|
32
|
-
if (next.length === length) props.onComplete?.(next)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
useKeyboard((key) => {
|
|
36
|
-
if (!isFocused) return
|
|
37
|
-
|
|
38
|
-
if (key.name === "backspace") {
|
|
39
|
-
if (value.length > 0) setValue(value.slice(0, -1))
|
|
40
|
-
return
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (isDigit(key.name) && value.length < length) {
|
|
44
|
-
setValue(value + key.name)
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
const slots: number[] = []
|
|
49
|
-
for (let index = 0; index < length; index++) slots.push(index)
|
|
50
|
-
|
|
51
|
-
const focusedIndex = Math.min(value.length, length - 1)
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<box flexDirection="row" gap={1}>
|
|
55
|
-
{slots.map((index) => {
|
|
56
|
-
const char = value[index]
|
|
57
|
-
const isSlotFocused = isFocused && index === focusedIndex
|
|
58
|
-
const isFilled = char !== undefined
|
|
59
|
-
|
|
60
|
-
const borderColor = isSlotFocused
|
|
61
|
-
? theme.color.ring
|
|
62
|
-
: isFilled
|
|
63
|
-
? theme.color.border
|
|
64
|
-
: theme.color.input
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<box
|
|
68
|
-
key={index}
|
|
69
|
-
border
|
|
70
|
-
borderStyle="rounded"
|
|
71
|
-
borderColor={borderColor}
|
|
72
|
-
backgroundColor={theme.color.background}
|
|
73
|
-
width={5}
|
|
74
|
-
height={3}
|
|
75
|
-
alignItems="center"
|
|
76
|
-
justifyContent="center"
|
|
77
|
-
>
|
|
78
|
-
<text fg={isFilled ? theme.color.foreground : theme.color.mutedForeground}>
|
|
79
|
-
{char ?? "·"}
|
|
80
|
-
</text>
|
|
81
|
-
</box>
|
|
82
|
-
)
|
|
83
|
-
})}
|
|
84
|
-
</box>
|
|
85
|
-
)
|
|
86
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { useKeyboard } from "@opentui/react"
|
|
2
|
-
import { useId, useState } from "react"
|
|
3
|
-
import { useHasciiFormItem } from "@/tui/utils/hascii/form-item-context"
|
|
4
|
-
import { useHasciiInputFocus } from "@/tui/utils/hascii/input-focus-context"
|
|
5
|
-
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
6
|
-
import { usePressable } from "@/tui/hooks/hascii/use-pressable"
|
|
7
|
-
|
|
8
|
-
type Variant = "default" | "outline"
|
|
9
|
-
|
|
10
|
-
export type Props = {
|
|
11
|
-
variant?: Variant
|
|
12
|
-
placeholder?: string
|
|
13
|
-
value?: string
|
|
14
|
-
width?: number
|
|
15
|
-
defaultFocused?: boolean
|
|
16
|
-
onInput?: (value: string) => void
|
|
17
|
-
onChange?: (value: string) => void
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Single-line text input. Click to focus, Esc / outside click to blur (requires HasciiInputFocusProvider for outside click). */
|
|
21
|
-
export function HasciiInput(props: Props) {
|
|
22
|
-
const variant = props.variant ?? "default"
|
|
23
|
-
const width = props.width ?? 32
|
|
24
|
-
const placeholder = props.placeholder ?? ""
|
|
25
|
-
|
|
26
|
-
const fallbackId = useId()
|
|
27
|
-
const formItem = useHasciiFormItem()
|
|
28
|
-
const id = formItem?.focusId ?? fallbackId
|
|
29
|
-
const focusCtx = useHasciiInputFocus()
|
|
30
|
-
const fallbackState = useState(props.defaultFocused ?? false)
|
|
31
|
-
const isFocused = focusCtx ? focusCtx.focusedId === id : fallbackState[0]
|
|
32
|
-
|
|
33
|
-
const focus = (): void => {
|
|
34
|
-
if (focusCtx) focusCtx.setFocusedId(id)
|
|
35
|
-
else fallbackState[1](true)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const blur = (): void => {
|
|
39
|
-
if (focusCtx) focusCtx.setFocusedId(null)
|
|
40
|
-
else fallbackState[1](false)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const theme = useHasciiTheme()
|
|
44
|
-
const press = usePressable()
|
|
45
|
-
|
|
46
|
-
useKeyboard((key) => {
|
|
47
|
-
if (!isFocused) return
|
|
48
|
-
if (key.name === "escape") blur()
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
if (variant === "outline") {
|
|
52
|
-
const borderColor = press.isPressed
|
|
53
|
-
? theme.color.foreground
|
|
54
|
-
: isFocused
|
|
55
|
-
? theme.color.ring
|
|
56
|
-
: press.isHovered
|
|
57
|
-
? theme.color.mutedForeground
|
|
58
|
-
: theme.color.input
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<box
|
|
62
|
-
border
|
|
63
|
-
borderStyle="rounded"
|
|
64
|
-
borderColor={borderColor}
|
|
65
|
-
height={3}
|
|
66
|
-
width={width}
|
|
67
|
-
paddingLeft={1}
|
|
68
|
-
paddingRight={1}
|
|
69
|
-
backgroundColor={theme.color.background}
|
|
70
|
-
justifyContent="center"
|
|
71
|
-
{...press.bind}
|
|
72
|
-
onMouseDown={(event) => {
|
|
73
|
-
event.stopPropagation()
|
|
74
|
-
press.bind.onMouseDown()
|
|
75
|
-
focus()
|
|
76
|
-
}}
|
|
77
|
-
>
|
|
78
|
-
<input
|
|
79
|
-
focused={isFocused}
|
|
80
|
-
placeholder={placeholder}
|
|
81
|
-
value={props.value}
|
|
82
|
-
textColor={theme.color.foreground}
|
|
83
|
-
placeholderColor={theme.color.mutedForeground}
|
|
84
|
-
cursorColor={theme.color.foreground}
|
|
85
|
-
onInput={props.onInput}
|
|
86
|
-
onChange={props.onChange}
|
|
87
|
-
/>
|
|
88
|
-
</box>
|
|
89
|
-
)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const bg = press.isPressed
|
|
93
|
-
? theme.color.secondaryActive
|
|
94
|
-
: isFocused || press.isHovered
|
|
95
|
-
? theme.color.secondaryHover
|
|
96
|
-
: theme.color.muted
|
|
97
|
-
|
|
98
|
-
return (
|
|
99
|
-
<box
|
|
100
|
-
height={3}
|
|
101
|
-
width={width}
|
|
102
|
-
paddingLeft={2}
|
|
103
|
-
paddingRight={2}
|
|
104
|
-
backgroundColor={bg}
|
|
105
|
-
justifyContent="center"
|
|
106
|
-
{...press.bind}
|
|
107
|
-
onMouseDown={(event) => {
|
|
108
|
-
event.stopPropagation()
|
|
109
|
-
press.bind.onMouseDown()
|
|
110
|
-
focus()
|
|
111
|
-
}}
|
|
112
|
-
>
|
|
113
|
-
<input
|
|
114
|
-
focused={isFocused}
|
|
115
|
-
placeholder={placeholder}
|
|
116
|
-
value={props.value}
|
|
117
|
-
textColor={theme.color.foreground}
|
|
118
|
-
placeholderColor={theme.color.mutedForeground}
|
|
119
|
-
cursorColor={theme.color.foreground}
|
|
120
|
-
onInput={props.onInput}
|
|
121
|
-
onChange={props.onChange}
|
|
122
|
-
/>
|
|
123
|
-
{isFocused ? (
|
|
124
|
-
<box position="absolute" bottom={0} left={0} right={0}>
|
|
125
|
-
<text fg={theme.color.primary}>{"▁".repeat(width)}</text>
|
|
126
|
-
</box>
|
|
127
|
-
) : null}
|
|
128
|
-
</box>
|
|
129
|
-
)
|
|
130
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
2
|
-
import { usePressable } from "@/tui/hooks/hascii/use-pressable"
|
|
3
|
-
|
|
4
|
-
export type Props = {
|
|
5
|
-
page: number
|
|
6
|
-
pageCount: number
|
|
7
|
-
onChange?: (page: number) => void
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
type ButtonProps = {
|
|
11
|
-
label: string
|
|
12
|
-
isActive?: boolean
|
|
13
|
-
isDisabled?: boolean
|
|
14
|
-
onPress: () => void
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function PageButton(props: ButtonProps) {
|
|
18
|
-
const theme = useHasciiTheme()
|
|
19
|
-
const press = usePressable({
|
|
20
|
-
isDisabled: props.isDisabled,
|
|
21
|
-
onPress: props.onPress,
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
const fg = props.isDisabled
|
|
25
|
-
? theme.color.mutedForeground
|
|
26
|
-
: props.isActive
|
|
27
|
-
? theme.color.primaryForeground
|
|
28
|
-
: theme.color.foreground
|
|
29
|
-
|
|
30
|
-
const bg = props.isDisabled
|
|
31
|
-
? undefined
|
|
32
|
-
: props.isActive
|
|
33
|
-
? theme.color.primary
|
|
34
|
-
: press.isPressed
|
|
35
|
-
? theme.color.accentActive
|
|
36
|
-
: press.isHovered
|
|
37
|
-
? theme.color.accentHover
|
|
38
|
-
: undefined
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<box paddingLeft={1} paddingRight={1} height={1} backgroundColor={bg} {...press.bind}>
|
|
42
|
-
<text fg={fg}>{props.label}</text>
|
|
43
|
-
</box>
|
|
44
|
-
)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export const buildPageList = (page: number, pageCount: number): (number | null)[] => {
|
|
48
|
-
if (pageCount <= 7) {
|
|
49
|
-
const list: number[] = []
|
|
50
|
-
for (let index = 1; index <= pageCount; index++) list.push(index)
|
|
51
|
-
return list
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const result: (number | null)[] = [1]
|
|
55
|
-
const left = Math.max(2, page - 1)
|
|
56
|
-
const right = Math.min(pageCount - 1, page + 1)
|
|
57
|
-
|
|
58
|
-
if (left > 2) result.push(null)
|
|
59
|
-
for (let index = left; index <= right; index++) result.push(index)
|
|
60
|
-
if (right < pageCount - 1) result.push(null)
|
|
61
|
-
|
|
62
|
-
result.push(pageCount)
|
|
63
|
-
return result
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Page navigator with previous, next, and numeric jumps. Renders ellipses when collapsed. */
|
|
67
|
-
export function HasciiPagination(props: Props) {
|
|
68
|
-
const theme = useHasciiTheme()
|
|
69
|
-
|
|
70
|
-
const change = (next: number) => {
|
|
71
|
-
if (next < 1 || next > props.pageCount || next === props.page) return
|
|
72
|
-
props.onChange?.(next)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const pages = buildPageList(props.page, props.pageCount)
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<box flexDirection="row" gap={1} alignItems="center">
|
|
79
|
-
<PageButton label="<" isDisabled={props.page <= 1} onPress={() => change(props.page - 1)} />
|
|
80
|
-
{pages.map((entry, index) => {
|
|
81
|
-
if (entry === null) {
|
|
82
|
-
return (
|
|
83
|
-
<box key={`gap-${index}`} paddingLeft={1} paddingRight={1}>
|
|
84
|
-
<text fg={theme.color.mutedForeground}>…</text>
|
|
85
|
-
</box>
|
|
86
|
-
)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<PageButton
|
|
91
|
-
key={entry}
|
|
92
|
-
label={String(entry)}
|
|
93
|
-
isActive={entry === props.page}
|
|
94
|
-
onPress={() => change(entry)}
|
|
95
|
-
/>
|
|
96
|
-
)
|
|
97
|
-
})}
|
|
98
|
-
<PageButton
|
|
99
|
-
label=">"
|
|
100
|
-
isDisabled={props.page >= props.pageCount}
|
|
101
|
-
onPress={() => change(props.page + 1)}
|
|
102
|
-
/>
|
|
103
|
-
</box>
|
|
104
|
-
)
|
|
105
|
-
}
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { useHasciiTheme } from "@/tui/utils/hascii/theme-context"
|
|
2
|
-
|
|
3
|
-
export type Props = {
|
|
4
|
-
value?: number
|
|
5
|
-
width?: number
|
|
6
|
-
fillColor?: string
|
|
7
|
-
trackColor?: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Horizontal progress bar. Value is 0–1; clamped on render. */
|
|
11
|
-
export function HasciiProgress(props: Props) {
|
|
12
|
-
const value = Math.max(0, Math.min(1, props.value ?? 0))
|
|
13
|
-
const width = props.width ?? 32
|
|
14
|
-
|
|
15
|
-
const theme = useHasciiTheme()
|
|
16
|
-
const fillColor = props.fillColor ?? theme.color.primary
|
|
17
|
-
const trackColor = props.trackColor ?? theme.color.muted
|
|
18
|
-
|
|
19
|
-
const filled = Math.round(value * width)
|
|
20
|
-
const empty = width - filled
|
|
21
|
-
|
|
22
|
-
return (
|
|
23
|
-
<box flexDirection="row" width={width} height={1}>
|
|
24
|
-
{filled > 0 ? <box width={filled} height={1} backgroundColor={fillColor} /> : null}
|
|
25
|
-
{empty > 0 ? <box width={empty} height={1} backgroundColor={trackColor} /> : null}
|
|
26
|
-
</box>
|
|
27
|
-
)
|
|
28
|
-
}
|