@furystack/shades-common-components 13.1.0 → 13.3.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/CHANGELOG.md +58 -0
- package/esm/components/app-bar.d.ts.map +1 -1
- package/esm/components/app-bar.js +12 -3
- package/esm/components/app-bar.js.map +1 -1
- package/esm/components/avatar.d.ts.map +1 -1
- package/esm/components/avatar.js +3 -5
- package/esm/components/avatar.js.map +1 -1
- package/esm/components/command-palette/command-palette-input.d.ts +1 -2
- package/esm/components/command-palette/command-palette-input.d.ts.map +1 -1
- package/esm/components/command-palette/command-palette-input.js +14 -36
- package/esm/components/command-palette/command-palette-input.js.map +1 -1
- package/esm/components/command-palette/command-palette-input.spec.js +14 -116
- package/esm/components/command-palette/command-palette-input.spec.js.map +1 -1
- package/esm/components/command-palette/index.d.ts.map +1 -1
- package/esm/components/command-palette/index.js +3 -0
- package/esm/components/command-palette/index.js.map +1 -1
- package/esm/components/drawer/index.d.ts.map +1 -1
- package/esm/components/drawer/index.js +4 -0
- package/esm/components/drawer/index.js.map +1 -1
- package/esm/components/drawer/index.spec.js +47 -0
- package/esm/components/drawer/index.spec.js.map +1 -1
- package/esm/components/noty-list.d.ts.map +1 -1
- package/esm/components/noty-list.js +1 -3
- package/esm/components/noty-list.js.map +1 -1
- package/esm/services/collection-service.d.ts +8 -4
- package/esm/services/collection-service.d.ts.map +1 -1
- package/esm/services/collection-service.js +12 -4
- package/esm/services/collection-service.js.map +1 -1
- package/esm/services/collection-service.spec.js +37 -0
- package/esm/services/collection-service.spec.js.map +1 -1
- package/esm/services/css-variable-theme.d.ts +1 -1
- package/esm/services/css-variable-theme.d.ts.map +1 -1
- package/esm/services/css-variable-theme.js +5 -5
- package/esm/services/css-variable-theme.js.map +1 -1
- package/esm/services/css-variable-theme.spec.js +29 -1
- package/esm/services/css-variable-theme.spec.js.map +1 -1
- package/esm/services/layout-service.d.ts +8 -0
- package/esm/services/layout-service.d.ts.map +1 -1
- package/esm/services/layout-service.js +16 -0
- package/esm/services/layout-service.js.map +1 -1
- package/esm/services/layout-service.spec.js +55 -0
- package/esm/services/layout-service.spec.js.map +1 -1
- package/esm/services/theme-provider-service.d.ts +11 -10
- package/esm/services/theme-provider-service.d.ts.map +1 -1
- package/esm/services/theme-provider-service.js +3 -2
- package/esm/services/theme-provider-service.js.map +1 -1
- package/esm/services/theme-provider-service.spec.js +35 -1
- package/esm/services/theme-provider-service.spec.js.map +1 -1
- package/esm/themes/architect-theme.d.ts +1 -0
- package/esm/themes/architect-theme.d.ts.map +1 -1
- package/esm/themes/architect-theme.js +1 -0
- package/esm/themes/architect-theme.js.map +1 -1
- package/esm/themes/auditore-theme.d.ts +1 -0
- package/esm/themes/auditore-theme.d.ts.map +1 -1
- package/esm/themes/auditore-theme.js +1 -0
- package/esm/themes/auditore-theme.js.map +1 -1
- package/esm/themes/black-mesa-theme.d.ts +1 -0
- package/esm/themes/black-mesa-theme.d.ts.map +1 -1
- package/esm/themes/black-mesa-theme.js +1 -0
- package/esm/themes/black-mesa-theme.js.map +1 -1
- package/esm/themes/default-dark-theme.d.ts +1 -0
- package/esm/themes/default-dark-theme.d.ts.map +1 -1
- package/esm/themes/default-dark-theme.js +1 -0
- package/esm/themes/default-dark-theme.js.map +1 -1
- package/esm/themes/default-light-theme.d.ts +1 -0
- package/esm/themes/default-light-theme.d.ts.map +1 -1
- package/esm/themes/default-light-theme.js +1 -0
- package/esm/themes/default-light-theme.js.map +1 -1
- package/esm/themes/dragonborn-theme.d.ts +1 -0
- package/esm/themes/dragonborn-theme.d.ts.map +1 -1
- package/esm/themes/dragonborn-theme.js +1 -0
- package/esm/themes/dragonborn-theme.js.map +1 -1
- package/esm/themes/hawkins-theme.d.ts +1 -0
- package/esm/themes/hawkins-theme.d.ts.map +1 -1
- package/esm/themes/hawkins-theme.js +1 -0
- package/esm/themes/hawkins-theme.js.map +1 -1
- package/esm/themes/jedi-theme.d.ts +1 -0
- package/esm/themes/jedi-theme.d.ts.map +1 -1
- package/esm/themes/jedi-theme.js +1 -0
- package/esm/themes/jedi-theme.js.map +1 -1
- package/esm/themes/neon-runner-theme.d.ts +1 -0
- package/esm/themes/neon-runner-theme.d.ts.map +1 -1
- package/esm/themes/neon-runner-theme.js +1 -0
- package/esm/themes/neon-runner-theme.js.map +1 -1
- package/esm/themes/plumber-theme.d.ts +1 -0
- package/esm/themes/plumber-theme.d.ts.map +1 -1
- package/esm/themes/plumber-theme.js +1 -0
- package/esm/themes/plumber-theme.js.map +1 -1
- package/esm/themes/replicant-theme.d.ts +1 -0
- package/esm/themes/replicant-theme.d.ts.map +1 -1
- package/esm/themes/replicant-theme.js +1 -0
- package/esm/themes/replicant-theme.js.map +1 -1
- package/esm/themes/sandworm-theme.d.ts +1 -0
- package/esm/themes/sandworm-theme.d.ts.map +1 -1
- package/esm/themes/sandworm-theme.js +1 -0
- package/esm/themes/sandworm-theme.js.map +1 -1
- package/esm/themes/shadow-broker-theme.d.ts +1 -0
- package/esm/themes/shadow-broker-theme.d.ts.map +1 -1
- package/esm/themes/shadow-broker-theme.js +1 -0
- package/esm/themes/shadow-broker-theme.js.map +1 -1
- package/esm/themes/sith-theme.d.ts +1 -0
- package/esm/themes/sith-theme.d.ts.map +1 -1
- package/esm/themes/sith-theme.js +1 -0
- package/esm/themes/sith-theme.js.map +1 -1
- package/esm/themes/vault-dweller-theme.d.ts +1 -0
- package/esm/themes/vault-dweller-theme.d.ts.map +1 -1
- package/esm/themes/vault-dweller-theme.js +1 -0
- package/esm/themes/vault-dweller-theme.js.map +1 -1
- package/esm/themes/wild-hunt-theme.d.ts +1 -0
- package/esm/themes/wild-hunt-theme.d.ts.map +1 -1
- package/esm/themes/wild-hunt-theme.js +1 -0
- package/esm/themes/wild-hunt-theme.js.map +1 -1
- package/esm/themes/xenomorph-theme.d.ts +1 -0
- package/esm/themes/xenomorph-theme.d.ts.map +1 -1
- package/esm/themes/xenomorph-theme.js +1 -0
- package/esm/themes/xenomorph-theme.js.map +1 -1
- package/package.json +6 -6
- package/src/components/app-bar.tsx +12 -3
- package/src/components/avatar.tsx +20 -5
- package/src/components/command-palette/command-palette-input.spec.tsx +14 -156
- package/src/components/command-palette/command-palette-input.tsx +13 -45
- package/src/components/command-palette/index.tsx +4 -0
- package/src/components/drawer/index.spec.tsx +64 -0
- package/src/components/drawer/index.tsx +5 -0
- package/src/components/noty-list.tsx +1 -3
- package/src/services/collection-service.spec.ts +46 -0
- package/src/services/collection-service.ts +23 -8
- package/src/services/css-variable-theme.spec.ts +43 -1
- package/src/services/css-variable-theme.ts +5 -5
- package/src/services/layout-service.spec.ts +74 -0
- package/src/services/layout-service.ts +18 -0
- package/src/services/theme-provider-service.spec.ts +49 -1
- package/src/services/theme-provider-service.ts +12 -11
- package/src/themes/architect-theme.ts +1 -0
- package/src/themes/auditore-theme.ts +1 -0
- package/src/themes/black-mesa-theme.ts +1 -0
- package/src/themes/default-dark-theme.ts +1 -0
- package/src/themes/default-light-theme.ts +1 -0
- package/src/themes/dragonborn-theme.ts +1 -0
- package/src/themes/hawkins-theme.ts +1 -0
- package/src/themes/jedi-theme.ts +1 -0
- package/src/themes/neon-runner-theme.ts +1 -0
- package/src/themes/plumber-theme.ts +1 -0
- package/src/themes/replicant-theme.ts +1 -0
- package/src/themes/sandworm-theme.ts +1 -0
- package/src/themes/shadow-broker-theme.ts +1 -0
- package/src/themes/sith-theme.ts +1 -0
- package/src/themes/vault-dweller-theme.ts +1 -0
- package/src/themes/wild-hunt-theme.ts +1 -0
- package/src/themes/xenomorph-theme.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@furystack/shades-common-components",
|
|
3
|
-
"version": "13.
|
|
3
|
+
"version": "13.3.0",
|
|
4
4
|
"description": "Common UI components for FuryStack Shades",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"vitest": "^4.0.18"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@furystack/cache": "^6.
|
|
52
|
-
"@furystack/core": "^15.2.
|
|
53
|
-
"@furystack/inject": "^12.0.
|
|
54
|
-
"@furystack/shades": "^12.2.
|
|
55
|
-
"@furystack/utils": "^8.
|
|
51
|
+
"@furystack/cache": "^6.1.0",
|
|
52
|
+
"@furystack/core": "^15.2.3",
|
|
53
|
+
"@furystack/inject": "^12.0.31",
|
|
54
|
+
"@furystack/shades": "^12.2.5",
|
|
55
|
+
"@furystack/utils": "^8.2.0",
|
|
56
56
|
"path-to-regexp": "^8.3.0"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
@@ -5,9 +5,6 @@ export const AppBar = Shade({
|
|
|
5
5
|
shadowDomName: 'shade-app-bar',
|
|
6
6
|
css: {
|
|
7
7
|
width: '100%',
|
|
8
|
-
background: `color-mix(in srgb, ${cssVariableTheme.background.paper} 85%, transparent)`,
|
|
9
|
-
backgroundImage: cssVariableTheme.background.paperImage,
|
|
10
|
-
backdropFilter: `blur(${cssVariableTheme.effects.blurLg})`,
|
|
11
8
|
display: 'flex',
|
|
12
9
|
justifyContent: 'flex-start',
|
|
13
10
|
alignItems: 'center',
|
|
@@ -18,6 +15,18 @@ export const AppBar = Shade({
|
|
|
18
15
|
zIndex: '1',
|
|
19
16
|
fontFamily: cssVariableTheme.typography.fontFamily,
|
|
20
17
|
color: cssVariableTheme.text.primary,
|
|
18
|
+
// backdrop-filter on the host would create a containing block for position:fixed
|
|
19
|
+
// descendants (per CSS spec), breaking Dropdown overlays inside the AppBar.
|
|
20
|
+
// Using a pseudo-element avoids this while preserving the visual effect.
|
|
21
|
+
'&::before': {
|
|
22
|
+
content: '""',
|
|
23
|
+
position: 'absolute',
|
|
24
|
+
inset: '0',
|
|
25
|
+
zIndex: '-1',
|
|
26
|
+
background: `color-mix(in srgb, ${cssVariableTheme.background.paper} 85%, transparent)`,
|
|
27
|
+
backgroundImage: cssVariableTheme.background.paperImage,
|
|
28
|
+
backdropFilter: `blur(${cssVariableTheme.effects.blurLg})`,
|
|
29
|
+
},
|
|
21
30
|
'&[data-visible]': {
|
|
22
31
|
opacity: '1',
|
|
23
32
|
},
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { PartialElement } from '@furystack/shades'
|
|
2
2
|
import { Shade, createComponent } from '@furystack/shades'
|
|
3
3
|
import { cssVariableTheme } from '../services/css-variable-theme.js'
|
|
4
|
-
import { Icon } from './icons/icon.js'
|
|
5
4
|
import { user as userIcon } from './icons/icon-definitions.js'
|
|
6
5
|
|
|
7
6
|
export type AvatarProps = { avatarUrl: string; fallback?: JSX.Element } & PartialElement<HTMLDivElement>
|
|
@@ -37,14 +36,13 @@ export const Avatar = Shade<AvatarProps>({
|
|
|
37
36
|
display: 'flex',
|
|
38
37
|
alignItems: 'center',
|
|
39
38
|
justifyContent: 'center',
|
|
40
|
-
width: '
|
|
41
|
-
height: '
|
|
39
|
+
width: '100%',
|
|
40
|
+
height: '100%',
|
|
42
41
|
borderRadius: cssVariableTheme.shape.borderRadius.full,
|
|
43
42
|
background: `color-mix(in srgb, ${cssVariableTheme.palette.primary.main} 20%, transparent)`,
|
|
44
43
|
backdropFilter: 'blur(10px)',
|
|
45
44
|
textAlign: 'center',
|
|
46
45
|
userSelect: 'none',
|
|
47
|
-
fontSize: '48px',
|
|
48
46
|
lineHeight: '1',
|
|
49
47
|
},
|
|
50
48
|
},
|
|
@@ -59,7 +57,24 @@ export const Avatar = Shade<AvatarProps>({
|
|
|
59
57
|
if (hasError) {
|
|
60
58
|
return (
|
|
61
59
|
<div className="avatar-fallback-container">
|
|
62
|
-
<div className="avatar-fallback-icon">
|
|
60
|
+
<div className="avatar-fallback-icon">
|
|
61
|
+
{props.fallback || (
|
|
62
|
+
<svg
|
|
63
|
+
width="100%"
|
|
64
|
+
height="100%"
|
|
65
|
+
viewBox={userIcon.viewBox ?? '0 0 24 24'}
|
|
66
|
+
fill="none"
|
|
67
|
+
stroke="currentColor"
|
|
68
|
+
stroke-width="2"
|
|
69
|
+
stroke-linecap="round"
|
|
70
|
+
stroke-linejoin="round"
|
|
71
|
+
>
|
|
72
|
+
{userIcon.paths.map((p) => (
|
|
73
|
+
<path d={p.d} />
|
|
74
|
+
))}
|
|
75
|
+
</svg>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
63
78
|
</div>
|
|
64
79
|
)
|
|
65
80
|
}
|
|
@@ -6,42 +6,14 @@ import { CommandPaletteInput } from './command-palette-input.js'
|
|
|
6
6
|
import { CommandPaletteManager } from './command-palette-manager.js'
|
|
7
7
|
|
|
8
8
|
describe('CommandPaletteInput', () => {
|
|
9
|
-
let originalAnimate: typeof Element.prototype.animate
|
|
10
|
-
let animateCalls: Array<{ keyframes: unknown; options: unknown }>
|
|
11
|
-
|
|
12
9
|
beforeEach(() => {
|
|
13
10
|
vi.useFakeTimers()
|
|
14
11
|
document.body.innerHTML = '<div id="root"></div>'
|
|
15
|
-
animateCalls = []
|
|
16
|
-
originalAnimate = Element.prototype.animate
|
|
17
|
-
|
|
18
|
-
Element.prototype.animate = vi.fn(
|
|
19
|
-
(keyframes: Keyframe[] | PropertyIndexedKeyframes | null, options?: number | KeyframeAnimationOptions) => {
|
|
20
|
-
animateCalls.push({ keyframes, options })
|
|
21
|
-
const mockAnimation = {
|
|
22
|
-
onfinish: null as ((event: AnimationPlaybackEvent) => void) | null,
|
|
23
|
-
oncancel: null as ((event: AnimationPlaybackEvent) => void) | null,
|
|
24
|
-
cancel: vi.fn(),
|
|
25
|
-
play: vi.fn(),
|
|
26
|
-
pause: vi.fn(),
|
|
27
|
-
finish: vi.fn(),
|
|
28
|
-
addEventListener: vi.fn(),
|
|
29
|
-
removeEventListener: vi.fn(),
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
setTimeout(() => {
|
|
33
|
-
mockAnimation.onfinish?.({} as AnimationPlaybackEvent)
|
|
34
|
-
}, 10)
|
|
35
|
-
|
|
36
|
-
return mockAnimation as unknown as Animation
|
|
37
|
-
},
|
|
38
|
-
) as typeof Element.prototype.animate
|
|
39
12
|
})
|
|
40
13
|
|
|
41
14
|
afterEach(() => {
|
|
42
15
|
vi.useRealTimers()
|
|
43
16
|
document.body.innerHTML = ''
|
|
44
|
-
Element.prototype.animate = originalAnimate
|
|
45
17
|
vi.restoreAllMocks()
|
|
46
18
|
})
|
|
47
19
|
|
|
@@ -89,7 +61,7 @@ describe('CommandPaletteInput', () => {
|
|
|
89
61
|
})
|
|
90
62
|
})
|
|
91
63
|
|
|
92
|
-
it('should
|
|
64
|
+
it('should always have width 100%', async () => {
|
|
93
65
|
await usingAsync(new Injector(), async (injector) => {
|
|
94
66
|
await usingAsync(createManager(), async (manager) => {
|
|
95
67
|
manager.isOpened.setValue(false)
|
|
@@ -104,15 +76,15 @@ describe('CommandPaletteInput', () => {
|
|
|
104
76
|
await flushUpdates()
|
|
105
77
|
|
|
106
78
|
const component = document.querySelector('shades-command-palette-input') as HTMLElement
|
|
107
|
-
|
|
79
|
+
const computedStyle = window.getComputedStyle(component)
|
|
80
|
+
expect(computedStyle.width).toBe('100%')
|
|
108
81
|
})
|
|
109
82
|
})
|
|
110
83
|
})
|
|
111
84
|
|
|
112
|
-
it('should have
|
|
85
|
+
it('should have overflow hidden style', async () => {
|
|
113
86
|
await usingAsync(new Injector(), async (injector) => {
|
|
114
87
|
await usingAsync(createManager(), async (manager) => {
|
|
115
|
-
manager.isOpened.setValue(true)
|
|
116
88
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
117
89
|
|
|
118
90
|
initializeShadeRoot({
|
|
@@ -124,73 +96,13 @@ describe('CommandPaletteInput', () => {
|
|
|
124
96
|
await flushUpdates()
|
|
125
97
|
|
|
126
98
|
const component = document.querySelector('shades-command-palette-input') as HTMLElement
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
})
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('should animate width when opening', async () => {
|
|
133
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
134
|
-
await usingAsync(createManager(), async (manager) => {
|
|
135
|
-
manager.isOpened.setValue(false)
|
|
136
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
137
|
-
|
|
138
|
-
initializeShadeRoot({
|
|
139
|
-
injector,
|
|
140
|
-
rootElement,
|
|
141
|
-
jsxElement: <CommandPaletteInput manager={manager} />,
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
await flushUpdates()
|
|
145
|
-
animateCalls = []
|
|
146
|
-
|
|
147
|
-
manager.isOpened.setValue(true)
|
|
148
|
-
await flushUpdates()
|
|
149
|
-
|
|
150
|
-
const widthAnimation = animateCalls.find(
|
|
151
|
-
(call) =>
|
|
152
|
-
Array.isArray(call.keyframes) &&
|
|
153
|
-
call.keyframes.some((kf: Keyframe) => kf.width === '0%') &&
|
|
154
|
-
call.keyframes.some((kf: Keyframe) => kf.width === '100%'),
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
expect(widthAnimation).toBeDefined()
|
|
158
|
-
expect((widthAnimation?.options as KeyframeAnimationOptions)?.duration).toBe(300)
|
|
159
|
-
})
|
|
160
|
-
})
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('should animate width when closing', async () => {
|
|
164
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
165
|
-
await usingAsync(createManager(), async (manager) => {
|
|
166
|
-
manager.isOpened.setValue(true)
|
|
167
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
168
|
-
|
|
169
|
-
initializeShadeRoot({
|
|
170
|
-
injector,
|
|
171
|
-
rootElement,
|
|
172
|
-
jsxElement: <CommandPaletteInput manager={manager} />,
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
await flushUpdates()
|
|
176
|
-
animateCalls = []
|
|
177
|
-
|
|
178
|
-
manager.isOpened.setValue(false)
|
|
179
|
-
await flushUpdates()
|
|
180
|
-
|
|
181
|
-
const widthAnimation = animateCalls.find(
|
|
182
|
-
(call) =>
|
|
183
|
-
Array.isArray(call.keyframes) &&
|
|
184
|
-
call.keyframes.some((kf: Keyframe) => kf.width === '100%') &&
|
|
185
|
-
call.keyframes.some((kf: Keyframe) => kf.width === '0%'),
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
expect(widthAnimation).toBeDefined()
|
|
99
|
+
const computedStyle = window.getComputedStyle(component)
|
|
100
|
+
expect(computedStyle.overflow).toBe('hidden')
|
|
189
101
|
})
|
|
190
102
|
})
|
|
191
103
|
})
|
|
192
104
|
|
|
193
|
-
it('should
|
|
105
|
+
it('should focus input when opened', async () => {
|
|
194
106
|
await usingAsync(new Injector(), async (injector) => {
|
|
195
107
|
await usingAsync(createManager(), async (manager) => {
|
|
196
108
|
manager.isOpened.setValue(false)
|
|
@@ -206,12 +118,12 @@ describe('CommandPaletteInput', () => {
|
|
|
206
118
|
|
|
207
119
|
const component = document.querySelector('shades-command-palette-input') as HTMLElement
|
|
208
120
|
const inputElement = component?.querySelector('input') as HTMLInputElement
|
|
209
|
-
|
|
121
|
+
const focusSpy = vi.spyOn(inputElement, 'focus')
|
|
210
122
|
|
|
211
123
|
manager.isOpened.setValue(true)
|
|
212
124
|
await flushUpdates()
|
|
213
125
|
|
|
214
|
-
expect(
|
|
126
|
+
expect(focusSpy).toHaveBeenCalled()
|
|
215
127
|
})
|
|
216
128
|
})
|
|
217
129
|
})
|
|
@@ -236,35 +148,13 @@ describe('CommandPaletteInput', () => {
|
|
|
236
148
|
|
|
237
149
|
manager.isOpened.setValue(false)
|
|
238
150
|
await flushUpdates()
|
|
239
|
-
await vi.advanceTimersByTimeAsync(20)
|
|
240
|
-
await flushUpdates()
|
|
241
151
|
|
|
242
152
|
expect(inputElement.value).toBe('')
|
|
243
153
|
})
|
|
244
154
|
})
|
|
245
155
|
})
|
|
246
156
|
|
|
247
|
-
it('should
|
|
248
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
249
|
-
await usingAsync(createManager(), async (manager) => {
|
|
250
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
251
|
-
|
|
252
|
-
initializeShadeRoot({
|
|
253
|
-
injector,
|
|
254
|
-
rootElement,
|
|
255
|
-
jsxElement: <CommandPaletteInput manager={manager} />,
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
await flushUpdates()
|
|
259
|
-
|
|
260
|
-
const component = document.querySelector('shades-command-palette-input') as HTMLElement
|
|
261
|
-
const computedStyle = window.getComputedStyle(component)
|
|
262
|
-
expect(computedStyle.overflow).toBe('hidden')
|
|
263
|
-
})
|
|
264
|
-
})
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
it('should use cubic-bezier easing for animations', async () => {
|
|
157
|
+
it('should preserve input value when opening', async () => {
|
|
268
158
|
await usingAsync(new Injector(), async (injector) => {
|
|
269
159
|
await usingAsync(createManager(), async (manager) => {
|
|
270
160
|
manager.isOpened.setValue(false)
|
|
@@ -277,47 +167,15 @@ describe('CommandPaletteInput', () => {
|
|
|
277
167
|
})
|
|
278
168
|
|
|
279
169
|
await flushUpdates()
|
|
280
|
-
animateCalls = []
|
|
281
170
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
const widthAnimation = animateCalls.find(
|
|
286
|
-
(call) => Array.isArray(call.keyframes) && call.keyframes.some((kf: Keyframe) => 'width' in kf),
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
expect(widthAnimation).toBeDefined()
|
|
290
|
-
expect((widthAnimation?.options as KeyframeAnimationOptions)?.easing).toBe(
|
|
291
|
-
'cubic-bezier(0.595, 0.425, 0.415, 0.845)',
|
|
292
|
-
)
|
|
293
|
-
})
|
|
294
|
-
})
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
it('should fill animation forwards', async () => {
|
|
298
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
299
|
-
await usingAsync(createManager(), async (manager) => {
|
|
300
|
-
manager.isOpened.setValue(false)
|
|
301
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
302
|
-
|
|
303
|
-
initializeShadeRoot({
|
|
304
|
-
injector,
|
|
305
|
-
rootElement,
|
|
306
|
-
jsxElement: <CommandPaletteInput manager={manager} />,
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
await flushUpdates()
|
|
310
|
-
animateCalls = []
|
|
171
|
+
const component = document.querySelector('shades-command-palette-input') as HTMLElement
|
|
172
|
+
const inputElement = component?.querySelector('input') as HTMLInputElement
|
|
173
|
+
inputElement.value = 'some text'
|
|
311
174
|
|
|
312
175
|
manager.isOpened.setValue(true)
|
|
313
176
|
await flushUpdates()
|
|
314
177
|
|
|
315
|
-
|
|
316
|
-
(call) => Array.isArray(call.keyframes) && call.keyframes.some((kf: Keyframe) => 'width' in kf),
|
|
317
|
-
)
|
|
318
|
-
|
|
319
|
-
expect(widthAnimation).toBeDefined()
|
|
320
|
-
expect((widthAnimation?.options as KeyframeAnimationOptions)?.fill).toBe('forwards')
|
|
178
|
+
expect(inputElement.value).toBe('some text')
|
|
321
179
|
})
|
|
322
180
|
})
|
|
323
181
|
})
|
|
@@ -1,45 +1,13 @@
|
|
|
1
|
-
import type { RefObject } from '@furystack/shades'
|
|
2
1
|
import { Shade, createComponent } from '@furystack/shades'
|
|
3
2
|
import { cssVariableTheme } from '../../services/css-variable-theme.js'
|
|
4
|
-
import { promisifyAnimation } from '../../utils/promisify-animation.js'
|
|
5
3
|
import type { CommandPaletteManager } from './command-palette-manager.js'
|
|
6
4
|
|
|
7
|
-
const animateOpenState = async (
|
|
8
|
-
wrapperRef: RefObject<HTMLDivElement>,
|
|
9
|
-
inputRef: RefObject<HTMLInputElement>,
|
|
10
|
-
isOpened: boolean,
|
|
11
|
-
) => {
|
|
12
|
-
const wrapper = wrapperRef.current
|
|
13
|
-
const input = inputRef.current
|
|
14
|
-
if (wrapper && input) {
|
|
15
|
-
if (isOpened) {
|
|
16
|
-
input.value = ''
|
|
17
|
-
await promisifyAnimation(wrapper, [{ width: '0%' }, { width: '100%' }], {
|
|
18
|
-
duration: 300,
|
|
19
|
-
fill: 'forwards',
|
|
20
|
-
easing: 'cubic-bezier(0.595, 0.425, 0.415, 0.845)',
|
|
21
|
-
})
|
|
22
|
-
input.focus()
|
|
23
|
-
} else {
|
|
24
|
-
await promisifyAnimation(wrapper, [{ width: '100%' }, { width: '0%' }], {
|
|
25
|
-
duration: 300,
|
|
26
|
-
fill: 'forwards',
|
|
27
|
-
easing: 'cubic-bezier(0.595, 0.425, 0.415, 0.845)',
|
|
28
|
-
})
|
|
29
|
-
input.value = ''
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
5
|
export const CommandPaletteInput = Shade<{ manager: CommandPaletteManager }>({
|
|
35
6
|
shadowDomName: 'shades-command-palette-input',
|
|
36
7
|
css: {
|
|
37
|
-
width: '
|
|
8
|
+
width: '100%',
|
|
38
9
|
fontFamily: cssVariableTheme.typography.fontFamily,
|
|
39
10
|
overflow: 'hidden',
|
|
40
|
-
'&[data-opened]': {
|
|
41
|
-
width: '100%',
|
|
42
|
-
},
|
|
43
11
|
'& input': {
|
|
44
12
|
color: cssVariableTheme.text.primary,
|
|
45
13
|
outline: 'none',
|
|
@@ -53,20 +21,20 @@ export const CommandPaletteInput = Shade<{ manager: CommandPaletteManager }>({
|
|
|
53
21
|
letterSpacing: '0.01em',
|
|
54
22
|
},
|
|
55
23
|
},
|
|
56
|
-
render: ({ props, useObservable, useRef
|
|
57
|
-
const { manager } = props
|
|
58
|
-
const wrapperRef = useRef<HTMLDivElement>('wrapper')
|
|
24
|
+
render: ({ props, useObservable, useRef }) => {
|
|
59
25
|
const inputRef = useRef<HTMLInputElement>('input')
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
26
|
+
useObservable('isOpened', props.manager.isOpened, {
|
|
27
|
+
onChange: (isOpened) => {
|
|
28
|
+
if (inputRef.current) {
|
|
29
|
+
if (isOpened) {
|
|
30
|
+
inputRef.current.focus()
|
|
31
|
+
} else {
|
|
32
|
+
inputRef.current.value = ''
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
63
36
|
})
|
|
64
|
-
useHostProps({ ...(isCurrentlyOpened ? { 'data-opened': '' } : {}) })
|
|
65
37
|
|
|
66
|
-
return
|
|
67
|
-
<div ref={wrapperRef} style={{ width: isCurrentlyOpened ? '100%' : '0%', overflow: 'hidden' }}>
|
|
68
|
-
<input ref={inputRef} autofocus placeholder="Type to search commands..." />
|
|
69
|
-
</div>
|
|
70
|
-
)
|
|
38
|
+
return <input ref={inputRef} autofocus placeholder="Type to search commands..." />
|
|
71
39
|
},
|
|
72
40
|
})
|
|
@@ -718,6 +718,70 @@ describe('Drawer component', () => {
|
|
|
718
718
|
})
|
|
719
719
|
})
|
|
720
720
|
|
|
721
|
+
describe('cleanup on disposal', () => {
|
|
722
|
+
it('should call removeDrawer on LayoutService when the component is removed from DOM', async () => {
|
|
723
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
724
|
+
const layoutService = new LayoutService(createMockElement())
|
|
725
|
+
injector.setExplicitInstance(layoutService, LayoutService)
|
|
726
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
727
|
+
|
|
728
|
+
initializeShadeRoot({
|
|
729
|
+
injector,
|
|
730
|
+
rootElement,
|
|
731
|
+
jsxElement: (
|
|
732
|
+
<Drawer position="left" variant="collapsible">
|
|
733
|
+
<div>Drawer</div>
|
|
734
|
+
</Drawer>
|
|
735
|
+
),
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
await flushUpdates()
|
|
739
|
+
expect(layoutService.drawerState.getValue().left).toBeDefined()
|
|
740
|
+
|
|
741
|
+
const removeDrawerSpy = vi.spyOn(layoutService, 'removeDrawer')
|
|
742
|
+
const drawer = document.querySelector('shade-drawer') as HTMLElement
|
|
743
|
+
drawer.remove()
|
|
744
|
+
await flushUpdates()
|
|
745
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
746
|
+
|
|
747
|
+
expect(removeDrawerSpy).toHaveBeenCalledWith('left')
|
|
748
|
+
})
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
it('should only clean up its own drawer position on disposal', async () => {
|
|
752
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
753
|
+
const layoutService = new LayoutService(createMockElement())
|
|
754
|
+
injector.setExplicitInstance(layoutService, LayoutService)
|
|
755
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
756
|
+
|
|
757
|
+
layoutService.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' })
|
|
758
|
+
|
|
759
|
+
initializeShadeRoot({
|
|
760
|
+
injector,
|
|
761
|
+
rootElement,
|
|
762
|
+
jsxElement: (
|
|
763
|
+
<Drawer position="right" variant="temporary">
|
|
764
|
+
<div>Right Drawer</div>
|
|
765
|
+
</Drawer>
|
|
766
|
+
),
|
|
767
|
+
})
|
|
768
|
+
|
|
769
|
+
await flushUpdates()
|
|
770
|
+
expect(layoutService.drawerState.getValue().right).toBeDefined()
|
|
771
|
+
expect(layoutService.drawerState.getValue().left).toBeDefined()
|
|
772
|
+
|
|
773
|
+
const removeDrawerSpy = vi.spyOn(layoutService, 'removeDrawer')
|
|
774
|
+
const drawer = document.querySelector('shade-drawer') as HTMLElement
|
|
775
|
+
drawer.remove()
|
|
776
|
+
await flushUpdates()
|
|
777
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
778
|
+
|
|
779
|
+
expect(removeDrawerSpy).toHaveBeenCalledWith('right')
|
|
780
|
+
expect(removeDrawerSpy).not.toHaveBeenCalledWith('left')
|
|
781
|
+
})
|
|
782
|
+
})
|
|
783
|
+
})
|
|
784
|
+
|
|
721
785
|
describe('preserving user interactions', () => {
|
|
722
786
|
it('should not reset drawer state if already initialized', async () => {
|
|
723
787
|
await usingAsync(new Injector(), async (injector) => {
|
|
@@ -143,6 +143,11 @@ export const Drawer = Shade<DrawerProps>({
|
|
|
143
143
|
layoutService.setDrawerWidth(position, width)
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
// Clean up drawer state from LayoutService when this component is disposed
|
|
147
|
+
useDisposable('drawer-cleanup', () => ({
|
|
148
|
+
[Symbol.dispose]: () => layoutService.removeDrawer(position),
|
|
149
|
+
}))
|
|
150
|
+
|
|
146
151
|
// Subscribe to drawer state
|
|
147
152
|
const [drawerState] = useObservable('drawerState', layoutService.drawerState)
|
|
148
153
|
const isOpen = drawerState[position]?.open ?? false
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createComponent, Shade } from '@furystack/shades'
|
|
2
2
|
import { cssVariableTheme } from '../services/css-variable-theme.js'
|
|
3
|
-
import { getTextColor } from '../services/get-text-color.js'
|
|
4
3
|
import type { NotyModel } from '../services/noty-service.js'
|
|
5
4
|
import { NotyService } from '../services/noty-service.js'
|
|
6
5
|
import { ThemeProviderService } from '../services/theme-provider-service.js'
|
|
@@ -97,7 +96,6 @@ export const NotyComponent = Shade<{ model: NotyModel; onDismiss: () => void }>(
|
|
|
97
96
|
|
|
98
97
|
const themeProvider = injector.getInstance(ThemeProviderService)
|
|
99
98
|
const colors = themeProvider.theme.palette[props.model.type]
|
|
100
|
-
const textColor = getTextColor(colors.main)
|
|
101
99
|
|
|
102
100
|
const removeSelf = async () => {
|
|
103
101
|
const hostEl = wrapperRef.current?.closest('shade-noty') as HTMLElement | null
|
|
@@ -123,7 +121,7 @@ export const NotyComponent = Shade<{ model: NotyModel; onDismiss: () => void }>(
|
|
|
123
121
|
|
|
124
122
|
useHostProps({
|
|
125
123
|
'data-noty-type': props.model.type,
|
|
126
|
-
style: { '--noty-bg': colors.main, '--noty-text':
|
|
124
|
+
style: { '--noty-bg': colors.main, '--noty-text': colors.mainContrast },
|
|
127
125
|
})
|
|
128
126
|
|
|
129
127
|
return (
|
|
@@ -517,4 +517,50 @@ describe('CollectionService', () => {
|
|
|
517
517
|
})
|
|
518
518
|
})
|
|
519
519
|
})
|
|
520
|
+
|
|
521
|
+
describe('EventHub integration', () => {
|
|
522
|
+
it('Should allow subscribing to onRowClick via EventHub', () => {
|
|
523
|
+
const testEntries = createTestEntries()
|
|
524
|
+
const handler = vi.fn()
|
|
525
|
+
|
|
526
|
+
using(new CollectionService<TestEntry>(), (service) => {
|
|
527
|
+
service.addListener('onRowClick', handler)
|
|
528
|
+
service.data.setValue({ count: 3, entries: testEntries })
|
|
529
|
+
service.handleRowClick(testEntries[1], createMouseEvent())
|
|
530
|
+
|
|
531
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
532
|
+
expect(handler).toHaveBeenCalledWith(testEntries[1])
|
|
533
|
+
})
|
|
534
|
+
})
|
|
535
|
+
|
|
536
|
+
it('Should allow subscribing to onRowDoubleClick via EventHub', () => {
|
|
537
|
+
const testEntries = createTestEntries()
|
|
538
|
+
const handler = vi.fn()
|
|
539
|
+
|
|
540
|
+
using(new CollectionService<TestEntry>(), (service) => {
|
|
541
|
+
service.addListener('onRowDoubleClick', handler)
|
|
542
|
+
service.data.setValue({ count: 3, entries: testEntries })
|
|
543
|
+
service.handleRowDoubleClick(testEntries[2])
|
|
544
|
+
|
|
545
|
+
expect(handler).toHaveBeenCalledTimes(1)
|
|
546
|
+
expect(handler).toHaveBeenCalledWith(testEntries[2])
|
|
547
|
+
})
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it('Should support multiple subscribers for the same event', () => {
|
|
551
|
+
const testEntries = createTestEntries()
|
|
552
|
+
const handler1 = vi.fn()
|
|
553
|
+
const handler2 = vi.fn()
|
|
554
|
+
|
|
555
|
+
using(new CollectionService<TestEntry>(), (service) => {
|
|
556
|
+
service.addListener('onRowClick', handler1)
|
|
557
|
+
service.addListener('onRowClick', handler2)
|
|
558
|
+
service.data.setValue({ count: 3, entries: testEntries })
|
|
559
|
+
service.handleRowClick(testEntries[0], createMouseEvent())
|
|
560
|
+
|
|
561
|
+
expect(handler1).toHaveBeenCalledTimes(1)
|
|
562
|
+
expect(handler2).toHaveBeenCalledTimes(1)
|
|
563
|
+
})
|
|
564
|
+
})
|
|
565
|
+
})
|
|
520
566
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ObservableValue } from '@furystack/utils'
|
|
1
|
+
import { EventHub, ObservableValue, type ListenerErrorPayload } from '@furystack/utils'
|
|
2
2
|
|
|
3
3
|
export interface CollectionData<T> {
|
|
4
4
|
entries: T[]
|
|
@@ -12,25 +12,32 @@ export interface CollectionServiceOptions<T> {
|
|
|
12
12
|
searchField?: keyof T
|
|
13
13
|
/**
|
|
14
14
|
* @param entry The clicked entry
|
|
15
|
-
*
|
|
15
|
+
* @deprecated Use `subscribe('onRowClick', ...)` instead
|
|
16
16
|
*/
|
|
17
17
|
onRowClick?: (entry: T) => void
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Optional callback for row double clicks
|
|
21
20
|
* @param entry The clicked entry
|
|
21
|
+
* @deprecated Use `subscribe('onRowDoubleClick', ...)` instead
|
|
22
22
|
*/
|
|
23
|
-
|
|
24
23
|
onRowDoubleClick?: (entry: T) => void
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
export class CollectionService<T>
|
|
26
|
+
export class CollectionService<T>
|
|
27
|
+
extends EventHub<{
|
|
28
|
+
onRowClick: T
|
|
29
|
+
onRowDoubleClick: T
|
|
30
|
+
onListenerError: ListenerErrorPayload
|
|
31
|
+
}>
|
|
32
|
+
implements Disposable
|
|
33
|
+
{
|
|
28
34
|
public [Symbol.dispose]() {
|
|
29
35
|
this.data[Symbol.dispose]()
|
|
30
36
|
this.selection[Symbol.dispose]()
|
|
31
37
|
this.searchTerm[Symbol.dispose]()
|
|
32
38
|
this.hasFocus[Symbol.dispose]()
|
|
33
39
|
this.focusedEntry[Symbol.dispose]()
|
|
40
|
+
super[Symbol.dispose]()
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
public isSelected = (entry: T) => this.selection.getValue().includes(entry)
|
|
@@ -143,7 +150,7 @@ export class CollectionService<T> implements Disposable {
|
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
public handleRowClick(entry: T, ev: MouseEvent) {
|
|
146
|
-
this.
|
|
153
|
+
this.emit('onRowClick', entry)
|
|
147
154
|
const currentSelectionValue = this.selection.getValue()
|
|
148
155
|
const lastFocused = this.focusedEntry.getValue()
|
|
149
156
|
if (ev.ctrlKey) {
|
|
@@ -171,9 +178,17 @@ export class CollectionService<T> implements Disposable {
|
|
|
171
178
|
this.focusedEntry.setValue(entry)
|
|
172
179
|
}
|
|
173
180
|
|
|
174
|
-
constructor(private options: CollectionServiceOptions<T> = {}) {
|
|
181
|
+
constructor(private options: CollectionServiceOptions<T> = {}) {
|
|
182
|
+
super()
|
|
183
|
+
if (options.onRowClick) {
|
|
184
|
+
this.addListener('onRowClick', options.onRowClick)
|
|
185
|
+
}
|
|
186
|
+
if (options.onRowDoubleClick) {
|
|
187
|
+
this.addListener('onRowDoubleClick', options.onRowDoubleClick)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
175
190
|
|
|
176
191
|
public handleRowDoubleClick(entry: T) {
|
|
177
|
-
this.
|
|
192
|
+
this.emit('onRowDoubleClick', entry)
|
|
178
193
|
}
|
|
179
194
|
}
|