@corndev/ui-overlays 0.0.0-stage → 1.0.0-beta.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/LICENSE +21 -0
- package/README.md +45 -2
- package/dist/components/accordion/accordion.css +109 -0
- package/dist/components/accordion/accordion.d.ts +41 -0
- package/dist/components/drawer/drawer.css +117 -0
- package/dist/components/drawer/drawer.d.ts +29 -0
- package/dist/components/menu/menu.css +106 -0
- package/dist/components/menu/menu.d.ts +71 -0
- package/dist/components/modal/modal.css +122 -0
- package/dist/components/modal/modal.d.ts +27 -0
- package/dist/components/popover/popover.css +115 -0
- package/dist/components/popover/popover.d.ts +36 -0
- package/dist/components/tooltip/tooltip.css +60 -0
- package/dist/components/tooltip/tooltip.d.ts +19 -0
- package/dist/index.css +13 -0
- package/dist/index.js +1089 -0
- package/dist/lib/after-transition.d.ts +18 -0
- package/dist/lib/described-by.d.ts +23 -0
- package/dist/lib/dialog-focus.d.ts +48 -0
- package/dist/lib/inert.d.ts +56 -0
- package/dist/lib/styles-check.d.ts +12 -0
- package/dist/overlays.d.ts +7 -0
- package/package.json +50 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Corndog Development LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
<!-- Generated by scripts/generate-readmes.js. Edit the registry or that script, not this file. -->
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# @corndev/ui-overlays
|
|
4
|
+
|
|
5
|
+
Overlay components: Modal, Drawer, Accordion, Tooltip, Popover, Menu.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @corndev/ui-overlays
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { createModal } from '@corndev/ui-overlays';
|
|
17
|
+
import '@corndev/ui-overlays/styles';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Styles
|
|
21
|
+
|
|
22
|
+
The stylesheet ships with the package and is imported separately, so a page can use the classes with no JavaScript:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import '@corndev/ui-overlays/styles';
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Components
|
|
29
|
+
|
|
30
|
+
- [Modal](https://cornui.com/components/modal) — Dialog overlay with focus trap, backdrop, and Escape to close.
|
|
31
|
+
- [Drawer](https://cornui.com/components/drawer) — Slide-in panel from left or right with focus trap.
|
|
32
|
+
- [Accordion](https://cornui.com/components/accordion) — Collapsible sections. Vanilla expects [data-accordion-item][data-accordion-trigger] and [data-accordion-item][data-accordion-panel] in markup; React composes Accordion.Item children.
|
|
33
|
+
- [Tooltip](https://cornui.com/components/tooltip) — Small label shown on hover/focus. Auto-flips when near viewport edge.
|
|
34
|
+
- [Popover](https://cornui.com/components/popover) — Floating panel anchored to a trigger. Auto-flips when near viewport edge.
|
|
35
|
+
- [Menu](https://cornui.com/components/menu) — Dropdown menu anchored to a trigger, with keyboard navigation (arrows/Home/End/Escape) and menu roles. Vanilla takes an items array; React composes MenuItem/MenuSeparator children.
|
|
36
|
+
|
|
37
|
+
## Documentation
|
|
38
|
+
|
|
39
|
+
- [Component documentation](https://cornui.com/components/modal)
|
|
40
|
+
- [Installing](https://cornui.com/installation)
|
|
41
|
+
- [Theming](https://cornui.com/theming)
|
|
42
|
+
- [llms.txt](https://cornui.com/llms.txt) — the whole API in one file, for coding agents
|
|
43
|
+
|
|
44
|
+
## Licence
|
|
45
|
+
|
|
46
|
+
MIT © Corndog Development LLC. See [LICENSE](https://gitlab.com/corn-dev/corn-ui/-/blob/master/LICENSE).
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/* === Accordion Container === */
|
|
2
|
+
.corn-accordion {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
border: 1px solid var(--corn-border-light);
|
|
6
|
+
border-radius: var(--corn-radius-md);
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/* === Trigger (header button) === */
|
|
11
|
+
.corn-accordion-trigger {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: space-between;
|
|
15
|
+
width: 100%;
|
|
16
|
+
padding: var(--corn-spacing-md) var(--corn-spacing-lg);
|
|
17
|
+
background: var(--corn-bg-primary);
|
|
18
|
+
border: none;
|
|
19
|
+
border-bottom: 1px solid var(--corn-border-light);
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
font-family: var(--corn-font-family);
|
|
22
|
+
font-size: var(--corn-font-size-base);
|
|
23
|
+
font-weight: var(--corn-font-weight-medium);
|
|
24
|
+
color: var(--corn-text-primary);
|
|
25
|
+
text-align: left;
|
|
26
|
+
line-height: var(--corn-line-height);
|
|
27
|
+
transition: background-color var(--corn-transition-fast);
|
|
28
|
+
gap: var(--corn-spacing-sm);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.corn-accordion-trigger:hover {
|
|
32
|
+
background: var(--corn-bg-secondary);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.corn-accordion-trigger:focus-visible {
|
|
36
|
+
outline: 2px solid var(--corn-primary);
|
|
37
|
+
outline-offset: -2px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Last trigger gets no border when closed */
|
|
41
|
+
.corn-accordion-trigger:last-of-type {
|
|
42
|
+
border-bottom: none;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Chevron icon via ::after */
|
|
46
|
+
.corn-accordion-trigger::after {
|
|
47
|
+
content: '';
|
|
48
|
+
display: inline-block;
|
|
49
|
+
width: 8px;
|
|
50
|
+
height: 8px;
|
|
51
|
+
border-right: 2px solid currentColor;
|
|
52
|
+
border-bottom: 2px solid currentColor;
|
|
53
|
+
transform: rotate(45deg);
|
|
54
|
+
transition: transform var(--corn-transition-base);
|
|
55
|
+
flex-shrink: 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.corn-accordion-trigger[data-open]::after {
|
|
59
|
+
transform: rotate(-135deg);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* === Item ===
|
|
63
|
+
The wrapper around one trigger and its panel — React renders one per item,
|
|
64
|
+
and the vanilla controller marks one when the markup has it. Wrapped, each
|
|
65
|
+
trigger is the last button in its own wrapper, so the `:last-of-type` rule
|
|
66
|
+
above took every divider away: an accordion of closed items had no lines
|
|
67
|
+
between them at all. The divider belongs to the item instead. */
|
|
68
|
+
.corn-accordion-item {
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.corn-accordion-item + .corn-accordion-item {
|
|
74
|
+
border-top: 1px solid var(--corn-border-light);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* An open item's trigger is set off from its own panel. */
|
|
78
|
+
.corn-accordion-item > .corn-accordion-trigger[data-open] {
|
|
79
|
+
border-bottom: 1px solid var(--corn-border-light);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* === Panel (content area) === */
|
|
83
|
+
.corn-accordion-panel {
|
|
84
|
+
padding: var(--corn-spacing-md) var(--corn-spacing-lg);
|
|
85
|
+
border-bottom: 1px solid var(--corn-border-light);
|
|
86
|
+
background: var(--corn-bg-primary);
|
|
87
|
+
font-family: var(--corn-font-family);
|
|
88
|
+
font-size: var(--corn-font-size-base);
|
|
89
|
+
color: var(--corn-text-primary);
|
|
90
|
+
line-height: var(--corn-line-height);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.corn-accordion-panel:last-child {
|
|
94
|
+
border-bottom: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* === Dark Mode === */
|
|
98
|
+
[data-theme="dark"] .corn-accordion {
|
|
99
|
+
border-color: var(--corn-border-light);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
[data-theme="dark"] .corn-accordion-trigger,
|
|
103
|
+
[data-theme="dark"] .corn-accordion-panel {
|
|
104
|
+
background: var(--corn-bg-primary);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
[data-theme="dark"] .corn-accordion-trigger:hover {
|
|
108
|
+
background: var(--corn-bg-secondary);
|
|
109
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates and manages an accordion component.
|
|
3
|
+
*
|
|
4
|
+
* @param {HTMLElement} containerEl - The wrapper element containing accordion items.
|
|
5
|
+
* Each item must have:
|
|
6
|
+
* - A trigger: `[data-accordion-trigger]` with a matching `data-accordion-item` value
|
|
7
|
+
* - A panel: `[data-accordion-panel]` with a matching `data-accordion-item` value
|
|
8
|
+
* A nested accordion must carry `class="corn-accordion"` on its own wrapper.
|
|
9
|
+
* That is how each one tells its items from a nested one's — CSS-first markup
|
|
10
|
+
* already has it, since the styles do not work without it.
|
|
11
|
+
*
|
|
12
|
+
* @param {Object} options
|
|
13
|
+
* @param {boolean} [options.multiple=false] - Allow multiple panels open at once
|
|
14
|
+
* @param {string|string[]} [options.defaultOpen] - Item key(s) open on init
|
|
15
|
+
* @param {(panel: HTMLElement, key: string) => void} [options.onExpand] Called
|
|
16
|
+
* the **first** time each panel opens, with the panel element, before
|
|
17
|
+
* `onChange`. Ship a collapsed section with an empty panel and fill it here.
|
|
18
|
+
*
|
|
19
|
+
* This is what makes a large document survivable: a 283-path OpenAPI
|
|
20
|
+
* reference built 112,308 elements and 16MB of markup when every closed panel
|
|
21
|
+
* was constructed up front, and froze the window. Once per key, not once per
|
|
22
|
+
* open — reopening a branch must not rebuild it.
|
|
23
|
+
*
|
|
24
|
+
* Same contract as `createNavigation`'s `onExpand(item)` and
|
|
25
|
+
* `createTreeState`'s, so one "render these children" function serves
|
|
26
|
+
* navigation, tree and content alike.
|
|
27
|
+
* @param {Function} [options.onChange] - Called with (key, isOpen) on toggle,
|
|
28
|
+
* after `onExpand`, so a listener sees a panel that is already filled.
|
|
29
|
+
* @returns {{ open: Function, close: Function, toggle: Function, destroy: Function }}
|
|
30
|
+
*/
|
|
31
|
+
export function createAccordion(containerEl: HTMLElement, options?: {
|
|
32
|
+
multiple?: boolean;
|
|
33
|
+
defaultOpen?: string | string[];
|
|
34
|
+
onExpand?: (panel: HTMLElement, key: string) => void;
|
|
35
|
+
onChange?: Function;
|
|
36
|
+
}): {
|
|
37
|
+
open: Function;
|
|
38
|
+
close: Function;
|
|
39
|
+
toggle: Function;
|
|
40
|
+
destroy: Function;
|
|
41
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/* === Drawer Backdrop === */
|
|
2
|
+
.corn-drawer-backdrop {
|
|
3
|
+
position: fixed;
|
|
4
|
+
inset: 0;
|
|
5
|
+
z-index: 1000;
|
|
6
|
+
background-color: transparent;
|
|
7
|
+
transition: background-color var(--corn-transition-base);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.corn-drawer-backdrop[data-visible] {
|
|
11
|
+
background-color: var(--corn-backdrop);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* === Drawer Panel === */
|
|
15
|
+
.corn-drawer {
|
|
16
|
+
position: fixed;
|
|
17
|
+
top: 0;
|
|
18
|
+
bottom: 0;
|
|
19
|
+
z-index: 1001;
|
|
20
|
+
/* md's geometry, so the default size is the default — see
|
|
21
|
+
scripts/size-fallback.test.js. `sm` and `lg` still win on specificity. */
|
|
22
|
+
width: 400px;
|
|
23
|
+
background: var(--corn-bg-primary);
|
|
24
|
+
box-shadow: var(--corn-shadow-lg);
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
transition: transform var(--corn-transition-base);
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Sides */
|
|
32
|
+
.corn-drawer[data-side='right'] {
|
|
33
|
+
right: 0;
|
|
34
|
+
transform: translateX(100%);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.corn-drawer[data-side='left'] {
|
|
38
|
+
left: 0;
|
|
39
|
+
transform: translateX(-100%);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.corn-drawer[data-side='right'].corn-drawer[data-open],
|
|
43
|
+
.corn-drawer[data-side='left'].corn-drawer[data-open] {
|
|
44
|
+
transform: translateX(0);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* Sizes */
|
|
48
|
+
.corn-drawer[data-size='sm'] { width: 280px; }
|
|
49
|
+
.corn-drawer[data-size='md'] { width: 400px; }
|
|
50
|
+
.corn-drawer[data-size='lg'] { width: 560px; }
|
|
51
|
+
|
|
52
|
+
/* === Header === */
|
|
53
|
+
.corn-drawer-header {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: space-between;
|
|
57
|
+
padding: var(--corn-spacing-md) var(--corn-spacing-lg);
|
|
58
|
+
border-bottom: 1px solid var(--corn-border-light);
|
|
59
|
+
flex-shrink: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.corn-drawer-title {
|
|
63
|
+
font-family: var(--corn-font-family);
|
|
64
|
+
font-size: var(--corn-font-size-lg);
|
|
65
|
+
font-weight: var(--corn-font-weight-bold);
|
|
66
|
+
color: var(--corn-text-primary);
|
|
67
|
+
margin: 0;
|
|
68
|
+
line-height: var(--corn-line-height);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.corn-drawer-close {
|
|
72
|
+
background: none;
|
|
73
|
+
border: none;
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
color: var(--corn-text-secondary);
|
|
76
|
+
font-size: var(--corn-font-size-lg);
|
|
77
|
+
line-height: 1;
|
|
78
|
+
padding: var(--corn-spacing-xs);
|
|
79
|
+
border-radius: var(--corn-radius-sm);
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
transition: color var(--corn-transition-fast), background-color var(--corn-transition-fast);
|
|
84
|
+
flex-shrink: 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.corn-drawer-close:hover {
|
|
88
|
+
color: var(--corn-text-primary);
|
|
89
|
+
background-color: var(--corn-bg-tertiary);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* === Body === */
|
|
93
|
+
.corn-drawer-body {
|
|
94
|
+
padding: var(--corn-spacing-lg);
|
|
95
|
+
overflow-y: auto;
|
|
96
|
+
flex: 1;
|
|
97
|
+
font-family: var(--corn-font-family);
|
|
98
|
+
font-size: var(--corn-font-size-base);
|
|
99
|
+
color: var(--corn-text-primary);
|
|
100
|
+
line-height: var(--corn-line-height);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* === Footer === */
|
|
104
|
+
.corn-drawer-footer {
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: flex-end;
|
|
108
|
+
gap: var(--corn-spacing-sm);
|
|
109
|
+
padding: var(--corn-spacing-md) var(--corn-spacing-lg);
|
|
110
|
+
border-top: 1px solid var(--corn-border-light);
|
|
111
|
+
flex-shrink: 0;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* === Body scroll lock === */
|
|
115
|
+
body.corn-drawer-open {
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates and manages a slide-in drawer panel.
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} options
|
|
5
|
+
* @param {string} [options.title] - Drawer heading text
|
|
6
|
+
* @param {string|HTMLElement} [options.content] - Body content. A string is shown as text, never parsed as HTML; pass an element for markup.
|
|
7
|
+
* @param {HTMLElement} [options.footer] - Footer element
|
|
8
|
+
* @param {'left'|'right'} [options.side='right'] - Which side the drawer slides from
|
|
9
|
+
* @param {'sm'|'md'|'lg'} [options.size='md'] - Drawer width
|
|
10
|
+
* @param {boolean} [options.closeable=true] - Allow backdrop/esc/close-button to dismiss
|
|
11
|
+
* @param {Function} [options.onOpen] - Called when drawer opens
|
|
12
|
+
* @param {Function} [options.onClose] - Called when drawer closes
|
|
13
|
+
* @returns {{ open: Function, close: Function, destroy: Function, el: HTMLElement }}
|
|
14
|
+
*/
|
|
15
|
+
export function createDrawer(options?: {
|
|
16
|
+
title?: string;
|
|
17
|
+
content?: string | HTMLElement;
|
|
18
|
+
footer?: HTMLElement;
|
|
19
|
+
side?: "left" | "right";
|
|
20
|
+
size?: "sm" | "md" | "lg";
|
|
21
|
+
closeable?: boolean;
|
|
22
|
+
onOpen?: Function;
|
|
23
|
+
onClose?: Function;
|
|
24
|
+
}): {
|
|
25
|
+
open: Function;
|
|
26
|
+
close: Function;
|
|
27
|
+
destroy: Function;
|
|
28
|
+
el: HTMLElement;
|
|
29
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
.corn-menu {
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 8000;
|
|
4
|
+
min-width: 180px;
|
|
5
|
+
max-width: 320px;
|
|
6
|
+
padding: var(--corn-spacing-xs);
|
|
7
|
+
background-color: var(--corn-bg-primary);
|
|
8
|
+
border: 1px solid var(--corn-border-light);
|
|
9
|
+
border-radius: var(--corn-radius-md);
|
|
10
|
+
box-shadow: var(--corn-shadow-lg);
|
|
11
|
+
font-family: var(--corn-font-family);
|
|
12
|
+
font-size: var(--corn-font-size-base);
|
|
13
|
+
color: var(--corn-text-primary);
|
|
14
|
+
opacity: 0;
|
|
15
|
+
transform: scale(0.97);
|
|
16
|
+
transform-origin: top left;
|
|
17
|
+
transition: opacity var(--corn-transition-fast), transform var(--corn-transition-fast);
|
|
18
|
+
pointer-events: none;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.corn-menu[data-placement='top'] { transform-origin: bottom left; }
|
|
22
|
+
|
|
23
|
+
/* Closed is out of the accessibility tree, not just transparent: the vanilla
|
|
24
|
+
menu stays in the document between openings, and at opacity 0 a screen
|
|
25
|
+
reader's virtual cursor still reached its items — and could activate them.
|
|
26
|
+
Same pattern as .corn-popover, so the fade still plays: visibility holds
|
|
27
|
+
for the whole transition and flips at the end. */
|
|
28
|
+
.corn-menu:not([data-visible]) {
|
|
29
|
+
visibility: hidden;
|
|
30
|
+
transition:
|
|
31
|
+
opacity var(--corn-transition-fast),
|
|
32
|
+
transform var(--corn-transition-fast),
|
|
33
|
+
visibility var(--corn-transition-fast);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.corn-menu[data-visible] {
|
|
37
|
+
opacity: 1;
|
|
38
|
+
transform: scale(1);
|
|
39
|
+
pointer-events: auto;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* ── Items ──────────────────────────────────────────────────────────────────── */
|
|
43
|
+
|
|
44
|
+
.corn-menu-item {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: var(--corn-spacing-sm);
|
|
48
|
+
width: 100%;
|
|
49
|
+
padding: var(--corn-spacing-sm) var(--corn-spacing-md);
|
|
50
|
+
background: none;
|
|
51
|
+
border: none;
|
|
52
|
+
border-radius: var(--corn-radius-sm);
|
|
53
|
+
font: inherit;
|
|
54
|
+
color: var(--corn-text-primary);
|
|
55
|
+
text-align: left;
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
transition: background-color var(--corn-transition-fast);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.corn-menu-item:hover:not(:disabled),
|
|
61
|
+
.corn-menu-item:focus-visible {
|
|
62
|
+
outline: none;
|
|
63
|
+
background-color: var(--corn-bg-tertiary);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.corn-menu-item:disabled {
|
|
67
|
+
opacity: 0.5;
|
|
68
|
+
cursor: not-allowed;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* A destructive item. The label says what it does; the colour backs it up. */
|
|
72
|
+
.corn-menu-item[data-danger] {
|
|
73
|
+
color: var(--corn-danger);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.corn-menu-item[data-danger]:hover:not(:disabled),
|
|
77
|
+
.corn-menu-item[data-danger]:focus-visible {
|
|
78
|
+
background-color: var(--corn-danger-light);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ── Groups ─────────────────────────────────────────────────────────────────── */
|
|
82
|
+
|
|
83
|
+
/* A group after other entries is set off by a little space, so its heading
|
|
84
|
+
reads as starting something rather than belonging to the item above it. */
|
|
85
|
+
.corn-menu-group:not(:first-child) {
|
|
86
|
+
margin-top: var(--corn-spacing-xs);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* A heading, not an item: no role of its own, so the arrow keys pass over it,
|
|
90
|
+
and the group it names is announced by it. */
|
|
91
|
+
.corn-menu-label {
|
|
92
|
+
padding: var(--corn-spacing-xs) var(--corn-spacing-md);
|
|
93
|
+
font-size: var(--corn-font-size-xs);
|
|
94
|
+
font-weight: var(--corn-font-weight-medium);
|
|
95
|
+
color: var(--corn-text-secondary);
|
|
96
|
+
cursor: default;
|
|
97
|
+
user-select: none;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/* ── Separator ──────────────────────────────────────────────────────────────── */
|
|
101
|
+
|
|
102
|
+
.corn-menu-separator {
|
|
103
|
+
height: 1px;
|
|
104
|
+
margin: var(--corn-spacing-xs) 0;
|
|
105
|
+
background-color: var(--corn-border-light);
|
|
106
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a dropdown menu anchored to a trigger element, with full keyboard
|
|
3
|
+
* navigation (arrows, Home/End, Escape) and `aria-haspopup`/menu roles.
|
|
4
|
+
*
|
|
5
|
+
* @param {HTMLElement} triggerEl
|
|
6
|
+
* @param {MenuOptions} options
|
|
7
|
+
* @returns {{ open: Function, close: Function, toggle: Function, destroy: Function }}
|
|
8
|
+
*/
|
|
9
|
+
export function createMenu(triggerEl: HTMLElement, options?: MenuOptions): {
|
|
10
|
+
open: Function;
|
|
11
|
+
close: Function;
|
|
12
|
+
toggle: Function;
|
|
13
|
+
destroy: Function;
|
|
14
|
+
};
|
|
15
|
+
export type MenuItemSpec = {
|
|
16
|
+
/**
|
|
17
|
+
* Visible text (ignored when `separator` is true).
|
|
18
|
+
*/
|
|
19
|
+
label: string;
|
|
20
|
+
/**
|
|
21
|
+
* Value passed to `onSelect`.
|
|
22
|
+
*/
|
|
23
|
+
value?: any;
|
|
24
|
+
/**
|
|
25
|
+
* Render the item non-interactive.
|
|
26
|
+
*/
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Render a divider instead of an item.
|
|
30
|
+
*/
|
|
31
|
+
separator?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* A destructive action — "Delete", "Revoke".
|
|
34
|
+
* Coloured as danger; the label is what says so, so keep it a verb that does.
|
|
35
|
+
*/
|
|
36
|
+
danger?: boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Per-item select handler.
|
|
39
|
+
*/
|
|
40
|
+
onSelect?: (value: any, item: MenuItemSpec) => void;
|
|
41
|
+
};
|
|
42
|
+
export type MenuGroupSpec = {
|
|
43
|
+
/**
|
|
44
|
+
* The heading. Not an item — the arrow keys pass over it —
|
|
45
|
+
* and it names the group for a screen reader.
|
|
46
|
+
*/
|
|
47
|
+
group: string;
|
|
48
|
+
/**
|
|
49
|
+
* The entries under it.
|
|
50
|
+
*/
|
|
51
|
+
items: MenuItemSpec[];
|
|
52
|
+
};
|
|
53
|
+
export type MenuOptions = {
|
|
54
|
+
/**
|
|
55
|
+
* Menu entries. An entry with
|
|
56
|
+
* `group` and `items` is a labelled group — the same shape as `options` elsewhere.
|
|
57
|
+
*/
|
|
58
|
+
items: Array<MenuItemSpec | MenuGroupSpec>;
|
|
59
|
+
/**
|
|
60
|
+
* Preferred placement (flips if no room).
|
|
61
|
+
*/
|
|
62
|
+
placement?: "bottom" | "top";
|
|
63
|
+
/**
|
|
64
|
+
* Close the menu after selecting an item.
|
|
65
|
+
*/
|
|
66
|
+
closeOnSelect?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Called when any item is selected.
|
|
69
|
+
*/
|
|
70
|
+
onSelect?: (value: any, item: MenuItemSpec) => void;
|
|
71
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/* === Modal Backdrop === */
|
|
2
|
+
.corn-modal-backdrop {
|
|
3
|
+
position: fixed;
|
|
4
|
+
inset: 0;
|
|
5
|
+
z-index: 1000;
|
|
6
|
+
background-color: transparent;
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
padding: var(--corn-spacing-md);
|
|
11
|
+
transition: background-color var(--corn-transition-base);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.corn-modal-backdrop[data-visible] {
|
|
15
|
+
background-color: var(--corn-backdrop);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* === Modal Dialog === */
|
|
19
|
+
.corn-modal {
|
|
20
|
+
background: var(--corn-bg-primary);
|
|
21
|
+
border-radius: var(--corn-radius-md);
|
|
22
|
+
box-shadow: var(--corn-shadow-lg);
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
max-height: calc(100vh - var(--corn-spacing-xl) * 2);
|
|
26
|
+
width: 100%;
|
|
27
|
+
opacity: 0;
|
|
28
|
+
transform: translateY(-8px) scale(0.98);
|
|
29
|
+
transition:
|
|
30
|
+
opacity var(--corn-transition-base),
|
|
31
|
+
transform var(--corn-transition-base);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.corn-modal[data-visible] {
|
|
35
|
+
opacity: 1;
|
|
36
|
+
transform: translateY(0) scale(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/* Sizes */
|
|
40
|
+
.corn-modal[data-size='sm'] { max-width: 400px; }
|
|
41
|
+
.corn-modal[data-size='md'] { max-width: 560px; }
|
|
42
|
+
.corn-modal[data-size='lg'] { max-width: 720px; }
|
|
43
|
+
.corn-modal[data-size='xl'] { max-width: 960px; }
|
|
44
|
+
.corn-modal[data-size='fullscreen'] {
|
|
45
|
+
max-width: 100%;
|
|
46
|
+
max-height: 100%;
|
|
47
|
+
height: 100%;
|
|
48
|
+
border-radius: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* === Header === */
|
|
52
|
+
.corn-modal-header {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: space-between;
|
|
56
|
+
padding: var(--corn-spacing-md) var(--corn-spacing-lg);
|
|
57
|
+
border-bottom: 1px solid var(--corn-border-light);
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.corn-modal-title {
|
|
62
|
+
font-family: var(--corn-font-family);
|
|
63
|
+
font-size: var(--corn-font-size-lg);
|
|
64
|
+
font-weight: var(--corn-font-weight-bold);
|
|
65
|
+
color: var(--corn-text-primary);
|
|
66
|
+
margin: 0;
|
|
67
|
+
line-height: var(--corn-line-height);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.corn-modal-close {
|
|
71
|
+
background: none;
|
|
72
|
+
border: none;
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
color: var(--corn-text-secondary);
|
|
75
|
+
font-size: var(--corn-font-size-lg);
|
|
76
|
+
line-height: 1;
|
|
77
|
+
padding: var(--corn-spacing-xs);
|
|
78
|
+
border-radius: var(--corn-radius-sm);
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
transition: color var(--corn-transition-fast), background-color var(--corn-transition-fast);
|
|
83
|
+
flex-shrink: 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.corn-modal-close:hover {
|
|
87
|
+
color: var(--corn-text-primary);
|
|
88
|
+
background-color: var(--corn-bg-tertiary);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* === Body === */
|
|
92
|
+
.corn-modal-body {
|
|
93
|
+
padding: var(--corn-spacing-lg);
|
|
94
|
+
overflow-y: auto;
|
|
95
|
+
flex: 1;
|
|
96
|
+
font-family: var(--corn-font-family);
|
|
97
|
+
font-size: var(--corn-font-size-base);
|
|
98
|
+
color: var(--corn-text-primary);
|
|
99
|
+
line-height: var(--corn-line-height);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* === Footer === */
|
|
103
|
+
.corn-modal-footer {
|
|
104
|
+
display: flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: flex-end;
|
|
107
|
+
gap: var(--corn-spacing-sm);
|
|
108
|
+
padding: var(--corn-spacing-md) var(--corn-spacing-lg);
|
|
109
|
+
border-top: 1px solid var(--corn-border-light);
|
|
110
|
+
flex-shrink: 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* === Body scroll lock === */
|
|
114
|
+
body.corn-modal-open {
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* === Dark Mode === */
|
|
119
|
+
[data-theme="dark"] .corn-modal {
|
|
120
|
+
background: var(--corn-bg-primary);
|
|
121
|
+
border: 1px solid var(--corn-border-light);
|
|
122
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates and manages a modal dialog.
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} options
|
|
5
|
+
* @param {string} [options.title] - Modal heading text
|
|
6
|
+
* @param {string|HTMLElement} [options.content] - Body content. A string is shown as text, never parsed as HTML; pass an element for markup.
|
|
7
|
+
* @param {HTMLElement} [options.footer] - Footer element (e.g. buttons)
|
|
8
|
+
* @param {'sm'|'md'|'lg'|'xl'|'fullscreen'} [options.size='md'] - Dialog width
|
|
9
|
+
* @param {boolean} [options.closeable=true] - Show close button and allow backdrop/esc close
|
|
10
|
+
* @param {Function} [options.onOpen] - Called when modal opens
|
|
11
|
+
* @param {Function} [options.onClose] - Called when modal closes
|
|
12
|
+
* @returns {{ open: Function, close: Function, destroy: Function, el: HTMLElement }}
|
|
13
|
+
*/
|
|
14
|
+
export function createModal(options?: {
|
|
15
|
+
title?: string;
|
|
16
|
+
content?: string | HTMLElement;
|
|
17
|
+
footer?: HTMLElement;
|
|
18
|
+
size?: "sm" | "md" | "lg" | "xl" | "fullscreen";
|
|
19
|
+
closeable?: boolean;
|
|
20
|
+
onOpen?: Function;
|
|
21
|
+
onClose?: Function;
|
|
22
|
+
}): {
|
|
23
|
+
open: Function;
|
|
24
|
+
close: Function;
|
|
25
|
+
destroy: Function;
|
|
26
|
+
el: HTMLElement;
|
|
27
|
+
};
|