@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 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
- See [Chromatic storybook](https://master--64a2356b80885af35510b627.chromatic.com/) for documentation.
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
+ ![Generic modal example](/doc-images/generic-modal.png)
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
+ | ![Outline modal](/doc-images/prefab-modal-outline.png) | ![Info modal](/doc-images/prefab-modal-info.png) | ![Warning modal](/doc-images/prefab-modal-warning.png) | ![Error modal](/doc-images/prefab-modal-error.png) |
154
+
155
+ | Success | Progress | Blocked | Custom buttons |
156
+ |---------|----------|---------|----------------|
157
+ | ![Success modal](/doc-images/prefab-modal-success.png) | ![Progress modal](/doc-images/prefab-modal-progress.png) | ![Blocked modal](/doc-images/prefab-modal-blocked.png) | ![Custom buttons modal](/doc-images/prefab-modal-custom.png) |
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
+ ![Upload modal](/doc-images/file-upload.png)
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
+ ![Panel](/doc-images/show-panel.png)
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
+ ![Tabbed panel example screenshot placeholder](/doc-images/panel-tabbed.png)
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;
@@ -0,0 +1,4 @@
1
+ let counter = 0;
2
+
3
+ // We don't need a uuid a simple number would suffice
4
+ export const v4 = () => `__windowsId__${counter++}`;
@@ -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.4.0",
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": "rimraf dist && mkdirp ./dist",
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
- "@emotion/styled": "11.14.1",
53
- "@types/uuid": "^11.0.0",
54
- "lodash-es": ">=4",
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": "^9.1.20",
69
- "@storybook/addon-links": "^9.1.20",
70
- "@storybook/react-vite": "^9.1.20",
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.0",
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
- "oxlint": "^1.56.0",
86
- "oxfmt": "^0.41.0",
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.0",
82
+ "rollup": "^4.60.1",
91
83
  "rollup-plugin-copy": "^3.5.0",
92
84
  "sass": "^1.98.0",
93
- "sass-loader": "^16.0.7",
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%",