xterm-input-panel 1.0.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.
@@ -0,0 +1,11 @@
1
+ import type { StorybookConfig } from '@storybook/web-components-vite'
2
+
3
+ const config: StorybookConfig = {
4
+ stories: ['../src/**/*.stories.@(js|ts)'],
5
+ framework: {
6
+ name: '@storybook/web-components-vite',
7
+ options: {},
8
+ },
9
+ }
10
+
11
+ export default config
@@ -0,0 +1,15 @@
1
+ import type { Preview } from '@storybook/web-components-vite'
2
+
3
+ const preview: Preview = {
4
+ parameters: {
5
+ backgrounds: {
6
+ default: 'terminal',
7
+ values: [
8
+ { name: 'terminal', value: '#1a1a1a' },
9
+ { name: 'light', value: '#ffffff' },
10
+ ],
11
+ },
12
+ },
13
+ }
14
+
15
+ export default preview
@@ -0,0 +1,8 @@
1
+ import { beforeAll } from 'vitest'
2
+ import { setProjectAnnotations } from 'storybook/preview-api'
3
+ import * as rendererAnnotations from '@storybook/web-components/entry-preview'
4
+ import * as previewAnnotations from './preview'
5
+
6
+ const annotations = setProjectAnnotations([rendererAnnotations, previewAnnotations])
7
+
8
+ beforeAll(annotations.beforeAll)
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # xterm-input-panel
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - Release all workspace packages to `1.0.0` for the new major release.
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "xterm-input-panel",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "dependencies": {
7
+ "lit": "^3.3.2",
8
+ "lucide": "^0.472.0",
9
+ "pixi.js": "^8.16.0"
10
+ },
11
+ "peerDependencies": {
12
+ "@xterm/xterm": ">=5.0.0 || >=6.0.0-beta.0"
13
+ },
14
+ "peerDependenciesMeta": {
15
+ "@xterm/xterm": {
16
+ "optional": true
17
+ }
18
+ },
19
+ "devDependencies": {
20
+ "@storybook/addon-vitest": "^10.2.8",
21
+ "@storybook/web-components": "^10.2.8",
22
+ "@storybook/web-components-vite": "^10.2.8",
23
+ "@vitest/browser": "^4.0.18",
24
+ "@vitest/browser-playwright": "^4.0.18",
25
+ "@xterm/xterm": "6.1.0-beta.152",
26
+ "playwright": "^1.58.2",
27
+ "storybook": "^10.2.8",
28
+ "typescript": "^5.7.2",
29
+ "vite": "^7.3.1",
30
+ "vitest": "^4.0.18"
31
+ },
32
+ "scripts": {
33
+ "dev": "storybook dev -p 6007",
34
+ "test:browser": "vitest run --config vitest.storybook.config.ts",
35
+ "typecheck": "tsc --noEmit"
36
+ }
37
+ }
Binary file
Binary file
Binary file
package/src/icons.ts ADDED
@@ -0,0 +1,77 @@
1
+ import type { IconNode } from 'lucide'
2
+ import {
3
+ Combine,
4
+ createElement,
5
+ Keyboard,
6
+ MousePointer2,
7
+ Move,
8
+ Pin,
9
+ PinOff,
10
+ SendHorizontal,
11
+ SlidersHorizontal,
12
+ Type,
13
+ X,
14
+ } from 'lucide'
15
+
16
+ /**
17
+ * Icon helpers for InputPanel components.
18
+ * Uses the `lucide` package for consistent, high-quality SVG icons.
19
+ * Each function returns an SVGElement that Lit can render directly in templates.
20
+ */
21
+
22
+ function createIcon(icon: IconNode, size: number): SVGElement {
23
+ const el = createElement(icon)
24
+ el.setAttribute('width', String(size))
25
+ el.setAttribute('height', String(size))
26
+ return el
27
+ }
28
+
29
+ /** X / Close icon */
30
+ export function iconX(size = 16) {
31
+ return createIcon(X, size)
32
+ }
33
+
34
+ /** Settings / Sliders icon */
35
+ export function iconSettings(size = 16) {
36
+ return createIcon(SlidersHorizontal, size)
37
+ }
38
+
39
+ /** Send icon */
40
+ export function iconSend(size = 16) {
41
+ return createIcon(SendHorizontal, size)
42
+ }
43
+
44
+ /** Text cursor / Type icon — for Input tab */
45
+ export function iconType(size = 12) {
46
+ return createIcon(Type, size)
47
+ }
48
+
49
+ /** Keyboard icon — for Keys tab */
50
+ export function iconKeyboard(size = 12) {
51
+ return createIcon(Keyboard, size)
52
+ }
53
+
54
+ /** Touchpad / Move icon — for Trackpad tab */
55
+ export function iconMove(size = 12) {
56
+ return createIcon(Move, size)
57
+ }
58
+
59
+ /** Mouse pointer icon — for cursor overlay */
60
+ export function iconMousePointer2(size = 12) {
61
+ return createIcon(MousePointer2, size)
62
+ }
63
+
64
+ /** Combine icon — for Shortcuts tab */
65
+ export function iconCombine(size = 12) {
66
+ return createIcon(Combine, size)
67
+ }
68
+
69
+ /** Pin icon — for Fixed layout */
70
+ export function iconPin(size = 12) {
71
+ return createIcon(Pin, size)
72
+ }
73
+
74
+ /** PinOff icon — for Floating layout */
75
+ export function iconPinOff(size = 12) {
76
+ return createIcon(PinOff, size)
77
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ // Side-effect imports — registers all InputPanel custom elements
2
+ import './input-method-tab.js'
3
+ import './input-panel-settings.js'
4
+ import './input-panel.js'
5
+ import './shortcut-tab.js'
6
+ import './virtual-keyboard-tab.js'
7
+ import './virtual-trackpad-tab.js'
8
+
9
+ export type { InputPanelLayout, InputPanelTab } from './input-panel.js'
10
+ export { blendHex, cssColorToHex, onThemeChange, resolvePixiTheme } from './pixi-theme.js'
11
+ export type { PixiTheme } from './pixi-theme.js'
12
+ export {
13
+ InputPanelAddon,
14
+ type InputPanelHistoryItem,
15
+ type InputPanelSettingsPayload,
16
+ } from './xterm-addon.js'
@@ -0,0 +1,131 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite'
2
+ import type { LitElement } from 'lit'
3
+ import { html } from 'lit'
4
+ import { expect, fn } from 'storybook/test'
5
+
6
+ import './input-method-tab.js'
7
+
8
+ async function getLitElement(container: HTMLElement, selector: string) {
9
+ const el = container.querySelector(selector) as LitElement
10
+ await el.updateComplete
11
+ return el
12
+ }
13
+
14
+ const meta: Meta = {
15
+ title: 'InputMethodTab',
16
+ tags: ['autodocs'],
17
+ decorators: [
18
+ (story) => html`
19
+ <div style="width: 400px; height: 250px; background: #1a1a1a; color: #fff; font-family: monospace;">
20
+ ${story()}
21
+ </div>
22
+ `,
23
+ ],
24
+ }
25
+
26
+ export default meta
27
+
28
+ /**
29
+ * Default Input Method tab with textarea, send button, and empty history slot.
30
+ */
31
+ export const Default: StoryObj = {
32
+ render: () => html`<input-method-tab style="height: 100%;"></input-method-tab>`,
33
+ }
34
+
35
+ /**
36
+ * Verifies that typing and clicking Send dispatches `input-panel:send`.
37
+ */
38
+ export const SendInput: StoryObj = {
39
+ render: () => html`<input-method-tab style="height: 100%;"></input-method-tab>`,
40
+ play: async ({ canvasElement }) => {
41
+ const tab = await getLitElement(canvasElement, 'input-method-tab')
42
+
43
+ const sendHandler = fn()
44
+ tab.addEventListener('input-panel:send', sendHandler)
45
+
46
+ const shadow = tab.shadowRoot!
47
+ const textarea = shadow.querySelector('textarea') as HTMLTextAreaElement
48
+ const sendBtn = shadow.querySelector('.send-btn') as HTMLButtonElement
49
+
50
+ // Type into the textarea
51
+ textarea.value = 'echo test'
52
+ textarea.dispatchEvent(new Event('input', { bubbles: true }))
53
+ await tab.updateComplete
54
+
55
+ // Click send
56
+ sendBtn.click()
57
+
58
+ // Wait for _send to complete
59
+ await new Promise(resolve => setTimeout(resolve, 100))
60
+
61
+ expect(sendHandler).toHaveBeenCalledTimes(1)
62
+ const detail = (sendHandler.mock.calls[0] as unknown[])[0] as CustomEvent
63
+ expect(detail.detail.data).toBe('echo test\n')
64
+
65
+ tab.removeEventListener('input-panel:send', sendHandler)
66
+ },
67
+ }
68
+
69
+ /**
70
+ * Verifies that Ctrl+Enter triggers send.
71
+ */
72
+ export const CtrlEnterSend: StoryObj = {
73
+ render: () => html`<input-method-tab style="height: 100%;"></input-method-tab>`,
74
+ play: async ({ canvasElement }) => {
75
+ const tab = await getLitElement(canvasElement, 'input-method-tab')
76
+
77
+ const sendHandler = fn()
78
+ tab.addEventListener('input-panel:send', sendHandler)
79
+
80
+ const shadow = tab.shadowRoot!
81
+ const textarea = shadow.querySelector('textarea') as HTMLTextAreaElement
82
+
83
+ // Type and trigger Ctrl+Enter
84
+ textarea.value = 'ls -la'
85
+ textarea.dispatchEvent(new Event('input', { bubbles: true }))
86
+ await tab.updateComplete
87
+
88
+ textarea.dispatchEvent(new KeyboardEvent('keydown', {
89
+ key: 'Enter',
90
+ ctrlKey: true,
91
+ bubbles: true,
92
+ }))
93
+
94
+ await new Promise(resolve => setTimeout(resolve, 100))
95
+
96
+ expect(sendHandler).toHaveBeenCalledTimes(1)
97
+ const detail = (sendHandler.mock.calls[0] as unknown[])[0] as CustomEvent
98
+ expect(detail.detail.data).toBe('ls -la\n')
99
+
100
+ tab.removeEventListener('input-panel:send', sendHandler)
101
+ },
102
+ }
103
+
104
+ /**
105
+ * Verifies that slotted history content renders inside the component.
106
+ */
107
+ export const HistorySlot: StoryObj = {
108
+ render: () => html`
109
+ <input-method-tab style="height: 100%;">
110
+ <div slot="history" class="slotted-history">
111
+ <div style="padding: 4px 6px; color: #888; font-size: 12px; cursor: pointer;">
112
+ <span style="font-size: 10px; opacity: 0.6;">14:30</span>
113
+ <span>echo hello world</span>
114
+ </div>
115
+ <div style="padding: 4px 6px; color: #888; font-size: 12px; cursor: pointer;">
116
+ <span style="font-size: 10px; opacity: 0.6;">14:25</span>
117
+ <span>ls -la</span>
118
+ </div>
119
+ </div>
120
+ </input-method-tab>
121
+ `,
122
+ play: async ({ canvasElement }) => {
123
+ await getLitElement(canvasElement, 'input-method-tab')
124
+
125
+ // Verify slotted content is visible
126
+ const slottedHistory = canvasElement.querySelector('.slotted-history')
127
+ expect(slottedHistory).not.toBeNull()
128
+ expect(slottedHistory!.textContent).toContain('echo hello world')
129
+ expect(slottedHistory!.textContent).toContain('ls -la')
130
+ },
131
+ }
@@ -0,0 +1,142 @@
1
+ import { LitElement, html, css } from 'lit'
2
+ import { iconSend } from './icons.js'
3
+
4
+ /**
5
+ * Input Method tab — textarea + send button + slotted history.
6
+ *
7
+ * Dispatches `input-panel:send` CustomEvent<{ data: string }>.
8
+ * History is decoupled — use `<slot name="history">` to provide history content.
9
+ */
10
+ export class InputMethodTab extends LitElement {
11
+ static get properties() {
12
+ return {
13
+ _inputValue: { state: true },
14
+ }
15
+ }
16
+
17
+ static styles = css`
18
+ :host {
19
+ display: flex;
20
+ flex-direction: column;
21
+ height: 100%;
22
+ }
23
+
24
+ .input-area {
25
+ display: flex;
26
+ gap: 6px;
27
+ padding: 8px;
28
+ border-bottom: 1px solid var(--border, #333);
29
+ }
30
+
31
+ textarea {
32
+ flex: 1;
33
+ min-height: 60px;
34
+ max-height: 120px;
35
+ resize: vertical;
36
+ background: var(--background, #000);
37
+ color: var(--foreground, #fff);
38
+ border: 1px solid var(--border, #333);
39
+ border-radius: 3px;
40
+ padding: 6px 8px;
41
+ font-family: inherit;
42
+ font-size: 13px;
43
+ outline: none;
44
+ }
45
+
46
+ textarea:focus {
47
+ border-color: var(--primary, #e04a2f);
48
+ }
49
+
50
+ .send-btn {
51
+ display: inline-flex;
52
+ align-items: center;
53
+ gap: 4px;
54
+ align-self: flex-end;
55
+ padding: 6px 16px;
56
+ background: var(--primary, #e04a2f);
57
+ color: var(--primary-foreground, #fff);
58
+ border: none;
59
+ border-radius: 3px;
60
+ cursor: pointer;
61
+ font-family: inherit;
62
+ font-size: 12px;
63
+ font-weight: 600;
64
+ transition: opacity 0.15s;
65
+ }
66
+
67
+ .send-btn:hover {
68
+ opacity: 0.85;
69
+ }
70
+
71
+ .history-list {
72
+ flex: 1;
73
+ min-height: 0;
74
+ overflow-y: auto;
75
+ padding: 4px 8px;
76
+ scrollbar-width: thin;
77
+ }
78
+
79
+ .empty-state {
80
+ padding: 16px;
81
+ text-align: center;
82
+ color: var(--muted-foreground, #888);
83
+ font-size: 12px;
84
+ }
85
+ `
86
+
87
+ declare _inputValue: string
88
+
89
+ constructor() {
90
+ super()
91
+ this._inputValue = ''
92
+ }
93
+
94
+ private _onInput(e: Event) {
95
+ this._inputValue = (e.target as HTMLTextAreaElement).value
96
+ }
97
+
98
+ private _onKeyDown(e: KeyboardEvent) {
99
+ // Ctrl/Cmd+Enter to send
100
+ if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
101
+ e.preventDefault()
102
+ this._send()
103
+ }
104
+ }
105
+
106
+ private _send() {
107
+ const text = this._inputValue.trim()
108
+ if (!text) return
109
+
110
+ // Dispatch to terminal
111
+ this.dispatchEvent(
112
+ new CustomEvent('input-panel:send', {
113
+ detail: { data: text + '\n' },
114
+ bubbles: true,
115
+ composed: true,
116
+ })
117
+ )
118
+
119
+ this._inputValue = ''
120
+ }
121
+
122
+ render() {
123
+ return html`
124
+ <div class="input-area">
125
+ <textarea
126
+ placeholder="Type command and press Ctrl+Enter to send..."
127
+ .value=${this._inputValue}
128
+ @input=${this._onInput}
129
+ @keydown=${this._onKeyDown}
130
+ ></textarea>
131
+ <button class="send-btn" @click=${this._send}>${iconSend(14)} Send</button>
132
+ </div>
133
+ <div class="history-list">
134
+ <slot name="history">
135
+ <div class="empty-state">No history</div>
136
+ </slot>
137
+ </div>
138
+ `
139
+ }
140
+ }
141
+
142
+ customElements.define('input-method-tab', InputMethodTab)
@@ -0,0 +1,73 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite'
2
+ import { html } from 'lit'
3
+ import { expect, fn } from 'storybook/test'
4
+
5
+ import './input-panel-settings.js'
6
+
7
+ const meta: Meta = {
8
+ title: 'InputPanelSettings',
9
+ tags: ['autodocs'],
10
+ decorators: [
11
+ (story) => html`
12
+ <div
13
+ style="width: 300px; height: 250px; background: #1a1a1a; color: #fff; font-family: monospace; position: relative;"
14
+ >
15
+ ${story()}
16
+ </div>
17
+ `,
18
+ ],
19
+ }
20
+
21
+ export default meta
22
+
23
+ /**
24
+ * Settings panel with height sliders and vibration intensity.
25
+ */
26
+ export const Default: StoryObj = {
27
+ render: () => html`
28
+ <input-panel-settings
29
+ visible
30
+ fixed-height="250"
31
+ floating-height="200"
32
+ vibration-intensity="50"
33
+ ></input-panel-settings>
34
+ `,
35
+ }
36
+
37
+ /**
38
+ * Verifies that changing a slider dispatches `input-panel:settings-change`.
39
+ */
40
+ export const SliderChange: StoryObj = {
41
+ render: () => html`
42
+ <input-panel-settings
43
+ visible
44
+ fixed-height="250"
45
+ floating-height="200"
46
+ vibration-intensity="50"
47
+ ></input-panel-settings>
48
+ `,
49
+ play: async ({ canvasElement }) => {
50
+ const el = canvasElement.querySelector('input-panel-settings') as HTMLElement & {
51
+ updateComplete: Promise<boolean>
52
+ }
53
+ await el.updateComplete
54
+
55
+ const settingsHandler = fn()
56
+ el.addEventListener('input-panel:settings-change', settingsHandler)
57
+
58
+ const shadow = el.shadowRoot!
59
+ const ranges = shadow.querySelectorAll('input[type="range"]')
60
+ expect(ranges.length).toBe(5)
61
+
62
+ // Change the first slider (fixed height)
63
+ const fixedSlider = ranges[0] as HTMLInputElement
64
+ fixedSlider.value = '300'
65
+ fixedSlider.dispatchEvent(new Event('input', { bubbles: true }))
66
+
67
+ expect(settingsHandler).toHaveBeenCalledTimes(1)
68
+ const detail = (settingsHandler.mock.calls[0] as unknown[])[0] as CustomEvent
69
+ expect(detail.detail.fixedHeight).toBe(300)
70
+
71
+ el.removeEventListener('input-panel:settings-change', settingsHandler)
72
+ },
73
+ }