@linzjs/windows 9.4.0 → 9.5.2
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 +650 -2
- package/dist/LuiModalAsync/LuiModalAsyncContextProvider.tsx +2 -2
- package/dist/common/useInterval.ts +44 -0
- package/dist/common/useIsomorphicLayoutEffect.ts +18 -0
- package/dist/common/uuid.ts +4 -0
- package/dist/panel/Panel.tsx +1 -1
- package/dist/panel/PanelInline.tsx +1 -1
- package/dist/panel/PanelsContextProvider.tsx +2 -2
- package/dist/ribbon/RibbonSliderButton.tsx +1 -1
- package/package.json +14 -29
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ const result = await showModal(TestModal, { props... })
|
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
- Async HTML dialog based Modals.
|
|
28
|
-
- Draggable and resizeable, pop-in/out Panels/Windows.
|
|
28
|
+
- Draggable and resizeable, pop-in/out Panels/Windows.
|
|
29
29
|
|
|
30
30
|
## Install
|
|
31
31
|
```
|
|
@@ -48,4 +48,652 @@ localStorage.setItem("@linzjs/windows.debugEnabled", "true");
|
|
|
48
48
|
npm run storybook
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Components
|
|
54
|
+
|
|
55
|
+
### Modal (LuiModalAsync)
|
|
56
|
+
|
|
57
|
+
Promise-based async modals using the HTML `<dialog>` element. Show a modal, await the result, and continue.
|
|
58
|
+
|
|
59
|
+

|
|
60
|
+
|
|
61
|
+
**Setup** — wrap your app with the context provider once at the root:
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { LuiModalAsyncContextProvider } from '@linzjs/windows';
|
|
65
|
+
|
|
66
|
+
export const App = () => (
|
|
67
|
+
<LuiModalAsyncContextProvider>
|
|
68
|
+
<div>...the rest of your app...</div>
|
|
69
|
+
</LuiModalAsyncContextProvider>
|
|
70
|
+
);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**Define a modal component** — extend your props with `LuiModalAsyncCallback<RESULT_TYPE>` to declare the return type and receive `resolve`:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import React, { ReactElement } from 'react';
|
|
77
|
+
import {
|
|
78
|
+
LuiModalAsync,
|
|
79
|
+
LuiModalAsyncButtonContinue,
|
|
80
|
+
LuiModalAsyncButtonDismiss,
|
|
81
|
+
LuiModalAsyncButtonGroup,
|
|
82
|
+
LuiModalAsyncCallback,
|
|
83
|
+
LuiModalAsyncContent,
|
|
84
|
+
LuiModalAsyncHeader,
|
|
85
|
+
LuiModalAsyncMain,
|
|
86
|
+
} from '@linzjs/windows';
|
|
87
|
+
|
|
88
|
+
// The modal returns a number (or undefined if dismissed/escaped)
|
|
89
|
+
interface TestModalProps extends LuiModalAsyncCallback<number> {
|
|
90
|
+
text: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const TestModal = ({ text, resolve }: TestModalProps): ReactElement => {
|
|
94
|
+
const doSomething = () => {
|
|
95
|
+
resolve(10); // close modal and return 10
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<LuiModalAsync closeOnOverlayClick={false}>
|
|
100
|
+
<LuiModalAsyncMain>
|
|
101
|
+
<LuiModalAsyncHeader title={'Generic modal'} onHelpClick={() => alert('help link')} />
|
|
102
|
+
<LuiModalAsyncContent>{text}</LuiModalAsyncContent>
|
|
103
|
+
</LuiModalAsyncMain>
|
|
104
|
+
<LuiModalAsyncButtonGroup>
|
|
105
|
+
<LuiModalAsyncButtonDismiss autofocus={true}>Dismiss</LuiModalAsyncButtonDismiss>
|
|
106
|
+
<LuiModalAsyncButtonContinue level={'tertiary'} onClick={doSomething}>
|
|
107
|
+
Continue onClick
|
|
108
|
+
</LuiModalAsyncButtonContinue>
|
|
109
|
+
<LuiModalAsyncButtonContinue value={10}>Continue resolve value</LuiModalAsyncButtonContinue>
|
|
110
|
+
</LuiModalAsyncButtonGroup>
|
|
111
|
+
</LuiModalAsync>
|
|
112
|
+
);
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Invoke the modal** — use `useShowAsyncModal`, await the result:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { useShowAsyncModal } from '@linzjs/windows';
|
|
120
|
+
|
|
121
|
+
export const TestModalUsage = () => {
|
|
122
|
+
// modalOwnerRef is only required if you have popout windows
|
|
123
|
+
const { showModal, modalOwnerRef } = useShowAsyncModal();
|
|
124
|
+
|
|
125
|
+
const showModalHandler = async () => {
|
|
126
|
+
const result = await showModal(
|
|
127
|
+
TestModal,
|
|
128
|
+
{ text: "I'm generic modal content" },
|
|
129
|
+
{ showOnAllWindows: true },
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
if (!result) return alert('Modal closed');
|
|
133
|
+
alert(`Modal result is: ${String(result)}`);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Add modalOwnerRef so that modals work in popout windows
|
|
137
|
+
return (
|
|
138
|
+
<div ref={modalOwnerRef}>
|
|
139
|
+
<button onClick={() => void showModalHandler()}>Click to show the modal</button>
|
|
140
|
+
</div>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### Prefab Modal
|
|
148
|
+
|
|
149
|
+
Pre-built modals for common use cases: `outline`, `info`, `warning`, `error`, `success`, `progress`, and `blocked`.
|
|
150
|
+
|
|
151
|
+
| Outline | Info | Warning | Error |
|
|
152
|
+
|---------|------|---------|-------|
|
|
153
|
+
|  |  |  |  |
|
|
154
|
+
|
|
155
|
+
| Success | Progress | Blocked | Custom buttons |
|
|
156
|
+
|---------|----------|---------|----------------|
|
|
157
|
+
|  |  |  |  |
|
|
158
|
+
|
|
159
|
+
**Setup** — same `LuiModalAsyncContextProvider` as above.
|
|
160
|
+
|
|
161
|
+
**Invoke** — use `useLuiModalPrefab`:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import { useLuiModalPrefab, LuiModalDontShowSessionRemove } from '@linzjs/windows';
|
|
165
|
+
|
|
166
|
+
export const PrefabModalUsage = () => {
|
|
167
|
+
const { modalOwnerRef, showPrefabModal } = useLuiModalPrefab();
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<div ref={modalOwnerRef} style={{ display: 'flex', gap: 10 }}>
|
|
171
|
+
{/* Info */}
|
|
172
|
+
<button
|
|
173
|
+
onClick={() =>
|
|
174
|
+
void showPrefabModal({
|
|
175
|
+
level: 'info',
|
|
176
|
+
title: 'You are a fantastic person',
|
|
177
|
+
children: 'Keep it up!',
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
>
|
|
181
|
+
Info
|
|
182
|
+
</button>
|
|
183
|
+
|
|
184
|
+
{/* Warning with custom buttons */}
|
|
185
|
+
<button
|
|
186
|
+
onClick={() =>
|
|
187
|
+
void showPrefabModal<'delete'>({
|
|
188
|
+
level: 'warning',
|
|
189
|
+
title: 'You are about to make changes',
|
|
190
|
+
children: 'Are you sure that you want to make these changes?',
|
|
191
|
+
helpLink: 'https://www.example.com/help',
|
|
192
|
+
buttons: [
|
|
193
|
+
{ title: 'Cancel', icon: 'ic_navigate_before', value: undefined },
|
|
194
|
+
{ title: 'Delete the world!', icon: 'ic_delete_solid', value: 'delete', level: 'error' },
|
|
195
|
+
],
|
|
196
|
+
}).then((result) => {
|
|
197
|
+
alert('Warning result: ' + result);
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
>
|
|
201
|
+
Warning
|
|
202
|
+
</button>
|
|
203
|
+
|
|
204
|
+
{/* Warning with "don't show again" */}
|
|
205
|
+
<button
|
|
206
|
+
onClick={() =>
|
|
207
|
+
void showPrefabModal({
|
|
208
|
+
level: 'warning',
|
|
209
|
+
title: 'Warning',
|
|
210
|
+
children: 'This message can be suppressed.',
|
|
211
|
+
dontShowAgainSessionKey: 'userId_surveyId_modalId',
|
|
212
|
+
buttons: [{ title: 'Dismiss', default: true }],
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
>
|
|
216
|
+
Warning + Don't show again
|
|
217
|
+
</button>
|
|
218
|
+
|
|
219
|
+
{/* Error */}
|
|
220
|
+
<button
|
|
221
|
+
onClick={() =>
|
|
222
|
+
void showPrefabModal({
|
|
223
|
+
level: 'error',
|
|
224
|
+
title: 'Something is not right',
|
|
225
|
+
children: 'Maybe stop doing that',
|
|
226
|
+
onHelpClick: () => {},
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
>
|
|
230
|
+
Error
|
|
231
|
+
</button>
|
|
232
|
+
|
|
233
|
+
{/* Success */}
|
|
234
|
+
<button
|
|
235
|
+
onClick={() =>
|
|
236
|
+
void showPrefabModal({
|
|
237
|
+
level: 'success',
|
|
238
|
+
title: 'You are a success',
|
|
239
|
+
children: 'Keep succeeding!',
|
|
240
|
+
onHelpClick: () => {},
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
>
|
|
244
|
+
Success
|
|
245
|
+
</button>
|
|
246
|
+
|
|
247
|
+
{/* Progress — polledCloseCondition closes the modal automatically */}
|
|
248
|
+
<button
|
|
249
|
+
onClick={() => {
|
|
250
|
+
let done = false;
|
|
251
|
+
setTimeout(() => (done = true), 5000);
|
|
252
|
+
void showPrefabModal({
|
|
253
|
+
level: 'progress',
|
|
254
|
+
title: 'Signing in progress',
|
|
255
|
+
children: 'This modal will close in 5 seconds unless cancelled.',
|
|
256
|
+
buttons: [{ title: 'Cancel', value: true }],
|
|
257
|
+
polledCloseCondition: () => done,
|
|
258
|
+
}).then((result) => {
|
|
259
|
+
alert(result === true ? 'Cancelled' : 'Timed-out');
|
|
260
|
+
});
|
|
261
|
+
}}
|
|
262
|
+
>
|
|
263
|
+
Progress
|
|
264
|
+
</button>
|
|
265
|
+
|
|
266
|
+
{/* Blocked */}
|
|
267
|
+
<button
|
|
268
|
+
onClick={() =>
|
|
269
|
+
void showPrefabModal({
|
|
270
|
+
level: 'blocked',
|
|
271
|
+
title: 'Plan Generation blocked',
|
|
272
|
+
children: "This CSD is being used by 'Joe Bloggs'. Please close Plan Generation.",
|
|
273
|
+
buttons: [{ title: 'Return to Survey Capture' }],
|
|
274
|
+
})
|
|
275
|
+
}
|
|
276
|
+
>
|
|
277
|
+
Blocked
|
|
278
|
+
</button>
|
|
279
|
+
</div>
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
To clear a "don't show again" session key programmatically:
|
|
285
|
+
```tsx
|
|
286
|
+
import { LuiModalDontShowSessionRemove } from '@linzjs/windows';
|
|
287
|
+
|
|
288
|
+
LuiModalDontShowSessionRemove('userId_surveyId_modalId');
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
### Upload Modal
|
|
294
|
+
|
|
295
|
+
A prefab modal for file upload interactions.
|
|
296
|
+
|
|
297
|
+

|
|
298
|
+
|
|
299
|
+
**Setup** — same `LuiModalAsyncContextProvider` as above.
|
|
300
|
+
|
|
301
|
+
**Invoke** — use `useLuiModalUpload`:
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
import { useLuiModalUpload } from '@linzjs/windows';
|
|
305
|
+
|
|
306
|
+
export const ModalUploadUsage = () => {
|
|
307
|
+
const { modalOwnerRef, showUploadModal } = useLuiModalUpload();
|
|
308
|
+
|
|
309
|
+
return (
|
|
310
|
+
<div ref={modalOwnerRef}>
|
|
311
|
+
<button
|
|
312
|
+
onClick={() =>
|
|
313
|
+
void showUploadModal({
|
|
314
|
+
title: 'Add georeferenced image',
|
|
315
|
+
content: 'Once your image has been uploaded, place markers to align the image to the spatial view.',
|
|
316
|
+
width: 480,
|
|
317
|
+
fileDescription: 'image',
|
|
318
|
+
fileFormatText: 'Format: jpg, jpeg, png',
|
|
319
|
+
acceptedExtensions: ['jpg', 'jpeg', 'png'],
|
|
320
|
+
customFileErrorMessage: 'Incorrect file format!',
|
|
321
|
+
}).then((file) => {
|
|
322
|
+
console.log(file);
|
|
323
|
+
})
|
|
324
|
+
}
|
|
325
|
+
>
|
|
326
|
+
Upload Image...
|
|
327
|
+
</button>
|
|
328
|
+
</div>
|
|
329
|
+
);
|
|
330
|
+
};
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
### Panel
|
|
336
|
+
|
|
337
|
+
Draggable, resizeable, pop-in/pop-out panel windows. Panels are opened imperatively via `OpenPanelButton` or `openPanel`.
|
|
338
|
+
|
|
339
|
+

|
|
340
|
+
|
|
341
|
+
**Setup** — wrap your app with the context provider once at the root:
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
import { PanelsContextProvider } from '@linzjs/windows';
|
|
345
|
+
|
|
346
|
+
export const App = () => (
|
|
347
|
+
<PanelsContextProvider baseZIndex={500}>
|
|
348
|
+
<div>...the rest of your app...</div>
|
|
349
|
+
</PanelsContextProvider>
|
|
350
|
+
);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Define a panel component:**
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
import { Panel, PanelContent, PanelHeader } from '@linzjs/windows';
|
|
357
|
+
|
|
358
|
+
export const ShowPanelComponent = ({ data }: { data: number }) => (
|
|
359
|
+
<Panel title={`Panel demo ${data}`} size={{ width: 640, height: 'auto' }}>
|
|
360
|
+
<PanelHeader
|
|
361
|
+
helpUrl={'#help'}
|
|
362
|
+
icon={'ic_add_adopt'}
|
|
363
|
+
onHelpClick={() => alert('Help!')}
|
|
364
|
+
/>
|
|
365
|
+
<PanelContent>
|
|
366
|
+
{/* panel body content */}
|
|
367
|
+
</PanelContent>
|
|
368
|
+
</Panel>
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
// Panel as modal (disables popout, uses fixed height)
|
|
372
|
+
export const ShowPanelModalComponent = ({ data }: { data: number }) => (
|
|
373
|
+
<Panel title={`Panel demo ${data}`} size={{ width: 640, height: 400 }} modal={true}>
|
|
374
|
+
<PanelHeader icon={'ic_add_adopt'} disablePopout={true} />
|
|
375
|
+
<PanelContent>
|
|
376
|
+
{/* panel body content */}
|
|
377
|
+
</PanelContent>
|
|
378
|
+
</Panel>
|
|
379
|
+
);
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Open panels** — use `OpenPanelButton` or the `openPanel` function:
|
|
383
|
+
|
|
384
|
+
```tsx
|
|
385
|
+
import { OpenPanelButton } from '@linzjs/windows';
|
|
386
|
+
|
|
387
|
+
export const TestShowPanel = () => (
|
|
388
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
389
|
+
{/* uniqueId prevents duplicate panels */}
|
|
390
|
+
<OpenPanelButton
|
|
391
|
+
buttonText={'Panel 1'}
|
|
392
|
+
testId={'panel1'}
|
|
393
|
+
componentFn={() => <ShowPanelComponent data={1} />}
|
|
394
|
+
uniqueId="panel1"
|
|
395
|
+
/>
|
|
396
|
+
<OpenPanelButton
|
|
397
|
+
buttonText={'Panel 2'}
|
|
398
|
+
testId={'panel2'}
|
|
399
|
+
componentFn={() => <ShowPanelComponent data={2} />}
|
|
400
|
+
uniqueId="panel2"
|
|
401
|
+
/>
|
|
402
|
+
<OpenPanelButton
|
|
403
|
+
buttonText={'Panel as a Modal'}
|
|
404
|
+
componentFn={() => <ShowPanelModalComponent data={3} />}
|
|
405
|
+
/>
|
|
406
|
+
{/* Panel with dynamic bounds — recalculated on resize */}
|
|
407
|
+
<OpenPanelButton
|
|
408
|
+
buttonText={'Dynamic bounds panel'}
|
|
409
|
+
componentFn={() => (
|
|
410
|
+
<ShowPanelModalComponent
|
|
411
|
+
data={3}
|
|
412
|
+
resizeable={false}
|
|
413
|
+
dynamicBounds={() => ({
|
|
414
|
+
x: window.innerWidth / 3,
|
|
415
|
+
y: window.innerHeight / 4,
|
|
416
|
+
height: 'auto',
|
|
417
|
+
width: window.innerWidth * 0.4,
|
|
418
|
+
})}
|
|
419
|
+
/>
|
|
420
|
+
)}
|
|
421
|
+
/>
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Panel props:**
|
|
427
|
+
|
|
428
|
+
| Prop | Type | Description |
|
|
429
|
+
|------|------|-------------|
|
|
430
|
+
| `title` | `string` | Panel title bar text |
|
|
431
|
+
| `size` | `{ width: number, height: number \| 'auto' }` | Initial panel size |
|
|
432
|
+
| `modal` | `boolean` | Disable dragging and popout, center on screen |
|
|
433
|
+
| `resizeable` | `boolean` | Allow user resizing (default `true`) |
|
|
434
|
+
| `maxHeight` | `number` | Maximum height in px |
|
|
435
|
+
| `maxWidth` | `number` | Maximum width in px |
|
|
436
|
+
| `minHeight` | `number` | Minimum height in px |
|
|
437
|
+
| `minWidth` | `number` | Minimum width in px |
|
|
438
|
+
| `dynamicBounds` | `() => { x, y, width, height }` | Callback to recalculate position/size dynamically |
|
|
439
|
+
|
|
440
|
+
**Saving panel state** — pass `panelStateOptions` to `PanelsContextProvider` or `OpenPanelButton`:
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
<OpenPanelButton
|
|
444
|
+
buttonText={'Panel'}
|
|
445
|
+
componentFn={() => <ShowPanelComponent data={1} />}
|
|
446
|
+
panelStateOptions={{
|
|
447
|
+
saveStateIn: 'localStorage',
|
|
448
|
+
saveStateKey: 'userId',
|
|
449
|
+
}}
|
|
450
|
+
/>
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
---
|
|
454
|
+
|
|
455
|
+
### Panel with Docking
|
|
456
|
+
|
|
457
|
+
Panels can dock into a designated `PanelDock` area. Add `dockTo` to `PanelHeader` and place a `PanelDock` in your layout.
|
|
458
|
+
|
|
459
|
+
![Panel docking example screenshot placeholder]
|
|
460
|
+
|
|
461
|
+
```tsx
|
|
462
|
+
import { OpenPanelButton, Panel, PanelContent, PanelDock, PanelHeader } from '@linzjs/windows';
|
|
463
|
+
import { useState } from 'react';
|
|
464
|
+
|
|
465
|
+
// Panel that can dock to the left side
|
|
466
|
+
export const DockablePanel = ({ data }: { data: number }) => (
|
|
467
|
+
<Panel title={`Panel demo ${data}`} size={{ width: 640, height: 400 }}>
|
|
468
|
+
<PanelHeader dockTo={'leftSide'} />
|
|
469
|
+
<PanelContent>
|
|
470
|
+
{/* panel body content */}
|
|
471
|
+
</PanelContent>
|
|
472
|
+
</Panel>
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
export const TestShowPanel = () => {
|
|
476
|
+
const [visible, setVisible] = useState(true);
|
|
477
|
+
return (
|
|
478
|
+
<>
|
|
479
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
480
|
+
<OpenPanelButton buttonText={'panel'} componentFn={() => <DockablePanel data={1} />} />
|
|
481
|
+
<button onClick={() => setVisible(!visible)}>Toggle dock visible</button>
|
|
482
|
+
</div>
|
|
483
|
+
{/* The panel docks into this area when the user clicks the dock button */}
|
|
484
|
+
{visible && <PanelDock id={'leftSide'}>The Panel will dock in here</PanelDock>}
|
|
485
|
+
</>
|
|
486
|
+
);
|
|
487
|
+
};
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
### Tabbed Panel
|
|
493
|
+
|
|
494
|
+
Panels support tabs via the `@linzjs/lui` `LuiTabs` components. Wrap the `Panel` in `LuiTabs` and use `LuiTabsGroup`/`LuiTabsPanelSwitch` in the header.
|
|
495
|
+
|
|
496
|
+

|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
import { LuiTabs, LuiTabsGroup, LuiTabsPanel, LuiTabsPanelSwitch } from '@linzjs/lui';
|
|
500
|
+
import { OpenPanelButton, Panel, PanelContent, PanelHeader } from '@linzjs/windows';
|
|
501
|
+
|
|
502
|
+
export const TabbedPanelComponent = ({ data }: { data: number }) => (
|
|
503
|
+
<LuiTabs defaultPanel="africa">
|
|
504
|
+
<Panel title={`Panel demo ${data}`} size={{ width: 640, height: 400 }}>
|
|
505
|
+
<PanelHeader
|
|
506
|
+
icon={'ic_send'}
|
|
507
|
+
onHelpClick={() => alert('Help!!!')}
|
|
508
|
+
extraLeft={
|
|
509
|
+
<LuiTabsGroup ariaLabel="Animals">
|
|
510
|
+
<LuiTabsPanelSwitch targetPanel="africa">Africa</LuiTabsPanelSwitch>
|
|
511
|
+
<LuiTabsPanelSwitch targetPanel="asia">Asia</LuiTabsPanelSwitch>
|
|
512
|
+
</LuiTabsGroup>
|
|
513
|
+
}
|
|
514
|
+
/>
|
|
515
|
+
<PanelContent>
|
|
516
|
+
<LuiTabsPanel panel="africa">
|
|
517
|
+
<h2>African Countries</h2>
|
|
518
|
+
</LuiTabsPanel>
|
|
519
|
+
<LuiTabsPanel panel="asia">
|
|
520
|
+
<h2>Asian Countries</h2>
|
|
521
|
+
</LuiTabsPanel>
|
|
522
|
+
</PanelContent>
|
|
523
|
+
</Panel>
|
|
524
|
+
</LuiTabs>
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
export const TestShowTabbedPanel = () => (
|
|
528
|
+
<>
|
|
529
|
+
<OpenPanelButton buttonText={'TestPanel 1'} componentFn={() => <TabbedPanelComponent data={1} />} />
|
|
530
|
+
<OpenPanelButton buttonText={'TestPanel 2'} componentFn={() => <TabbedPanelComponent data={2} />} />
|
|
531
|
+
</>
|
|
532
|
+
);
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
---
|
|
536
|
+
|
|
537
|
+
### Panel with Global Modal
|
|
538
|
+
|
|
539
|
+
When a modal is shown from inside a panel that has been popped out into a separate window, use `showOnAllWindows: true` to ensure the modal appears in the correct window.
|
|
540
|
+
|
|
541
|
+
![Panel with global modal example screenshot placeholder]
|
|
542
|
+
|
|
543
|
+
```tsx
|
|
544
|
+
import { useLuiModalPrefab, OpenPanelButton, Panel, PanelContent, PanelHeader } from '@linzjs/windows';
|
|
545
|
+
|
|
546
|
+
// showOnAllWindows ensures the modal appears even in popped-out windows
|
|
547
|
+
const PanelContents = () => {
|
|
548
|
+
const { showPrefabModal } = useLuiModalPrefab();
|
|
549
|
+
|
|
550
|
+
return (
|
|
551
|
+
<div>
|
|
552
|
+
<button
|
|
553
|
+
onClick={() =>
|
|
554
|
+
void showPrefabModal({
|
|
555
|
+
showOnAllWindows: true,
|
|
556
|
+
level: 'info',
|
|
557
|
+
title: 'You are a fantastic person',
|
|
558
|
+
children: 'Keep it up!',
|
|
559
|
+
})
|
|
560
|
+
}
|
|
561
|
+
>
|
|
562
|
+
Show modal
|
|
563
|
+
</button>
|
|
564
|
+
</div>
|
|
565
|
+
);
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
export const TestShowPanelWithGlobalModal = () => (
|
|
569
|
+
<>
|
|
570
|
+
<OpenPanelButton buttonText={'TestPanel 1'} componentFn={() => (
|
|
571
|
+
<Panel title={'Panel demo 1'} size={{ width: 640, height: 400 }}>
|
|
572
|
+
<PanelHeader icon={'ic_send'} onHelpClick={() => alert('Help!!!')} />
|
|
573
|
+
<PanelContent><PanelContents /></PanelContent>
|
|
574
|
+
</Panel>
|
|
575
|
+
)} />
|
|
576
|
+
</>
|
|
577
|
+
);
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
### Ribbon
|
|
583
|
+
|
|
584
|
+
A toolbar ribbon with buttons, sliders, menus, and separators. Supports horizontal and vertical orientations.
|
|
585
|
+
|
|
586
|
+
![Ribbon example screenshot placeholder]
|
|
587
|
+
|
|
588
|
+
**Setup** — requires both `LuiModalAsyncContextProvider` and `PanelsContextProvider`:
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
import {
|
|
592
|
+
RibbonButton,
|
|
593
|
+
RibbonButtonLink,
|
|
594
|
+
RibbonButtonOpenPanel,
|
|
595
|
+
RibbonButtonSlider,
|
|
596
|
+
RibbonContainer,
|
|
597
|
+
RibbonSeparator,
|
|
598
|
+
RibbonMenu,
|
|
599
|
+
RibbonMenuOption,
|
|
600
|
+
RibbonMenuSeparator,
|
|
601
|
+
Panel,
|
|
602
|
+
PanelContent,
|
|
603
|
+
PanelHeader,
|
|
604
|
+
} from '@linzjs/windows';
|
|
605
|
+
import { useState } from 'react';
|
|
606
|
+
|
|
607
|
+
export const TestRibbonPanel = () => {
|
|
608
|
+
const [selectedItem, setSelectedItem] = useState('ic_add_rectangle');
|
|
609
|
+
const [loading, setLoading] = useState(false);
|
|
610
|
+
const [processing, setProcessing] = useState(false);
|
|
611
|
+
|
|
612
|
+
return (
|
|
613
|
+
<>
|
|
614
|
+
<button onClick={() => setLoading(!loading)}>Toggle loading</button>
|
|
615
|
+
<button onClick={() => setProcessing(!processing)}>Toggle processing</button>
|
|
616
|
+
|
|
617
|
+
{/* Horizontal ribbon */}
|
|
618
|
+
<RibbonContainer style={{ position: 'absolute', left: 220, top: 120 }}>
|
|
619
|
+
|
|
620
|
+
{/* Button that opens a panel — disabled state */}
|
|
621
|
+
<RibbonButtonOpenPanel
|
|
622
|
+
disabled
|
|
623
|
+
title={'Marks'}
|
|
624
|
+
icon={'ic_marks'}
|
|
625
|
+
componentFn={() => <Panel title="Marks" size={{ width: 640, height: 400 }}><PanelHeader /><PanelContent /></Panel>}
|
|
626
|
+
loading={loading}
|
|
627
|
+
/>
|
|
628
|
+
|
|
629
|
+
<RibbonSeparator />
|
|
630
|
+
|
|
631
|
+
{/* Button that opens a panel — with processing indicator */}
|
|
632
|
+
<RibbonButtonOpenPanel
|
|
633
|
+
title={'Vectors'}
|
|
634
|
+
icon={'ic_line_irregular'}
|
|
635
|
+
componentFn={() => <Panel title="Vectors" size={{ width: 640, height: 400 }}><PanelHeader /><PanelContent /></Panel>}
|
|
636
|
+
loading={loading}
|
|
637
|
+
processing={processing}
|
|
638
|
+
processingMessage={'Validating...'}
|
|
639
|
+
/>
|
|
640
|
+
|
|
641
|
+
{/* Slider button — opens a sub-panel of options below */}
|
|
642
|
+
<RibbonButtonSlider title={selectedItem} icon={selectedItem} alignment={'down'} loading={loading}>
|
|
643
|
+
<RibbonContainer orientation={'vertical'}>
|
|
644
|
+
<RibbonButton
|
|
645
|
+
title={'Rectangle'}
|
|
646
|
+
icon={'ic_add_rectangle'}
|
|
647
|
+
selected={selectedItem === 'ic_add_rectangle'}
|
|
648
|
+
onClick={() => setSelectedItem('ic_add_rectangle')}
|
|
649
|
+
/>
|
|
650
|
+
<RibbonButton
|
|
651
|
+
title={'Other'}
|
|
652
|
+
icon={'ic_polygon_selection'}
|
|
653
|
+
selected={selectedItem === 'ic_polygon_selection'}
|
|
654
|
+
onClick={() => setSelectedItem('ic_polygon_selection')}
|
|
655
|
+
/>
|
|
656
|
+
</RibbonContainer>
|
|
657
|
+
</RibbonButtonSlider>
|
|
658
|
+
|
|
659
|
+
{/* Slider button — dropdown menu with autoClose */}
|
|
660
|
+
<RibbonButtonSlider title={'Other'} icon={'ic_more_vert'} alignment={'down'} autoClose={true} loading={loading}>
|
|
661
|
+
<RibbonMenu>
|
|
662
|
+
<RibbonMenuOption icon={'ic_define_nonprimary_diagram_circle'}>Circle</RibbonMenuOption>
|
|
663
|
+
<RibbonMenuOption icon={'ic_define_nonprimary_diagram_rectangle'} disabled={true}>Rectangle</RibbonMenuOption>
|
|
664
|
+
<RibbonMenuSeparator />
|
|
665
|
+
<RibbonMenuOption>Item 3</RibbonMenuOption>
|
|
666
|
+
<RibbonMenuSeparator />
|
|
667
|
+
<RibbonMenuOption icon={'ic_clear'}>Cancel</RibbonMenuOption>
|
|
668
|
+
</RibbonMenu>
|
|
669
|
+
</RibbonButtonSlider>
|
|
670
|
+
</RibbonContainer>
|
|
671
|
+
|
|
672
|
+
{/* Vertical ribbon */}
|
|
673
|
+
<RibbonContainer orientation={'vertical'} style={{ position: 'absolute', left: 220, top: 180 }}>
|
|
674
|
+
<RibbonButtonOpenPanel
|
|
675
|
+
title={'Vectors'}
|
|
676
|
+
icon={'ic_timeline'}
|
|
677
|
+
componentFn={() => <Panel title="Vectors" size={{ width: 640, height: 400 }}><PanelHeader /><PanelContent /></Panel>}
|
|
678
|
+
loading={loading}
|
|
679
|
+
/>
|
|
680
|
+
{/* Slider opens to the right */}
|
|
681
|
+
<RibbonButtonSlider title={selectedItem} icon={selectedItem} alignment={'right'} loading={loading}>
|
|
682
|
+
<RibbonContainer>
|
|
683
|
+
<RibbonButton
|
|
684
|
+
title={'Rectangle'}
|
|
685
|
+
icon={'ic_add_rectangle'}
|
|
686
|
+
selected={selectedItem === 'ic_add_rectangle'}
|
|
687
|
+
onClick={() => setSelectedItem('ic_add_rectangle')}
|
|
688
|
+
/>
|
|
689
|
+
</RibbonContainer>
|
|
690
|
+
</RibbonButtonSlider>
|
|
691
|
+
{/* External link button */}
|
|
692
|
+
<RibbonButtonLink href={'https://example.com/'} icon={'ic_link'} loading={loading} />
|
|
693
|
+
</RibbonContainer>
|
|
694
|
+
</>
|
|
695
|
+
);
|
|
696
|
+
};
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
**`RibbonButtonSlider` alignment options:** `'down'` | `'up'` | `'left'` | `'right'` | `'left-up'` | `'right-up'` | `'right-center'`
|
|
@@ -8,8 +8,6 @@ import React, {
|
|
|
8
8
|
useState,
|
|
9
9
|
} from 'react';
|
|
10
10
|
import { createPortal } from 'react-dom';
|
|
11
|
-
import { useInterval } from 'usehooks-ts';
|
|
12
|
-
import { v4 } from 'uuid';
|
|
13
11
|
|
|
14
12
|
import { debugLog } from '../common/debug';
|
|
15
13
|
import {
|
|
@@ -20,6 +18,8 @@ import {
|
|
|
20
18
|
ShowModalProps,
|
|
21
19
|
} from './LuiModalAsyncContext';
|
|
22
20
|
import { LuiModalAsyncInstanceContext } from './LuiModalAsyncInstanceContext';
|
|
21
|
+
import { v4 } from 'uuid';
|
|
22
|
+
import { useInterval } from '../common/useInterval';
|
|
23
23
|
|
|
24
24
|
export interface LuiModalAsyncInstance {
|
|
25
25
|
uuid: string;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { useIsomorphicLayoutEffect } from 'usehooks-ts';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Copied from usehooks-ts
|
|
6
|
+
*
|
|
7
|
+
* Custom hook that creates an interval that invokes a callback function at a specified delay using the [`setInterval API`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval).
|
|
8
|
+
* @param {() => void} callback - The function to be invoked at each interval.
|
|
9
|
+
* @param {number | null} delay - The time, in milliseconds, between each invocation of the callback. Use `null` to clear the interval.
|
|
10
|
+
* @public
|
|
11
|
+
* @see [Documentation](https://usehooks-ts.com/react-hook/use-interval)
|
|
12
|
+
* @example
|
|
13
|
+
* ```tsx
|
|
14
|
+
* const handleInterval = () => {
|
|
15
|
+
* // Code to be executed at each interval
|
|
16
|
+
* };
|
|
17
|
+
* useInterval(handleInterval, 1000);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function useInterval(callback: () => void, delay: number | null) {
|
|
21
|
+
const savedCallback = useRef(callback);
|
|
22
|
+
|
|
23
|
+
// Remember the latest callback if it changes.
|
|
24
|
+
useIsomorphicLayoutEffect(() => {
|
|
25
|
+
savedCallback.current = callback;
|
|
26
|
+
}, [callback]);
|
|
27
|
+
|
|
28
|
+
// Set up the interval.
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
// Don't schedule if no delay is specified.
|
|
31
|
+
// Note: 0 is a valid value for delay.
|
|
32
|
+
if (delay === null) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const id = setInterval(() => {
|
|
37
|
+
savedCallback.current();
|
|
38
|
+
}, delay);
|
|
39
|
+
|
|
40
|
+
return () => {
|
|
41
|
+
clearInterval(id);
|
|
42
|
+
};
|
|
43
|
+
}, [delay]);
|
|
44
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Copied from usehooks-ts
|
|
5
|
+
*
|
|
6
|
+
* Custom hook that uses either `useLayoutEffect` or `useEffect` based on the environment (client-side or server-side).
|
|
7
|
+
* @param {Function} effect - The effect function to be executed.
|
|
8
|
+
* @param {Array<any>} [dependencies] - An array of dependencies for the effect (optional).
|
|
9
|
+
* @public
|
|
10
|
+
* @see [Documentation](https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect)
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* useIsomorphicLayoutEffect(() => {
|
|
14
|
+
* // Code to be executed during the layout phase on the client side
|
|
15
|
+
* }, [dependency1, dependency2]);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|
package/dist/panel/Panel.tsx
CHANGED
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
useState,
|
|
16
16
|
} from 'react';
|
|
17
17
|
import { createPortal } from 'react-dom';
|
|
18
|
-
import { useInterval } from 'usehooks-ts';
|
|
19
18
|
|
|
20
19
|
import { PanelInstanceContext } from './PanelInstanceContext';
|
|
21
20
|
import { PanelsContext } from './PanelsContext';
|
|
@@ -25,6 +24,7 @@ import { PanelPosition } from './types/PanelPosition';
|
|
|
25
24
|
import { PanelProps } from './types/PanelProps';
|
|
26
25
|
import { PanelSize } from './types/PanelSize';
|
|
27
26
|
import { useRestoreStateFrom } from './usePanelStateHandler';
|
|
27
|
+
import { useInterval } from '../common/useInterval';
|
|
28
28
|
|
|
29
29
|
const defaultInitialSize = { width: 320, height: 200 };
|
|
30
30
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { PropsWithChildren } from 'react';
|
|
2
|
-
import { v4 } from 'uuid';
|
|
3
2
|
|
|
4
3
|
import { PanelContext } from './PanelContext';
|
|
5
4
|
import { PanelInstanceContext } from './PanelInstanceContext';
|
|
5
|
+
import { v4 } from '../common/uuid';
|
|
6
6
|
|
|
7
7
|
export interface PanelInlineProps {
|
|
8
8
|
title: string;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { castArray, isEmpty, maxBy, partition, sortBy } from 'lodash-es';
|
|
2
2
|
import React, { Fragment, PropsWithChildren, ReactElement, useCallback, useMemo, useRef, useState } from 'react';
|
|
3
|
-
import { useInterval } from 'usehooks-ts';
|
|
4
|
-
import { v4 } from 'uuid';
|
|
5
3
|
|
|
6
4
|
import { PanelInstanceContextProvider } from './PanelInstanceContextProvider';
|
|
7
5
|
import { OpenPanelOptions, PanelInstance, PanelsContext } from './PanelsContext';
|
|
8
6
|
import { PanelPosition } from './types';
|
|
9
7
|
import { PanelStateOptions } from './types/PanelStateOptions';
|
|
8
|
+
import { v4 } from '../common/uuid';
|
|
9
|
+
import { useInterval } from '../common/useInterval';
|
|
10
10
|
|
|
11
11
|
export interface PanelsContextProviderProps {
|
|
12
12
|
baseZIndex?: number;
|
|
@@ -2,10 +2,10 @@ import './Ribbon.scss';
|
|
|
2
2
|
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { PropsWithChildren, ReactElement, useState } from 'react';
|
|
5
|
-
import { v4 } from 'uuid';
|
|
6
5
|
|
|
7
6
|
import { RibbonButton, RibbonButtonProps } from './RibbonButton';
|
|
8
7
|
import { RibbonButtonSliderContext, RibbonSliderAlignment, ribbonSliderAlignments } from './RibbonButtonSliderContext';
|
|
8
|
+
import { v4 } from '../common/uuid';
|
|
9
9
|
|
|
10
10
|
export interface RibbonSliderProps extends RibbonButtonProps {
|
|
11
11
|
autoClose?: boolean;
|
package/package.json
CHANGED
|
@@ -13,10 +13,9 @@
|
|
|
13
13
|
"popout"
|
|
14
14
|
],
|
|
15
15
|
"main": "./dist/index.ts",
|
|
16
|
-
"version": "9.
|
|
16
|
+
"version": "9.5.2",
|
|
17
17
|
"peerDependencies": {
|
|
18
18
|
"@linzjs/lui": ">=23",
|
|
19
|
-
"lodash-es": ">=4",
|
|
20
19
|
"react": ">=18",
|
|
21
20
|
"react-dom": ">=18"
|
|
22
21
|
},
|
|
@@ -31,9 +30,9 @@
|
|
|
31
30
|
"node": ">=22"
|
|
32
31
|
},
|
|
33
32
|
"scripts": {
|
|
34
|
-
"build": "run-s clean lintall bundle",
|
|
33
|
+
"build": "mkdirp ./dist && run-s clean lintall bundle",
|
|
35
34
|
"yalc": "run-s clean bundle && yalc push",
|
|
36
|
-
"clean": "
|
|
35
|
+
"clean": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\"",
|
|
37
36
|
"bundle": "tsc --noEmit && rollup -c",
|
|
38
37
|
"test": "vitest run",
|
|
39
38
|
"test:watch": "vitest --watch",
|
|
@@ -49,25 +48,19 @@
|
|
|
49
48
|
"dependencies": {
|
|
50
49
|
"@emotion/cache": "^11.14.0",
|
|
51
50
|
"@emotion/react": "^11.14.0",
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"react-loading-skeleton": "^3.5.0",
|
|
56
|
-
"react-rnd": "^10.5.3",
|
|
57
|
-
"usehooks-ts": "^3.1.1",
|
|
58
|
-
"uuid": "^13.0.0"
|
|
51
|
+
"lodash-es": "4.18.1",
|
|
52
|
+
"react-loading-skeleton": "3.5.0",
|
|
53
|
+
"react-rnd": "10.5.3"
|
|
59
54
|
},
|
|
60
55
|
"devDependencies": {
|
|
61
56
|
"@chromatic-com/storybook": "^4.1.3",
|
|
62
57
|
"@linzjs/lui": "^24.11.0",
|
|
63
58
|
"@linzjs/step-ag-grid": "^29.14.1",
|
|
64
|
-
"@linzjs/style": "^5.4.0",
|
|
65
59
|
"@rollup/plugin-commonjs": "^28.0.9",
|
|
66
|
-
"@rollup/plugin-json": "^6.1.0",
|
|
67
60
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
68
|
-
"@storybook/addon-docs": "
|
|
69
|
-
"@storybook/addon-links": "
|
|
70
|
-
"@storybook/react-vite": "
|
|
61
|
+
"@storybook/addon-docs": "9.1.20",
|
|
62
|
+
"@storybook/addon-links": "9.1.20",
|
|
63
|
+
"@storybook/react-vite": "9.1.20",
|
|
71
64
|
"@testing-library/dom": "^10.4.1",
|
|
72
65
|
"@testing-library/react": "^16.3.2",
|
|
73
66
|
"@testing-library/user-event": "^14.6.1",
|
|
@@ -76,23 +69,20 @@
|
|
|
76
69
|
"@types/react": "^18.3.28",
|
|
77
70
|
"@types/react-dom": "^18.3.7",
|
|
78
71
|
"@vitejs/plugin-react-swc": "^4.3.0",
|
|
79
|
-
"@vitest/ui": "^4.1.
|
|
72
|
+
"@vitest/ui": "^4.1.2",
|
|
80
73
|
"ag-grid-community": "34.2.0",
|
|
81
74
|
"ag-grid-react": "34.2.0",
|
|
82
75
|
"jsdom": "^27.3.0",
|
|
83
76
|
"mkdirp": "^3.0.1",
|
|
84
77
|
"npm-run-all": "^4.1.5",
|
|
85
|
-
"
|
|
86
|
-
"
|
|
78
|
+
"oxfmt": "^0.43.0",
|
|
79
|
+
"oxlint": "^1.58.0",
|
|
87
80
|
"react": "^18.3.1",
|
|
88
|
-
"react-app-polyfill": "^3.0.0",
|
|
89
81
|
"react-dom": "18.3.1",
|
|
90
|
-
"rollup": "^4.60.
|
|
82
|
+
"rollup": "^4.60.1",
|
|
91
83
|
"rollup-plugin-copy": "^3.5.0",
|
|
92
84
|
"sass": "^1.98.0",
|
|
93
|
-
"
|
|
94
|
-
"storybook": "^9.1.19",
|
|
95
|
-
"style-loader": "^4.0.0",
|
|
85
|
+
"storybook": "9.1.20",
|
|
96
86
|
"stylelint": "^16.26.1",
|
|
97
87
|
"stylelint-config-recommended": "^17.0.0",
|
|
98
88
|
"stylelint-config-recommended-scss": "^16.0.2",
|
|
@@ -101,14 +91,9 @@
|
|
|
101
91
|
"stylelint-scss": "6.14.0",
|
|
102
92
|
"typescript": "^5.9.3",
|
|
103
93
|
"vite": "^7.3.1",
|
|
104
|
-
"vite-plugin-html": "^3.2.2",
|
|
105
94
|
"vite-tsconfig-paths": "^5.1.4",
|
|
106
95
|
"vitest": "^4.1.0"
|
|
107
96
|
},
|
|
108
|
-
"optionalDependencies": {
|
|
109
|
-
"@rollup/rollup-linux-x64-gnu": "^4.60.0",
|
|
110
|
-
"@swc/core-linux-x64-gnu": "^1.15.21"
|
|
111
|
-
},
|
|
112
97
|
"browserslist": {
|
|
113
98
|
"production": [
|
|
114
99
|
">0.2%",
|