@lovett/ui 0.0.9 → 0.0.11

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.
Files changed (55) hide show
  1. package/dist/index.d.ts +823 -135
  2. package/dist/index.js +2048 -358
  3. package/dist/index.js.map +1 -1
  4. package/dist/styles.css +44 -2
  5. package/dist/theme-v2.css +228 -0
  6. package/dist/tokens.css +123 -8
  7. package/package.json +1 -1
  8. package/src/__tests__/anchor.test.tsx +422 -0
  9. package/src/__tests__/combobox.test.tsx +677 -0
  10. package/src/__tests__/dropdown-menu.test.tsx +418 -0
  11. package/src/__tests__/helpers/geometry.ts +58 -0
  12. package/src/__tests__/layer-stack.test.tsx +228 -0
  13. package/src/__tests__/modal.test.tsx +180 -6
  14. package/src/__tests__/popover.test.tsx +460 -0
  15. package/src/__tests__/select.test.tsx +543 -0
  16. package/src/__tests__/tooltip.test.tsx +355 -0
  17. package/src/calculator-shell-v2.tsx +19 -39
  18. package/src/code-block.tsx +15 -26
  19. package/src/combobox.tsx +796 -0
  20. package/src/dropdown-menu.tsx +142 -152
  21. package/src/icons/brand.tsx +81 -2
  22. package/src/index.ts +111 -0
  23. package/src/lib/anchor.ts +427 -0
  24. package/src/lib/focus.ts +32 -0
  25. package/src/lib/layer-stack.ts +188 -0
  26. package/src/lib/refs.ts +31 -0
  27. package/src/metric-card.tsx +57 -22
  28. package/src/modal.tsx +149 -9
  29. package/src/page-shell.tsx +91 -2
  30. package/src/popover.tsx +407 -0
  31. package/src/segmented-pill.tsx +33 -10
  32. package/src/select.tsx +646 -0
  33. package/src/stat-row.tsx +108 -70
  34. package/src/styles.css +44 -2
  35. package/src/theme-v2.css +7 -245
  36. package/src/tokens.css +123 -8
  37. package/src/tooltip.tsx +297 -0
  38. package/src/react-syntax-highlighter-prism.d.ts +0 -34
  39. package/src/v2/README.md +0 -208
  40. package/src/v2/__demo__/showcase.tsx +0 -1045
  41. package/src/v2/action.tsx +0 -91
  42. package/src/v2/callout.tsx +0 -76
  43. package/src/v2/document-section.tsx +0 -82
  44. package/src/v2/document-shell.tsx +0 -0
  45. package/src/v2/field-row.tsx +0 -113
  46. package/src/v2/icons.tsx +0 -165
  47. package/src/v2/index.ts +0 -147
  48. package/src/v2/layout.tsx +0 -293
  49. package/src/v2/progress-track.tsx +0 -89
  50. package/src/v2/stat-tile.tsx +0 -129
  51. package/src/v2/states.tsx +0 -271
  52. package/src/v2/status-pill.tsx +0 -74
  53. package/src/v2/theme.css +0 -1861
  54. package/src/v2/timeline.tsx +0 -81
  55. package/src/v2/tokens.ts +0 -228
@@ -0,0 +1,418 @@
1
+ /**
2
+ * DropdownMenu — 137 call sites, rebuilt on lib/anchor + lib/layer-stack.
3
+ *
4
+ * These pin the public contract the rebuild had to keep byte-for-byte:
5
+ * roles, data-slots, trigger ARIA, item click-closes, checkbox toggling,
6
+ * Escape-refocuses-trigger, outside-click, controlled mode, and the
7
+ * `align` / `side` / `sideOffset` / `alignOffset` props. Plus the one
8
+ * behaviour that CHANGED on purpose: a menu inside a Modal now closes
9
+ * alone on Escape.
10
+ */
11
+ import { createRef, useState } from 'react'
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
13
+ import { render, screen } from '@testing-library/react'
14
+ import userEvent from '@testing-library/user-event'
15
+ import Modal from '../modal'
16
+ import {
17
+ DropdownMenu,
18
+ DropdownMenuCheckboxItem,
19
+ DropdownMenuContent,
20
+ DropdownMenuGroup,
21
+ DropdownMenuItem,
22
+ DropdownMenuLabel,
23
+ DropdownMenuSeparator,
24
+ DropdownMenuTrigger,
25
+ type DropdownMenuContentProps,
26
+ } from '../dropdown-menu'
27
+ import { DropdownButton } from '../dropdown-button'
28
+ import { MenuButton } from '../menu-button'
29
+ import { mockGeometry, setViewport } from './helpers/geometry'
30
+
31
+ function Menu({
32
+ onSelect = () => {},
33
+ contentProps,
34
+ triggerProps,
35
+ }: {
36
+ onSelect?: () => void
37
+ contentProps?: Partial<React.ComponentProps<typeof DropdownMenuContent>>
38
+ triggerProps?: React.ComponentProps<typeof DropdownMenuTrigger>
39
+ }) {
40
+ return (
41
+ <DropdownMenu>
42
+ <DropdownMenuTrigger {...triggerProps}>Actions</DropdownMenuTrigger>
43
+ <DropdownMenuContent {...contentProps}>
44
+ <DropdownMenuGroup>
45
+ <DropdownMenuLabel>Section</DropdownMenuLabel>
46
+ </DropdownMenuGroup>
47
+ <DropdownMenuItem onClick={onSelect}>Rename</DropdownMenuItem>
48
+ <DropdownMenuSeparator />
49
+ <DropdownMenuItem variant="destructive">Delete</DropdownMenuItem>
50
+ </DropdownMenuContent>
51
+ </DropdownMenu>
52
+ )
53
+ }
54
+
55
+ const trigger = () => screen.getByRole('button', { name: 'Actions' })
56
+
57
+ describe('DropdownMenu', () => {
58
+ describe('trigger', () => {
59
+ it('is a menu-haspopup button with the secondary Button chrome by default', () => {
60
+ render(<Menu />)
61
+ const t = trigger()
62
+ expect(t).toHaveAttribute('type', 'button')
63
+ expect(t).toHaveAttribute('aria-haspopup', 'menu')
64
+ expect(t).toHaveAttribute('aria-expanded', 'false')
65
+ expect(t).not.toHaveAttribute('aria-controls')
66
+ expect(t).toHaveClass('btn', 'btn-secondary')
67
+ })
68
+
69
+ it('drops the chrome when unstyled', () => {
70
+ render(<Menu triggerProps={{ unstyled: true, className: 'mine' }} />)
71
+ expect(trigger()).not.toHaveClass('btn')
72
+ expect(trigger()).toHaveClass('mine')
73
+ })
74
+
75
+ it('forwards a ref to the <button>', () => {
76
+ const ref = createRef<HTMLButtonElement>()
77
+ render(<Menu triggerProps={{ ref }} />)
78
+ expect(ref.current).toBe(trigger())
79
+ })
80
+
81
+ it('keeps the menu closed when onClick prevents default', async () => {
82
+ render(<Menu triggerProps={{ onClick: (e) => e.preventDefault() }} />)
83
+ await userEvent.click(trigger())
84
+ expect(screen.queryByRole('menu')).toBeNull()
85
+ })
86
+ })
87
+
88
+ describe('content', () => {
89
+ it('renders nothing while closed', () => {
90
+ render(<Menu />)
91
+ expect(screen.queryByRole('menu')).toBeNull()
92
+ expect(document.querySelector('[data-slot="dropdown-menu-content"]')).toBeNull()
93
+ })
94
+
95
+ it('opens on click as role=menu, portaled to body, wired by aria-controls', async () => {
96
+ const { container } = render(<Menu />)
97
+ await userEvent.click(trigger())
98
+ const menu = screen.getByRole('menu')
99
+ expect(trigger()).toHaveAttribute('aria-expanded', 'true')
100
+ expect(trigger()).toHaveAttribute('aria-controls', menu.id)
101
+ expect(menu).toHaveAttribute('data-slot', 'dropdown-menu-content')
102
+ expect(menu).toHaveAttribute('data-state', 'open')
103
+ expect(menu.parentElement).toBe(document.body)
104
+ expect(container.contains(menu)).toBe(false)
105
+ })
106
+
107
+ it('exposes the compound parts with their roles and data-slots', async () => {
108
+ render(<Menu />)
109
+ await userEvent.click(trigger())
110
+ expect(screen.getByRole('group')).toHaveAttribute('data-slot', 'dropdown-menu-group')
111
+ expect(screen.getByText('Section')).toHaveAttribute('data-slot', 'dropdown-menu-label')
112
+ expect(screen.getByRole('separator')).toHaveAttribute('data-slot', 'dropdown-menu-separator')
113
+ const items = screen.getAllByRole('menuitem')
114
+ expect(items).toHaveLength(2)
115
+ expect(items[0]).toHaveAttribute('data-slot', 'dropdown-menu-item')
116
+ expect(items[0]).toHaveAttribute('data-variant', 'default')
117
+ expect(items[1]).toHaveAttribute('data-variant', 'destructive')
118
+ })
119
+
120
+ it('does not move focus into the menu on open (the trigger keeps it)', async () => {
121
+ render(<Menu />)
122
+ await userEvent.click(trigger())
123
+ expect(document.activeElement).toBe(trigger())
124
+ })
125
+
126
+ it('spreads className and style, caller style last', async () => {
127
+ render(
128
+ <Menu contentProps={{ className: 'w-56', style: { width: '17rem', padding: 6 } }} />,
129
+ )
130
+ await userEvent.click(trigger())
131
+ const menu = screen.getByRole('menu')
132
+ expect(menu).toHaveClass('w-56')
133
+ expect(menu.style.width).toBe('17rem')
134
+ expect(menu.style.padding).toBe('6px')
135
+ expect(menu.style.background).toContain('--popover')
136
+ })
137
+
138
+ it('forwards a ref to the menu element', async () => {
139
+ const ref = createRef<HTMLDivElement>()
140
+ render(<Menu contentProps={{ ref }} />)
141
+ await userEvent.click(trigger())
142
+ expect(ref.current).toBe(screen.getByRole('menu'))
143
+ })
144
+ })
145
+
146
+ describe('items', () => {
147
+ it('calls onClick and closes the menu', async () => {
148
+ const onSelect = vi.fn()
149
+ render(<Menu onSelect={onSelect} />)
150
+ await userEvent.click(trigger())
151
+ await userEvent.click(screen.getByRole('menuitem', { name: 'Rename' }))
152
+ expect(onSelect).toHaveBeenCalledTimes(1)
153
+ expect(screen.queryByRole('menu')).toBeNull()
154
+ })
155
+
156
+ it('stays open when the item onClick prevents default', async () => {
157
+ render(
158
+ <DropdownMenu>
159
+ <DropdownMenuTrigger>Actions</DropdownMenuTrigger>
160
+ <DropdownMenuContent>
161
+ <DropdownMenuItem onClick={(e) => e.preventDefault()}>Keep</DropdownMenuItem>
162
+ </DropdownMenuContent>
163
+ </DropdownMenu>,
164
+ )
165
+ await userEvent.click(trigger())
166
+ await userEvent.click(screen.getByRole('menuitem', { name: 'Keep' }))
167
+ expect(screen.getByRole('menu')).toBeInTheDocument()
168
+ })
169
+
170
+ it('inset pads the left edge', async () => {
171
+ render(
172
+ <DropdownMenu defaultOpen>
173
+ <DropdownMenuTrigger>Actions</DropdownMenuTrigger>
174
+ <DropdownMenuContent>
175
+ <DropdownMenuItem inset>Inset</DropdownMenuItem>
176
+ <DropdownMenuLabel inset>Inset label</DropdownMenuLabel>
177
+ </DropdownMenuContent>
178
+ </DropdownMenu>,
179
+ )
180
+ expect(screen.getByRole('menuitem', { name: 'Inset' })).toHaveClass('pl-9')
181
+ expect(screen.getByText('Inset label')).toHaveClass('pl-9')
182
+ })
183
+
184
+ it('checkbox item toggles through onCheckedChange and shows its indicator', async () => {
185
+ function Checkable() {
186
+ const [checked, setChecked] = useState(false)
187
+ return (
188
+ <DropdownMenu defaultOpen>
189
+ <DropdownMenuTrigger>Actions</DropdownMenuTrigger>
190
+ <DropdownMenuContent>
191
+ <DropdownMenuCheckboxItem checked={checked} onCheckedChange={setChecked}>
192
+ Show summary
193
+ </DropdownMenuCheckboxItem>
194
+ </DropdownMenuContent>
195
+ </DropdownMenu>
196
+ )
197
+ }
198
+ render(<Checkable />)
199
+ const item = screen.getByRole('menuitemcheckbox', { name: 'Show summary' })
200
+ expect(item).toHaveAttribute('aria-checked', 'false')
201
+ expect(item).toHaveAttribute('data-slot', 'dropdown-menu-checkbox-item')
202
+ expect(
203
+ item.querySelector('[data-slot="dropdown-menu-checkbox-item-indicator"]'),
204
+ ).toBeNull()
205
+
206
+ await userEvent.click(item)
207
+ expect(item).toHaveAttribute('aria-checked', 'true')
208
+ expect(
209
+ item.querySelector('[data-slot="dropdown-menu-checkbox-item-indicator"]'),
210
+ ).not.toBeNull()
211
+ // Toggling does NOT close the menu.
212
+ expect(screen.getByRole('menu')).toBeInTheDocument()
213
+ })
214
+
215
+ it('disabled checkbox item does not toggle', async () => {
216
+ const onCheckedChange = vi.fn()
217
+ render(
218
+ <DropdownMenu defaultOpen>
219
+ <DropdownMenuTrigger>Actions</DropdownMenuTrigger>
220
+ <DropdownMenuContent>
221
+ <DropdownMenuCheckboxItem disabled onCheckedChange={onCheckedChange}>
222
+ Locked
223
+ </DropdownMenuCheckboxItem>
224
+ </DropdownMenuContent>
225
+ </DropdownMenu>,
226
+ )
227
+ const item = screen.getByRole('menuitemcheckbox', { name: 'Locked' })
228
+ expect(item).toBeDisabled()
229
+ await userEvent.click(item)
230
+ expect(onCheckedChange).not.toHaveBeenCalled()
231
+ })
232
+ })
233
+
234
+ describe('dismissal', () => {
235
+ it('closes on Escape and returns focus to the trigger', async () => {
236
+ render(<Menu />)
237
+ await userEvent.click(trigger())
238
+ screen.getByRole('menuitem', { name: 'Rename' }).focus()
239
+ await userEvent.keyboard('{Escape}')
240
+ expect(screen.queryByRole('menu')).toBeNull()
241
+ expect(document.activeElement).toBe(trigger())
242
+ })
243
+
244
+ it('closes on an outside click', async () => {
245
+ render(
246
+ <>
247
+ <Menu />
248
+ <button type="button">Elsewhere</button>
249
+ </>,
250
+ )
251
+ await userEvent.click(trigger())
252
+ await userEvent.click(screen.getByRole('button', { name: 'Elsewhere' }))
253
+ expect(screen.queryByRole('menu')).toBeNull()
254
+ })
255
+
256
+ it('stays open on a click inside the menu that is not an item', async () => {
257
+ render(<Menu />)
258
+ await userEvent.click(trigger())
259
+ await userEvent.click(screen.getByText('Section'))
260
+ expect(screen.getByRole('menu')).toBeInTheDocument()
261
+ })
262
+
263
+ it('inside a Modal, Escape closes only the menu; the next Escape closes the modal', async () => {
264
+ // The regression this rebuild fixes: two document listeners used to
265
+ // both answer one Escape, closing menu AND modal together.
266
+ const onClose = vi.fn()
267
+ render(
268
+ <Modal isOpen onClose={onClose} title="Settings">
269
+ <Menu />
270
+ </Modal>,
271
+ )
272
+ await userEvent.click(trigger())
273
+ expect(screen.getByRole('menu')).toBeInTheDocument()
274
+
275
+ await userEvent.keyboard('{Escape}')
276
+ expect(screen.queryByRole('menu')).toBeNull()
277
+ expect(onClose).not.toHaveBeenCalled()
278
+
279
+ await userEvent.keyboard('{Escape}')
280
+ expect(onClose).toHaveBeenCalledTimes(1)
281
+ })
282
+
283
+ it('does not listen for Escape while closed', async () => {
284
+ const onOpenChange = vi.fn()
285
+ render(
286
+ <DropdownMenu onOpenChange={onOpenChange}>
287
+ <DropdownMenuTrigger>Actions</DropdownMenuTrigger>
288
+ <DropdownMenuContent>
289
+ <DropdownMenuItem>Rename</DropdownMenuItem>
290
+ </DropdownMenuContent>
291
+ </DropdownMenu>,
292
+ )
293
+ await userEvent.keyboard('{Escape}')
294
+ expect(onOpenChange).not.toHaveBeenCalled()
295
+ })
296
+ })
297
+
298
+ describe('controlled', () => {
299
+ it('reports transitions through onOpenChange and follows `open`', async () => {
300
+ function Controlled() {
301
+ const [open, setOpen] = useState(false)
302
+ return (
303
+ <>
304
+ <span data-testid="state">{open ? 'open' : 'closed'}</span>
305
+ <DropdownMenu open={open} onOpenChange={setOpen}>
306
+ <DropdownMenuTrigger>Actions</DropdownMenuTrigger>
307
+ <DropdownMenuContent>
308
+ <DropdownMenuItem>Rename</DropdownMenuItem>
309
+ </DropdownMenuContent>
310
+ </DropdownMenu>
311
+ </>
312
+ )
313
+ }
314
+ render(<Controlled />)
315
+ await userEvent.click(trigger())
316
+ expect(screen.getByTestId('state')).toHaveTextContent('open')
317
+ await userEvent.click(screen.getByRole('menuitem', { name: 'Rename' }))
318
+ expect(screen.getByTestId('state')).toHaveTextContent('closed')
319
+ expect(screen.queryByRole('menu')).toBeNull()
320
+ })
321
+ })
322
+
323
+ describe('positioning (mocked geometry)', () => {
324
+ let restore: () => void
325
+ beforeEach(() => {
326
+ restore = mockGeometry()
327
+ setViewport(1000, 800)
328
+ })
329
+ afterEach(() => restore())
330
+
331
+ function Positioned(props: Partial<DropdownMenuContentProps> & { rect?: string }) {
332
+ const { rect = '400,300,80,32', ...contentProps } = props
333
+ return (
334
+ <DropdownMenu defaultOpen>
335
+ <DropdownMenuTrigger data-rect={rect}>Actions</DropdownMenuTrigger>
336
+ <DropdownMenuContent data-size="200,120" {...contentProps}>
337
+ <DropdownMenuItem>Rename</DropdownMenuItem>
338
+ </DropdownMenuContent>
339
+ </DropdownMenu>
340
+ )
341
+ }
342
+
343
+ it('defaults to bottom-start with a 6px gap and animates in once positioned', () => {
344
+ render(<Positioned />)
345
+ const menu = screen.getByRole('menu')
346
+ expect(menu.style.position).toBe('fixed')
347
+ expect(menu.style.left).toBe('400px')
348
+ expect(menu.style.top).toBe('338px')
349
+ expect(menu).toHaveAttribute('data-positioned', 'true')
350
+ expect(menu).toHaveAttribute('data-side', 'bottom')
351
+ expect(menu).toHaveClass('ds-enter-pop')
352
+ })
353
+
354
+ it('align="end" right-aligns; alignOffset shifts inward', () => {
355
+ render(<Positioned align="end" alignOffset={10} />)
356
+ expect(screen.getByRole('menu').style.left).toBe(`${480 - 200 - 10}px`)
357
+ })
358
+
359
+ it('align="center" centres on the trigger', () => {
360
+ render(<Positioned align="center" />)
361
+ expect(screen.getByRole('menu').style.left).toBe('340px')
362
+ })
363
+
364
+ it('side="top" with sideOffset opens above', () => {
365
+ render(<Positioned side="top" sideOffset={10} />)
366
+ const menu = screen.getByRole('menu')
367
+ expect(menu).toHaveAttribute('data-side', 'top')
368
+ expect(menu.style.top).toBe(`${300 - 120 - 10}px`)
369
+ })
370
+
371
+ it('flips to top when the trigger sits at the bottom of the viewport', () => {
372
+ render(<Positioned rect="400,700,80,32" />)
373
+ const menu = screen.getByRole('menu')
374
+ expect(menu).toHaveAttribute('data-side', 'top')
375
+ expect(menu.style.top).toBe(`${700 - 120 - 6}px`)
376
+ })
377
+
378
+ it('clamps into the viewport gutter at the right edge', () => {
379
+ render(<Positioned rect="950,300,40,32" />)
380
+ expect(screen.getByRole('menu').style.left).toBe(`${1000 - 200 - 8}px`)
381
+ })
382
+ })
383
+
384
+ describe('composed primitives still work', () => {
385
+ it('DropdownButton opens its items and selects', async () => {
386
+ const onSelect = vi.fn()
387
+ render(
388
+ <DropdownButton
389
+ label="New plan"
390
+ items={[
391
+ { id: 'set', label: 'Set', description: 'A set', onSelect },
392
+ { id: 'group', label: 'Group', onSelect: () => {} },
393
+ ]}
394
+ />,
395
+ )
396
+ await userEvent.click(screen.getByRole('button', { name: 'New plan' }))
397
+ await userEvent.click(screen.getByRole('menuitem', { name: /Set/ }))
398
+ expect(onSelect).toHaveBeenCalledTimes(1)
399
+ expect(screen.queryByRole('menu')).toBeNull()
400
+ })
401
+
402
+ it('MenuButton renders separators and shortcuts', async () => {
403
+ render(
404
+ <MenuButton
405
+ label="File"
406
+ items={[
407
+ { id: 'new', label: 'New', shortcut: 'N', onSelect: () => {} },
408
+ { type: 'separator' },
409
+ { id: 'quit', label: 'Quit', variant: 'destructive', onSelect: () => {} },
410
+ ]}
411
+ />,
412
+ )
413
+ await userEvent.click(screen.getByRole('button', { name: 'File' }))
414
+ expect(screen.getAllByRole('menuitem')).toHaveLength(2)
415
+ expect(screen.getByRole('separator')).toBeInTheDocument()
416
+ })
417
+ })
418
+ })
@@ -0,0 +1,58 @@
1
+ /**
2
+ * jsdom has no layout: every rect is 0×0 at (0, 0) and every offset size is
3
+ * 0. The positioning hook needs real numbers, so elements under test declare
4
+ * their geometry in data attributes and these spies read them back:
5
+ *
6
+ * data-rect="left,top,width,height" → getBoundingClientRect()
7
+ * data-size="width,height" → offsetWidth / offsetHeight
8
+ *
9
+ * Call `mockGeometry()` in `beforeEach` and the returned restore in
10
+ * `afterEach`. Viewport is `window.innerWidth/Height` (jsdom's
11
+ * documentElement.clientWidth is 0, which the hook already falls through).
12
+ */
13
+ import { vi } from 'vitest'
14
+
15
+ function numbers(spec: string | undefined, count: number): number[] {
16
+ const parsed = spec ? spec.split(',').map(Number) : []
17
+ return Array.from({ length: count }, (_, i) => parsed[i] ?? 0)
18
+ }
19
+
20
+ export function mockGeometry(): () => void {
21
+ const rect = vi
22
+ .spyOn(Element.prototype, 'getBoundingClientRect')
23
+ .mockImplementation(function (this: Element) {
24
+ const spec = this instanceof HTMLElement ? this.dataset.rect : undefined
25
+ const [left, top, width, height] = numbers(spec, 4) as [number, number, number, number]
26
+ return {
27
+ left,
28
+ top,
29
+ width,
30
+ height,
31
+ right: left + width,
32
+ bottom: top + height,
33
+ x: left,
34
+ y: top,
35
+ toJSON: () => ({ left, top, width, height }),
36
+ } as DOMRect
37
+ })
38
+ const width = vi
39
+ .spyOn(HTMLElement.prototype, 'offsetWidth', 'get')
40
+ .mockImplementation(function (this: HTMLElement) {
41
+ return numbers(this.dataset.size, 2)[0] ?? 0
42
+ })
43
+ const height = vi
44
+ .spyOn(HTMLElement.prototype, 'offsetHeight', 'get')
45
+ .mockImplementation(function (this: HTMLElement) {
46
+ return numbers(this.dataset.size, 2)[1] ?? 0
47
+ })
48
+ return () => {
49
+ rect.mockRestore()
50
+ width.mockRestore()
51
+ height.mockRestore()
52
+ }
53
+ }
54
+
55
+ export function setViewport(width: number, height: number): void {
56
+ Object.defineProperty(window, 'innerWidth', { configurable: true, writable: true, value: width })
57
+ Object.defineProperty(window, 'innerHeight', { configurable: true, writable: true, value: height })
58
+ }