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.
- package/.storybook/main.ts +11 -0
- package/.storybook/preview.ts +15 -0
- package/.storybook/vitest.setup.ts +8 -0
- package/CHANGELOG.md +7 -0
- package/package.json +37 -0
- package/src/brand-icons/claude.png +0 -0
- package/src/brand-icons/codex.png +0 -0
- package/src/brand-icons/gemini.png +0 -0
- package/src/icons.ts +77 -0
- package/src/index.ts +16 -0
- package/src/input-method-tab.stories.ts +131 -0
- package/src/input-method-tab.ts +142 -0
- package/src/input-panel-settings.stories.ts +73 -0
- package/src/input-panel-settings.ts +245 -0
- package/src/input-panel.stories.ts +241 -0
- package/src/input-panel.ts +815 -0
- package/src/pixi-theme.test.ts +58 -0
- package/src/pixi-theme.ts +179 -0
- package/src/platform.ts +14 -0
- package/src/shortcut-pages.ts +204 -0
- package/src/shortcut-tab.ts +543 -0
- package/src/virtual-keyboard-layouts.ts +150 -0
- package/src/virtual-keyboard-tab.stories.ts +390 -0
- package/src/virtual-keyboard-tab.ts +642 -0
- package/src/virtual-trackpad-tab.stories.ts +476 -0
- package/src/virtual-trackpad-tab.ts +556 -0
- package/src/xterm-addon.stories.ts +413 -0
- package/src/xterm-addon.ts +947 -0
- package/tsconfig.json +8 -0
- package/vite.config.ts +13 -0
- package/vitest.storybook.config.ts +23 -0
|
@@ -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
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
|
+
}
|