@iyulab/modern-app 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +30 -0
- package/dist/components/MasterDetailLayout.d.ts +14 -0
- package/dist/components/SidebarButton.d.ts +6 -3
- package/dist/components/SidebarButton.js +11 -5
- package/dist/components/Wizard.d.ts +14 -0
- package/dist/index.d.ts +4 -2
- package/dist/layouts/SidebarLayout.d.ts +32 -0
- package/dist/layouts/SidebarLayout.js +81 -36
- package/dist/layouts/SidebarLayout.types.d.ts +28 -2
- package/dist/layouts/filterSidebarItems.d.ts +1 -0
- package/dist/layouts/filterSidebarItems.js +7 -0
- package/dist/react.d.ts +5 -5
- package/package.json +1 -1
- package/skills/modern-app/references/layout.md +57 -32
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.36.0] - 2026-10-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`type: 'menu'` sidebar items — a button that opens a popup menu.** `{ type: 'menu', label, icon,
|
|
8
|
+
items: [{ label, icon, onClick, disabled }] }`. The shell draws the trigger and the menu together and
|
|
9
|
+
owns what only it knows: the menu opens beside the sidebar and downwards on the full-width mobile
|
|
10
|
+
panel; the trigger carries `aria-haspopup="menu"` and `aria-expanded`; opening moves focus into the
|
|
11
|
+
menu, and Escape or choosing an entry closes it and returns focus to the trigger. Entries take
|
|
12
|
+
`requirePermission`/`requireAnyPermission`, and a menu left with no entries is dropped.
|
|
13
|
+
- The sidebar config types (`SidebarItem`, `SidebarLinkConfig`, `SidebarMenuConfig`, …, `SidebarState`)
|
|
14
|
+
are exported from the main entry too — they were only on `/react`, so a TypeScript app without React
|
|
15
|
+
could not name them.
|
|
16
|
+
|
|
17
|
+
### Removed (breaking)
|
|
18
|
+
|
|
19
|
+
- **`SidebarButtonConfig.id`.** It was meant for anchoring a `u-popover` you place outside the layout,
|
|
20
|
+
but the button renders inside the layout's shadow root, where `for="#id"` never reaches — it never
|
|
21
|
+
worked. Use `type: 'menu'` for a menu, or `type: 'html'` with the trigger and the popover in one
|
|
22
|
+
template for any other flyout.
|
|
23
|
+
|
|
24
|
+
## [0.35.1] - 2026-10-06
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- **`u-wizard`'s `step-change`, `u-master-detail-layout`'s `detail-close` and `u-sidebar-layout`'s
|
|
29
|
+
`overlay-close` are typed for TypeScript listeners** (`WizardEventMap`, `MasterDetailLayoutEventMap`,
|
|
30
|
+
`SidebarLayoutEventMap`, exported). `wizard.addEventListener('step-change', (e) => e.detail.to)` no
|
|
31
|
+
longer needs a cast; the React `onStepChange`/`onOverlayClose` props use the same types.
|
|
32
|
+
|
|
3
33
|
## [0.35.0] - 2026-10-06
|
|
4
34
|
|
|
5
35
|
### Changed
|
|
@@ -86,6 +86,20 @@ export declare class MasterDetailLayout extends StyledElement<ElementParts> {
|
|
|
86
86
|
private handleDetailSlotChange;
|
|
87
87
|
render(): import('lit-html').TemplateResult<1>;
|
|
88
88
|
}
|
|
89
|
+
/** Events `<u-master-detail-layout>` dispatches — bubbling and composed. */
|
|
90
|
+
export interface MasterDetailLayoutEventMap {
|
|
91
|
+
/** The detail pane's close button was pressed (narrow width, overlay mode). Clearing the `detail` slot is yours. */
|
|
92
|
+
'detail-close': CustomEvent<null>;
|
|
93
|
+
}
|
|
94
|
+
/** Typed listeners for {@link MasterDetailLayoutEventMap} — element-scoped, the DOM's own pattern (`HTMLMediaElementEventMap`). */
|
|
95
|
+
export interface MasterDetailLayout {
|
|
96
|
+
addEventListener<K extends keyof MasterDetailLayoutEventMap>(type: K, listener: (this: MasterDetailLayout, ev: MasterDetailLayoutEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
97
|
+
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: MasterDetailLayout, ev: HTMLElementEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
98
|
+
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
99
|
+
removeEventListener<K extends keyof MasterDetailLayoutEventMap>(type: K, listener: (this: MasterDetailLayout, ev: MasterDetailLayoutEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
|
|
100
|
+
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: MasterDetailLayout, ev: HTMLElementEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
|
|
101
|
+
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
102
|
+
}
|
|
89
103
|
declare global {
|
|
90
104
|
interface HTMLElementTagNameMap {
|
|
91
105
|
'u-master-detail-layout': MasterDetailLayout;
|
|
@@ -6,9 +6,6 @@ type ElementParts = 'host' | 'base' | 'icon' | 'label';
|
|
|
6
6
|
/** 버튼 항목 구성 */
|
|
7
7
|
export interface SidebarButtonConfig extends SidebarPermissionGuard {
|
|
8
8
|
type: 'button';
|
|
9
|
-
/** 렌더된 `<u-sidebar-button>` 호스트에 그대로 전달된다 — `u-popover[for="#id"]` 같은
|
|
10
|
-
* 외부 앵커링에 쓴다. */
|
|
11
|
-
id?: string;
|
|
12
9
|
icon?: string;
|
|
13
10
|
/** `icon`을 어느 등록 라이브러리에서 찾을지. 미지정 시 `u-icon`의 기본 URL 경로로
|
|
14
11
|
* 해석된다(`IconRegistry.register()`로 등록한 이름 있는 세트를 쓰려면 지정해야 함). */
|
|
@@ -30,6 +27,12 @@ export declare class SidebarButton extends StyledElement<ElementParts> {
|
|
|
30
27
|
lib?: string;
|
|
31
28
|
/** 버튼 텍스트 라벨 */
|
|
32
29
|
label?: string | DirectiveResult;
|
|
30
|
+
/** 이 버튼이 여는 팝업의 종류 — 안쪽 `<button>` 의 `aria-haspopup` 으로 간다(셸의 `type: 'menu'` 트리거). */
|
|
31
|
+
haspopup?: 'menu';
|
|
32
|
+
/** 팝업이 열려 있는가 — `haspopup` 과 함께 `aria-expanded` 로 간다. 팝업이 없는 버튼에는 붙이지 않는다. */
|
|
33
|
+
expanded: boolean;
|
|
34
|
+
/** 포커스는 안쪽 `<button>` 으로 — 호스트는 포커스를 받지 않는다(팝업을 닫을 때 셸이 이것으로 되돌린다). */
|
|
35
|
+
focus(options?: FocusOptions): void;
|
|
33
36
|
render(): import('lit-html').TemplateResult<1>;
|
|
34
37
|
}
|
|
35
38
|
export {};
|
|
@@ -6,17 +6,23 @@ import { styles as i } from "./SidebarButton.styles.js";
|
|
|
6
6
|
import { html as a } from "lit";
|
|
7
7
|
import { customElement as o, property as s } from "lit/decorators.js";
|
|
8
8
|
import "@iyulab/components/dist/components/icon/UIcon.js";
|
|
9
|
+
import { ifDefined as c } from "lit/directives/if-defined.js";
|
|
9
10
|
//#region src/components/SidebarButton.ts
|
|
10
|
-
var
|
|
11
|
+
var l = class extends n {
|
|
11
12
|
constructor(...e) {
|
|
12
|
-
super(...e), this.compact = !1;
|
|
13
|
+
super(...e), this.compact = !1, this.expanded = !1;
|
|
13
14
|
}
|
|
14
15
|
static {
|
|
15
16
|
this.styles = [super.styles, i];
|
|
16
17
|
}
|
|
18
|
+
focus(e) {
|
|
19
|
+
this.renderRoot.querySelector("button")?.focus(e);
|
|
20
|
+
}
|
|
17
21
|
render() {
|
|
18
22
|
return a`
|
|
19
|
-
<button part="base" ?compact=${this.compact}
|
|
23
|
+
<button part="base" ?compact=${this.compact}
|
|
24
|
+
aria-haspopup=${c(this.haspopup)}
|
|
25
|
+
aria-expanded=${c(this.haspopup ? String(this.expanded) : void 0)}>
|
|
20
26
|
<u-icon part="icon"
|
|
21
27
|
.lib=${this.lib}
|
|
22
28
|
.name=${this.icon}
|
|
@@ -32,6 +38,6 @@ var c = class extends n {
|
|
|
32
38
|
t([s({
|
|
33
39
|
type: Boolean,
|
|
34
40
|
reflect: !0
|
|
35
|
-
}), e("design:type", Object)],
|
|
41
|
+
}), e("design:type", Object)], l.prototype, "compact", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "icon", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "lib", void 0), t([s({ type: String }), e("design:type", Object)], l.prototype, "label", void 0), t([s({ type: String }), e("design:type", String)], l.prototype, "haspopup", void 0), t([s({ type: Boolean }), e("design:type", Object)], l.prototype, "expanded", void 0), l = t([o("u-sidebar-button")], l);
|
|
36
42
|
//#endregion
|
|
37
|
-
export {
|
|
43
|
+
export { l as SidebarButton };
|
|
@@ -89,6 +89,20 @@ export declare class Wizard extends StyledElement<ElementParts> {
|
|
|
89
89
|
private renderStep;
|
|
90
90
|
render(): import('lit-html').TemplateResult<1>;
|
|
91
91
|
}
|
|
92
|
+
/** Events `<u-wizard>` dispatches — bubbling and composed. */
|
|
93
|
+
export interface WizardEventMap {
|
|
94
|
+
/** Cancelable: `preventDefault()` keeps the wizard on `from` (validation, unsaved work). */
|
|
95
|
+
'step-change': CustomEvent<WizardStepChangeDetail>;
|
|
96
|
+
}
|
|
97
|
+
/** Typed listeners for {@link WizardEventMap} — element-scoped, the DOM's own pattern (`HTMLMediaElementEventMap`). */
|
|
98
|
+
export interface Wizard {
|
|
99
|
+
addEventListener<K extends keyof WizardEventMap>(type: K, listener: (this: Wizard, ev: WizardEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
100
|
+
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: Wizard, ev: HTMLElementEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
101
|
+
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
102
|
+
removeEventListener<K extends keyof WizardEventMap>(type: K, listener: (this: Wizard, ev: WizardEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
|
|
103
|
+
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: Wizard, ev: HTMLElementEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
|
|
104
|
+
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
105
|
+
}
|
|
92
106
|
declare global {
|
|
93
107
|
interface HTMLElementTagNameMap {
|
|
94
108
|
'u-wizard': Wizard;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ export type * from './types/AppConfigs';
|
|
|
3
3
|
export type * from './types/AppOptions';
|
|
4
4
|
export type * from './types/AuthConfig';
|
|
5
5
|
export type { SidebarPermissionGuard } from './layouts/SidebarPermission';
|
|
6
|
-
export type { SidebarIconsConfig } from './layouts/SidebarLayout.types';
|
|
6
|
+
export type { SidebarIconsConfig, SidebarLayoutConfig, SidebarItem, SidebarLogoConfig, SidebarLinkConfig, SidebarSectionConfig, SidebarGroupConfig, SidebarButtonConfig, SidebarMenuConfig, SidebarMenuItemConfig, SidebarHtmlConfig, SidebarState, } from './layouts/SidebarLayout.types';
|
|
7
|
+
export type { SidebarLayoutEventMap } from './layouts/SidebarLayout.js';
|
|
7
8
|
export { filterSidebarItems } from './layouts/filterSidebarItems.js';
|
|
8
9
|
export { PageHeader } from './components/PageHeader.js';
|
|
9
10
|
export { GroupBox } from './components/GroupBox.js';
|
|
@@ -12,8 +13,9 @@ export { InfoField, isBlank } from './components/InfoField.js';
|
|
|
12
13
|
export { EmptyState } from './components/EmptyState.js';
|
|
13
14
|
export { ActionBar } from './components/ActionBar.js';
|
|
14
15
|
export { MasterDetailLayout } from './components/MasterDetailLayout.js';
|
|
16
|
+
export type { MasterDetailLayoutEventMap } from './components/MasterDetailLayout.js';
|
|
15
17
|
export { Wizard } from './components/Wizard.js';
|
|
16
|
-
export type { WizardStep, WizardStepState, WizardStepChangeDetail } from './components/Wizard.js';
|
|
18
|
+
export type { WizardStep, WizardStepState, WizardStepChangeDetail, WizardEventMap } from './components/Wizard.js';
|
|
17
19
|
export { translate } from './translate.js';
|
|
18
20
|
export { modernAppLocale, registerLocale, setDefaultLocale, getLocaleStrings, getDefaultLocale, } from './internals/locale.js';
|
|
19
21
|
export type { ModernAppLocaleStrings, ModernAppMessageKey } from './internals/locale.js';
|
|
@@ -19,6 +19,11 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
19
19
|
progressBarEl: UProgressBar;
|
|
20
20
|
/** overlay 슬롯 배정 상태 — CSS `:has()`로는 알 수 없다(`internals/slotted.ts` 참조). */
|
|
21
21
|
private hasOverlay;
|
|
22
|
+
/** 열린 `type: 'menu'` 팝업의 트리거 id — 트리거의 `aria-expanded` 가 읽는다. */
|
|
23
|
+
private openMenu;
|
|
24
|
+
/** `type: 'menu'` 항목마다 고정된 트리거 id — 팝오버의 `for` 가 같은 섀도 루트 안에서 찾는다. */
|
|
25
|
+
private readonly menuIds;
|
|
26
|
+
private static menuSeq;
|
|
22
27
|
/** notice 슬롯 배정 상태 — 비었으면 스택 자체가 자리를 차지하지 않는다. */
|
|
23
28
|
private hasNotice;
|
|
24
29
|
/**
|
|
@@ -57,6 +62,19 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
57
62
|
private readonly unsizedWarnKey;
|
|
58
63
|
private warnIfUnsized;
|
|
59
64
|
render(): typeof nothing | import('lit-html').TemplateResult<1>;
|
|
65
|
+
/**
|
|
66
|
+
* `type: 'menu'` — 트리거와 팝업을 **같은 섀도 루트** 에 함께 그린다(팝오버의 `for` 는 섀도 경계를 넘지 못한다 —
|
|
67
|
+
* 종전 `SidebarButtonConfig.id` 가 약속하고 지키지 못한 자리). 배치는 셸 상태가 정한다: 모바일 패널은 화면 폭을
|
|
68
|
+
* 거의 다 차지해 옆에 자리가 없으므로 아래로, 그 밖에는 사이드바 옆으로.
|
|
69
|
+
*/
|
|
70
|
+
private renderMenu;
|
|
71
|
+
/** 항목을 고르면 그 동작을 부르고 닫는다 — 포커스는 닫힘 처리(`handleMenuHide`)가 트리거로 되돌린다. */
|
|
72
|
+
private handleMenuPick;
|
|
73
|
+
/**
|
|
74
|
+
* 닫힐 때 포커스가 팝업 안에 있었다면 트리거로 돌린다(APG 메뉴 버튼 — Escape · 항목 선택). 바깥을 눌러 닫힌
|
|
75
|
+
* 경우에는 포커스가 이미 그 바깥 자리로 갔으므로 건드리지 않는다.
|
|
76
|
+
*/
|
|
77
|
+
private handleMenuHide;
|
|
60
78
|
/** 사이드바 아이템 렌더링 */
|
|
61
79
|
private renderItem;
|
|
62
80
|
/** 현재 경로와 패턴 매칭 여부 확인 */
|
|
@@ -156,6 +174,20 @@ export declare class SidebarLayout extends StyledElement<SidebarParts> {
|
|
|
156
174
|
/** 화면 크기 변경에 따른 사이드바 상태 업데이트 */
|
|
157
175
|
private handleScreenResize;
|
|
158
176
|
}
|
|
177
|
+
/** Events `<u-sidebar-layout>` dispatches — bubbling and composed. */
|
|
178
|
+
export interface SidebarLayoutEventMap {
|
|
179
|
+
/** The overlay's close button was pressed, or Escape with focus inside the panel. Emptying the `overlay` slot is yours. */
|
|
180
|
+
'overlay-close': CustomEvent<null>;
|
|
181
|
+
}
|
|
182
|
+
/** Typed listeners for {@link SidebarLayoutEventMap} — element-scoped, the DOM's own pattern (`HTMLMediaElementEventMap`). */
|
|
183
|
+
export interface SidebarLayout {
|
|
184
|
+
addEventListener<K extends keyof SidebarLayoutEventMap>(type: K, listener: (this: SidebarLayout, ev: SidebarLayoutEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
185
|
+
addEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: SidebarLayout, ev: HTMLElementEventMap[K]) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
186
|
+
addEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions): void;
|
|
187
|
+
removeEventListener<K extends keyof SidebarLayoutEventMap>(type: K, listener: (this: SidebarLayout, ev: SidebarLayoutEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
|
|
188
|
+
removeEventListener<K extends keyof HTMLElementEventMap>(type: K, listener: (this: SidebarLayout, ev: HTMLElementEventMap[K]) => unknown, options?: boolean | EventListenerOptions): void;
|
|
189
|
+
removeEventListener(type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions): void;
|
|
190
|
+
}
|
|
159
191
|
declare global {
|
|
160
192
|
interface HTMLElementTagNameMap {
|
|
161
193
|
'u-sidebar-layout': SidebarLayout;
|
|
@@ -17,20 +17,22 @@ import "@iyulab/components/dist/components/button/UButton.js";
|
|
|
17
17
|
import "@iyulab/components/dist/components/icon/UIcon.js";
|
|
18
18
|
import { unsafeHTML as h } from "lit/directives/unsafe-html.js";
|
|
19
19
|
import { repeat as g } from "lit/directives/repeat.js";
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
20
|
+
import "@iyulab/components/dist/components/popover/UPopover.js";
|
|
21
|
+
import "@iyulab/components/dist/components/menu/UMenu.js";
|
|
22
|
+
import "@iyulab/components/dist/components/menu-item/UMenuItem.js";
|
|
23
|
+
import { createDevWarner as _ } from "@iyulab/components/dist/utilities/devWarning.js";
|
|
24
|
+
import { isFocusCandidate as v, querySelectorDeep as y } from "@iyulab/components/dist/utilities/elements.js";
|
|
25
|
+
import { OverlayManager as b } from "@iyulab/components/dist/utilities/OverlayManager.js";
|
|
26
|
+
import { UProgressBar as x } from "@iyulab/components/dist/components/progress-bar/UProgressBar.js";
|
|
25
27
|
//#region src/layouts/SidebarLayout.ts
|
|
26
|
-
var
|
|
27
|
-
function
|
|
28
|
+
var S, C = _("@iyulab/modern-app"), w = 0;
|
|
29
|
+
function T(e) {
|
|
28
30
|
let t = e.tagName;
|
|
29
31
|
if (t === "INPUT" || t === "TEXTAREA" || t === "SELECT" || e.isContentEditable) return !0;
|
|
30
32
|
let n = e.getAttribute("role");
|
|
31
33
|
return n === "textbox" || n === "searchbox" || n === "combobox" || n === "spinbutton";
|
|
32
34
|
}
|
|
33
|
-
var
|
|
35
|
+
var E = /* @__PURE__ */ new Set([
|
|
34
36
|
"button",
|
|
35
37
|
"link",
|
|
36
38
|
"checkbox",
|
|
@@ -43,27 +45,27 @@ var D = /* @__PURE__ */ new Set([
|
|
|
43
45
|
"tab",
|
|
44
46
|
"treeitem"
|
|
45
47
|
]);
|
|
46
|
-
function
|
|
48
|
+
function D(e) {
|
|
47
49
|
let t = e.tagName;
|
|
48
|
-
return t === "BUTTON" || t === "SUMMARY" || t === "A" && e.hasAttribute("href") ? !0 :
|
|
50
|
+
return t === "BUTTON" || t === "SUMMARY" || t === "A" && e.hasAttribute("href") ? !0 : E.has(e.getAttribute("role") ?? "");
|
|
49
51
|
}
|
|
50
|
-
function
|
|
52
|
+
function O(e) {
|
|
51
53
|
let t = getComputedStyle(e);
|
|
52
54
|
return (t.overflowY === "auto" || t.overflowY === "scroll") && e.scrollHeight > e.clientHeight;
|
|
53
55
|
}
|
|
54
|
-
function
|
|
56
|
+
function k(e, t) {
|
|
55
57
|
for (let n of e) if (n instanceof HTMLElement) {
|
|
56
|
-
if (
|
|
58
|
+
if (O(n)) return n === t;
|
|
57
59
|
if (n === t) break;
|
|
58
60
|
}
|
|
59
61
|
return !0;
|
|
60
62
|
}
|
|
61
|
-
function
|
|
63
|
+
function A() {
|
|
62
64
|
let e = document.activeElement;
|
|
63
65
|
for (; e?.shadowRoot?.activeElement;) e = e.shadowRoot.activeElement;
|
|
64
66
|
return e;
|
|
65
67
|
}
|
|
66
|
-
function
|
|
68
|
+
function j(e, t) {
|
|
67
69
|
let n = t;
|
|
68
70
|
for (; n;) {
|
|
69
71
|
if (n === e) return !0;
|
|
@@ -71,12 +73,12 @@ function M(e, t) {
|
|
|
71
73
|
}
|
|
72
74
|
return !1;
|
|
73
75
|
}
|
|
74
|
-
var
|
|
76
|
+
var M = class extends i {
|
|
75
77
|
static {
|
|
76
|
-
|
|
78
|
+
S = this;
|
|
77
79
|
}
|
|
78
80
|
constructor(...t) {
|
|
79
|
-
super(...t), this.state = "default", this.counts = {}, this.locale = "", this.hasOverlay = !1, this.hasNotice = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.context = null, this.unsizedWarnKey = `unsized:${++
|
|
81
|
+
super(...t), this.state = "default", this.counts = {}, this.locale = "", this.hasOverlay = !1, this.openMenu = null, this.menuIds = /* @__PURE__ */ new WeakMap(), this.hasNotice = !1, this.overlayTrigger = null, this.internals = typeof this.attachInternals == "function" ? this.attachInternals() : void 0, this.context = null, this.unsizedWarnKey = `unsized:${++w}`, this.isMatchedLink = (e) => !this.context || !e ? !1 : (e = typeof e == "string" ? new URLPattern(e, window.location.origin) : e, e.test(this.context.path, window.location.origin)), this.handleToggleButtonClick = () => {
|
|
80
82
|
let t = this.screenSize ?? e.screen ?? "large";
|
|
81
83
|
t === "large" ? this.state = this.state === "default" ? "slim" : "default" : t === "medium" ? this.state = this.state === "slim" ? "modal" : "slim" : t === "small" ? this.state = this.state === "mobile" ? "mobile-open" : "mobile" : console.warn("Unknown screen size:", t);
|
|
82
84
|
}, this.handleBackdropClick = () => {
|
|
@@ -102,7 +104,7 @@ var N = class extends i {
|
|
|
102
104
|
}, this.handleOverlaySlotChange = (e) => {
|
|
103
105
|
let t = o(e.target);
|
|
104
106
|
if (t && !this.hasOverlay) {
|
|
105
|
-
let e =
|
|
107
|
+
let e = A();
|
|
106
108
|
this.overlayTrigger = e && e !== document.body ? e : null;
|
|
107
109
|
}
|
|
108
110
|
this.hasOverlay = t;
|
|
@@ -116,9 +118,9 @@ var N = class extends i {
|
|
|
116
118
|
}, this._handleMainKeydown = (e) => {
|
|
117
119
|
if (e.defaultPrevented) return;
|
|
118
120
|
let t = e.composedPath()[0];
|
|
119
|
-
if (t instanceof HTMLElement &&
|
|
121
|
+
if (t instanceof HTMLElement && T(t) || e.key === " " && t instanceof HTMLElement && D(t)) return;
|
|
120
122
|
let n = this.shadowRoot?.querySelector(".main");
|
|
121
|
-
if (!n || !
|
|
123
|
+
if (!n || !k(e.composedPath(), n)) return;
|
|
122
124
|
let r = n.clientHeight;
|
|
123
125
|
switch (e.key) {
|
|
124
126
|
case " ":
|
|
@@ -151,6 +153,9 @@ var N = class extends i {
|
|
|
151
153
|
static {
|
|
152
154
|
this.styles = [super.styles, c];
|
|
153
155
|
}
|
|
156
|
+
static {
|
|
157
|
+
this.menuSeq = 0;
|
|
158
|
+
}
|
|
154
159
|
get mainElement() {
|
|
155
160
|
return this.shadowRoot?.querySelector(".main") ?? null;
|
|
156
161
|
}
|
|
@@ -158,13 +163,13 @@ var N = class extends i {
|
|
|
158
163
|
super.connectedCallback(), window.addEventListener("route-begin", this.handleRouteBegin), window.addEventListener("route-done", this.handleRouteDone), window.addEventListener("route-progress", this.handleRouteProgress), window.addEventListener("route-error", this.handleRouteError), window.addEventListener("screen-resize", this.handleScreenResize);
|
|
159
164
|
}
|
|
160
165
|
disconnectedCallback() {
|
|
161
|
-
window.removeEventListener("route-begin", this.handleRouteBegin), window.removeEventListener("route-done", this.handleRouteDone), window.removeEventListener("route-progress", this.handleRouteProgress), window.removeEventListener("route-error", this.handleRouteError), window.removeEventListener("screen-resize", this.handleScreenResize),
|
|
166
|
+
window.removeEventListener("route-begin", this.handleRouteBegin), window.removeEventListener("route-done", this.handleRouteDone), window.removeEventListener("route-progress", this.handleRouteProgress), window.removeEventListener("route-error", this.handleRouteError), window.removeEventListener("screen-resize", this.handleScreenResize), b.closeLayer(this), super.disconnectedCallback();
|
|
162
167
|
}
|
|
163
168
|
willUpdate(e) {
|
|
164
169
|
super.willUpdate(e), e.has("config") && (this.styles = this.config?.styles);
|
|
165
170
|
}
|
|
166
171
|
updated(e) {
|
|
167
|
-
super.updated(e), e.has("hasOverlay") && (this.internals?.states?.[this.hasOverlay ? "add" : "delete"]("overlay"), this.hasOverlay ? (
|
|
172
|
+
super.updated(e), e.has("hasOverlay") && (this.internals?.states?.[this.hasOverlay ? "add" : "delete"]("overlay"), this.hasOverlay ? (b.openLayer(this, this.handleOverlayEscape), this.focusOverlay()) : e.get("hasOverlay") === !0 && (b.closeLayer(this), this.restoreOverlayFocus()));
|
|
168
173
|
}
|
|
169
174
|
firstUpdated(e) {
|
|
170
175
|
super.firstUpdated(e), this.warnIfUnsized();
|
|
@@ -173,7 +178,7 @@ var N = class extends i {
|
|
|
173
178
|
process.env.NODE_ENV !== "production" && requestAnimationFrame(() => {
|
|
174
179
|
if (!this.isConnected) return;
|
|
175
180
|
let e = this.getBoundingClientRect().height;
|
|
176
|
-
e >= 200 ||
|
|
181
|
+
e >= 200 || C(this.unsizedWarnKey, `u-sidebar-layout is only ${Math.round(e)}px tall — its height: 100% found no sized ancestor, so the shell sits at its own chrome height and the route area has almost no room. Give the root element a height (e.g. #app { height: 100vh } — app.load() does this for document.body).`);
|
|
177
182
|
});
|
|
178
183
|
}
|
|
179
184
|
render() {
|
|
@@ -260,6 +265,46 @@ var N = class extends i {
|
|
|
260
265
|
></div>
|
|
261
266
|
` : u;
|
|
262
267
|
}
|
|
268
|
+
renderMenu(e) {
|
|
269
|
+
let t = this.menuIds.get(e);
|
|
270
|
+
t || (t = `u-sidebar-menu-${++S.menuSeq}`, this.menuIds.set(e, t));
|
|
271
|
+
let n = t, r = this.state === "mobile" || this.state === "mobile-open" ? "bottom-start" : "right-start";
|
|
272
|
+
return l`
|
|
273
|
+
<u-sidebar-button
|
|
274
|
+
id=${n}
|
|
275
|
+
haspopup="menu"
|
|
276
|
+
.expanded=${this.openMenu === n}
|
|
277
|
+
?compact=${this.state === "slim"}
|
|
278
|
+
.icon="${e.icon}"
|
|
279
|
+
.lib="${e.lib}"
|
|
280
|
+
.label="${e.label}"
|
|
281
|
+
></u-sidebar-button>
|
|
282
|
+
<u-popover for="#${n}" placement=${r} autofocus
|
|
283
|
+
@show=${() => {
|
|
284
|
+
this.openMenu = n;
|
|
285
|
+
}}
|
|
286
|
+
@hide=${(e) => this.handleMenuHide(e, n)}
|
|
287
|
+
@pick=${(t) => this.handleMenuPick(t, e)}>
|
|
288
|
+
<u-menu>
|
|
289
|
+
${g(e.items, (e, t) => t, (e, t) => l`
|
|
290
|
+
<u-menu-item value=${String(t)} ?disabled=${e.disabled === !0}>
|
|
291
|
+
${e.icon ? l`<u-icon slot="prefix" .lib=${e.lib} .name=${e.icon}></u-icon>` : u}
|
|
292
|
+
${e.label}
|
|
293
|
+
</u-menu-item>
|
|
294
|
+
`)}
|
|
295
|
+
</u-menu>
|
|
296
|
+
</u-popover>
|
|
297
|
+
`;
|
|
298
|
+
}
|
|
299
|
+
handleMenuPick(e, t) {
|
|
300
|
+
let n = t.items[Number(e.detail.value)];
|
|
301
|
+
n && !n.disabled && (e.currentTarget.hide(), n.onClick?.(e));
|
|
302
|
+
}
|
|
303
|
+
handleMenuHide(e, t) {
|
|
304
|
+
this.openMenu === t && (this.openMenu = null);
|
|
305
|
+
let n = e.currentTarget, r = A();
|
|
306
|
+
r && j(n, r) && this.shadowRoot?.querySelector(`#${t}`)?.focus();
|
|
307
|
+
}
|
|
263
308
|
renderItem(e) {
|
|
264
309
|
if (!e) return u;
|
|
265
310
|
if (e.type === "html") {
|
|
@@ -268,7 +313,6 @@ var N = class extends i {
|
|
|
268
313
|
}
|
|
269
314
|
if (e.type === "button") return l`
|
|
270
315
|
<u-sidebar-button
|
|
271
|
-
id=${_(e.id)}
|
|
272
316
|
?compact=${this.state === "slim"}
|
|
273
317
|
.icon="${e.icon}"
|
|
274
318
|
.lib="${e.lib}"
|
|
@@ -277,6 +321,7 @@ var N = class extends i {
|
|
|
277
321
|
@click="${e.onClick}"
|
|
278
322
|
></u-sidebar-button>
|
|
279
323
|
`;
|
|
324
|
+
if (e.type === "menu") return this.renderMenu(e);
|
|
280
325
|
if (e.type === "link") {
|
|
281
326
|
let t = this.isMatchedLink(e.pattern || e.href);
|
|
282
327
|
return l`
|
|
@@ -331,7 +376,7 @@ var N = class extends i {
|
|
|
331
376
|
};
|
|
332
377
|
}
|
|
333
378
|
icon(e) {
|
|
334
|
-
return this.config?.icons?.[e] ??
|
|
379
|
+
return this.config?.icons?.[e] ?? S.DEFAULT_ICONS[e];
|
|
335
380
|
}
|
|
336
381
|
renderLogo() {
|
|
337
382
|
let e = this.config?.logo, t = this.config?.title ?? "Home";
|
|
@@ -368,14 +413,14 @@ var N = class extends i {
|
|
|
368
413
|
async focusRoute(e) {
|
|
369
414
|
let t = this.routeContent();
|
|
370
415
|
if (await Promise.all(t.map((e) => e.updateComplete)), this.hasOverlay || !e.isConnected) return;
|
|
371
|
-
let n =
|
|
372
|
-
n && t.some((e) =>
|
|
416
|
+
let n = A();
|
|
417
|
+
n && t.some((e) => j(e, n)) || this.focusContentStart(t, e);
|
|
373
418
|
}
|
|
374
419
|
focusContentStart(e, t) {
|
|
375
|
-
let n =
|
|
420
|
+
let n = y(e, "[autofocus]", v);
|
|
376
421
|
n?.focus();
|
|
377
|
-
let r =
|
|
378
|
-
n && r &&
|
|
422
|
+
let r = A();
|
|
423
|
+
n && r && j(n, r) || t.focus({ preventScroll: !0 });
|
|
379
424
|
}
|
|
380
425
|
overlayPanels() {
|
|
381
426
|
return (this.shadowRoot?.querySelector("slot[name=\"overlay\"]"))?.assignedElements({ flatten: !0 }) ?? [];
|
|
@@ -383,19 +428,19 @@ var N = class extends i {
|
|
|
383
428
|
async focusOverlay() {
|
|
384
429
|
let e = this.overlayPanels();
|
|
385
430
|
if (await Promise.all(e.map((e) => e.updateComplete)), !this.hasOverlay) return;
|
|
386
|
-
let t =
|
|
387
|
-
t && e.some((e) =>
|
|
431
|
+
let t = A();
|
|
432
|
+
t && e.some((e) => j(e, t)) || (y(e, "[autofocus]", v) ?? y(e, "input, select, textarea, u-input, u-textarea, u-select, u-checkbox, u-radio, u-switch, u-slider", v) ?? this.shadowRoot?.querySelector(".overlay-close"))?.focus();
|
|
388
433
|
}
|
|
389
434
|
restoreOverlayFocus() {
|
|
390
435
|
let e = this.overlayTrigger;
|
|
391
436
|
this.overlayTrigger = null;
|
|
392
|
-
let t =
|
|
437
|
+
let t = A();
|
|
393
438
|
t && t !== document.body || (e?.isConnected ? e.focus() : this.mainElement?.focus({ preventScroll: !0 }));
|
|
394
439
|
}
|
|
395
440
|
};
|
|
396
441
|
r([f({
|
|
397
442
|
type: String,
|
|
398
443
|
reflect: !0
|
|
399
|
-
}), n("design:type", Object)],
|
|
444
|
+
}), n("design:type", Object)], M.prototype, "state", void 0), r([f({ type: Object }), n("design:type", Object)], M.prototype, "config", void 0), r([f({ type: Object }), n("design:type", typeof Record > "u" ? Object : Record)], M.prototype, "counts", void 0), r([f({ type: String }), n("design:type", Object)], M.prototype, "locale", void 0), r([p("u-progress-bar"), n("design:type", x === void 0 ? Object : x)], M.prototype, "progressBarEl", void 0), r([m(), n("design:type", Object)], M.prototype, "hasOverlay", void 0), r([m(), n("design:type", Object)], M.prototype, "openMenu", void 0), r([m(), n("design:type", Object)], M.prototype, "hasNotice", void 0), r([m(), n("design:type", Object)], M.prototype, "context", void 0), M = S = r([d("u-sidebar-layout")], M);
|
|
400
445
|
//#endregion
|
|
401
|
-
export {
|
|
446
|
+
export { M as SidebarLayout };
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { TemplateResult } from 'lit';
|
|
2
|
+
import { DirectiveResult } from 'lit/directive.js';
|
|
2
3
|
import { RouteContext } from '@iyulab/router';
|
|
3
4
|
import { StyleMap } from '../internals/StyledElement';
|
|
4
5
|
import { SidebarLinkConfig } from '../components/SidebarLink';
|
|
@@ -58,8 +59,33 @@ export interface SidebarLogoImage {
|
|
|
58
59
|
export type SidebarLogoRenderer = (state: SidebarState) => TemplateResult<1> | HTMLElement | string;
|
|
59
60
|
/** 최상단 앱 로고 설정: 아이콘명(문자열, 기존 동작) | 이미지 | 커스텀 렌더 함수 */
|
|
60
61
|
export type SidebarLogoConfig = string | SidebarLogoImage | SidebarLogoRenderer;
|
|
61
|
-
/**
|
|
62
|
-
export
|
|
62
|
+
/** One entry of a `type: 'menu'` popup. */
|
|
63
|
+
export interface SidebarMenuItemConfig extends SidebarPermissionGuard {
|
|
64
|
+
label: string | DirectiveResult;
|
|
65
|
+
icon?: string;
|
|
66
|
+
/** Icon library for `icon` (see `SidebarButtonConfig.lib`). */
|
|
67
|
+
lib?: string;
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
/** Runs when the entry is chosen (click, Enter or Space). The menu closes and focus returns to its trigger. */
|
|
70
|
+
onClick?: (event?: Event) => void;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A sidebar button that opens a popup menu — the flyout counterpart of `type: 'group'` (which expands in
|
|
74
|
+
* place). The shell draws the trigger and the menu together and owns what only it knows: the menu opens
|
|
75
|
+
* sideways beside the sidebar and downwards when the sidebar is the full-width mobile panel. It sets
|
|
76
|
+
* `aria-haspopup`/`aria-expanded` on the trigger, moves focus into the menu when it opens, and returns
|
|
77
|
+
* focus to the trigger on Escape or after an entry is chosen.
|
|
78
|
+
*/
|
|
79
|
+
export interface SidebarMenuConfig extends SidebarPermissionGuard {
|
|
80
|
+
type: 'menu';
|
|
81
|
+
/** The trigger's label — also its accessible name when the sidebar is slim. */
|
|
82
|
+
label: string | DirectiveResult;
|
|
83
|
+
icon?: string;
|
|
84
|
+
lib?: string;
|
|
85
|
+
items: SidebarMenuItemConfig[];
|
|
86
|
+
}
|
|
87
|
+
/** union: section | group | link | button | menu | html */
|
|
88
|
+
export type SidebarItem = (SidebarLinkConfig | SidebarSectionConfig | SidebarGroupConfig | SidebarButtonConfig | SidebarMenuConfig | SidebarHtmlConfig);
|
|
63
89
|
/** 사이드바 레이아웃 전체 설정 */
|
|
64
90
|
export interface SidebarLayoutConfig {
|
|
65
91
|
type: 'sidebar';
|
|
@@ -4,6 +4,7 @@ import { SidebarItem } from './SidebarLayout.types';
|
|
|
4
4
|
*
|
|
5
5
|
* - 각 항목의 `requirePermission`/`requireAnyPermission` 를 `hasPermission` 으로 검사해 탈락 항목 제거.
|
|
6
6
|
* - `section`/`group` 은 자식(`items`)을 재귀 필터하고, **자식이 모두 걸러지면 컨테이너째 제거**한다.
|
|
7
|
+
* - `menu` 는 팝업 항목(`items`)을 같은 가드로 거르고, 남는 항목이 없으면 트리거째 제거한다.
|
|
7
8
|
* - `hasPermission` 이 없으면(undefined) 원본을 그대로 반환한다(필터링 없음).
|
|
8
9
|
*
|
|
9
10
|
* 원본 배열/항목을 변형하지 않고 새 배열/얕은 복제본을 반환한다(순수).
|
package/dist/react.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { default as React } from 'react';
|
|
2
2
|
import { EventName } from '@lit/react';
|
|
3
|
-
import { SidebarLayout as SidebarLayoutElement } from './layouts/SidebarLayout.js';
|
|
4
|
-
import { Wizard as WizardElement,
|
|
3
|
+
import { SidebarLayout as SidebarLayoutElement, SidebarLayoutEventMap } from './layouts/SidebarLayout.js';
|
|
4
|
+
import { Wizard as WizardElement, WizardEventMap } from './components/Wizard.js';
|
|
5
5
|
import { GroupBoxLevel } from './components/GroupBox.js';
|
|
6
6
|
import { InfoFieldFormat, InfoFieldSize, InfoFieldTrend, InfoFieldTone } from './components/InfoField.js';
|
|
7
7
|
export { ScreenObserver } from './internals/ScreenObserver.js';
|
|
@@ -12,7 +12,7 @@ export type { ScreenSize, ScreenObserverConfig, ScreenResizeEvent } from './inte
|
|
|
12
12
|
* React 18 (JSX attributes stringify), so this wrapper keeps `config` working on both.
|
|
13
13
|
*/
|
|
14
14
|
export declare const SidebarLayout: import('@lit/react').ReactWebComponent<SidebarLayoutElement, {
|
|
15
|
-
onOverlayClose: EventName<
|
|
15
|
+
onOverlayClose: EventName<SidebarLayoutEventMap["overlay-close"]>;
|
|
16
16
|
}>;
|
|
17
17
|
export type SidebarLayoutProps = React.ComponentProps<typeof SidebarLayout>;
|
|
18
18
|
/**
|
|
@@ -23,11 +23,11 @@ export type SidebarLayoutProps = React.ComponentProps<typeof SidebarLayout>;
|
|
|
23
23
|
* wrapper keeps working on React 18 the same way it does for `SidebarLayout`'s `config`.
|
|
24
24
|
*/
|
|
25
25
|
export declare const Wizard: import('@lit/react').ReactWebComponent<WizardElement, {
|
|
26
|
-
onStepChange: EventName<
|
|
26
|
+
onStepChange: EventName<WizardEventMap["step-change"]>;
|
|
27
27
|
}>;
|
|
28
28
|
export type WizardProps = React.ComponentProps<typeof Wizard>;
|
|
29
29
|
export type { SidebarLayoutElement, WizardElement };
|
|
30
|
-
export type { SidebarLayoutConfig, SidebarItem, SidebarLogoConfig, SidebarLinkConfig, SidebarSectionConfig, SidebarGroupConfig, SidebarButtonConfig, SidebarHtmlConfig, } from './layouts/SidebarLayout.types.js';
|
|
30
|
+
export type { SidebarLayoutConfig, SidebarItem, SidebarLogoConfig, SidebarLinkConfig, SidebarSectionConfig, SidebarGroupConfig, SidebarButtonConfig, SidebarMenuConfig, SidebarMenuItemConfig, SidebarHtmlConfig, } from './layouts/SidebarLayout.types.js';
|
|
31
31
|
export type { WizardStep, WizardStepState, WizardStepChangeDetail } from './components/Wizard.js';
|
|
32
32
|
declare module 'react' {
|
|
33
33
|
namespace JSX {
|
package/package.json
CHANGED
|
@@ -176,7 +176,6 @@ Action button — triggers a callback instead of navigating.
|
|
|
176
176
|
```typescript
|
|
177
177
|
interface SidebarButtonConfig {
|
|
178
178
|
type: 'button';
|
|
179
|
-
id?: string;
|
|
180
179
|
icon?: string;
|
|
181
180
|
lib?: string;
|
|
182
181
|
label: string | DirectiveResult;
|
|
@@ -191,11 +190,54 @@ Example:
|
|
|
191
190
|
{ type: 'button', icon: 'logout', label: 'Sign Out', onClick: () => auth.signOut() }
|
|
192
191
|
```
|
|
193
192
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
For a button that opens a menu, use `type: 'menu'` below — not a button plus your own popover.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### `SidebarMenuConfig` — `type: 'menu'`
|
|
198
|
+
|
|
199
|
+
A sidebar button that opens a popup menu — the flyout counterpart of `type: 'group'` (which expands
|
|
200
|
+
in place).
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
interface SidebarMenuConfig {
|
|
204
|
+
type: 'menu';
|
|
205
|
+
label: string | DirectiveResult; // the trigger's label (its accessible name when slim)
|
|
206
|
+
icon?: string;
|
|
207
|
+
lib?: string;
|
|
208
|
+
items: SidebarMenuItemConfig[];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface SidebarMenuItemConfig {
|
|
212
|
+
label: string | DirectiveResult;
|
|
213
|
+
icon?: string;
|
|
214
|
+
lib?: string;
|
|
215
|
+
disabled?: boolean;
|
|
216
|
+
onClick?: (event?: Event) => void; // runs when chosen; the menu closes
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Example:
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
{
|
|
224
|
+
type: 'menu', icon: 'three-dots', label: 'More',
|
|
225
|
+
items: [
|
|
226
|
+
{ icon: 'download', label: 'Export', onClick: () => exportAll() },
|
|
227
|
+
{ icon: 'gear', label: 'Settings', onClick: () => app.navigate('/settings') },
|
|
228
|
+
],
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
The shell owns what only it knows:
|
|
233
|
+
|
|
234
|
+
- **Placement** — beside the sidebar, and downwards when the sidebar is the full-width mobile panel
|
|
235
|
+
(`mobile`/`mobile-open`), where there is no room beside it.
|
|
236
|
+
- **Keyboard and focus** — the trigger carries `aria-haspopup="menu"` and `aria-expanded`; opening
|
|
237
|
+
moves focus to the first entry; Escape or choosing an entry closes the menu and returns focus to
|
|
238
|
+
the trigger.
|
|
239
|
+
- **Permissions** — each entry takes `requirePermission`/`requireAnyPermission`; with
|
|
240
|
+
`hasPermission` set, a menu left with no entries is dropped.
|
|
199
241
|
|
|
200
242
|
---
|
|
201
243
|
|
|
@@ -229,41 +271,24 @@ Example:
|
|
|
229
271
|
|
|
230
272
|
---
|
|
231
273
|
|
|
232
|
-
###
|
|
274
|
+
### A custom flyout (`type: 'html'`)
|
|
233
275
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
for `for="#id"` anchoring to resolve (see the `id` note above).
|
|
276
|
+
`type: 'menu'` covers a list of actions. For anything else that flies out of the sidebar (a form, a
|
|
277
|
+
card), assemble the trigger and a `u-popover` inside one `type: 'html'` item — both must live in the
|
|
278
|
+
sidebar's shadow root, because `for="#id"` never crosses a shadow boundary.
|
|
238
279
|
|
|
239
280
|
```typescript
|
|
240
281
|
{
|
|
241
282
|
type: 'html',
|
|
242
|
-
render: (
|
|
243
|
-
<u-sidebar-button id="
|
|
244
|
-
<u-popover for="#
|
|
245
|
-
<u-menu>
|
|
246
|
-
<u-menu-item @click=${doA}>Action A</u-menu-item>
|
|
247
|
-
<u-menu-item @click=${doB}>Action B</u-menu-item>
|
|
248
|
-
</u-menu>
|
|
249
|
-
</u-popover>
|
|
283
|
+
render: () => html`
|
|
284
|
+
<u-sidebar-button id="quick-note" icon="pencil" label="Note"></u-sidebar-button>
|
|
285
|
+
<u-popover for="#quick-note" placement="right-start">…</u-popover>
|
|
250
286
|
`,
|
|
251
287
|
}
|
|
252
288
|
```
|
|
253
289
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
the natural desktop flyout) has room on neither side. `flip()` now falls back **across the**
|
|
257
|
-
**axis** in exactly that case, landing the popover vertically instead of off-screen.
|
|
258
|
-
|
|
259
|
-
⚠ Against an older `components` it did render off-screen and invisible — `flip()` only ever
|
|
260
|
-
considered the opposite side on the same axis, found no room there either, and gave up. If you
|
|
261
|
-
pin below `1.37.1`, keep choosing `placement` from `state` as the snippet above does.
|
|
262
|
-
|
|
263
|
-
Choosing from `state` is still reasonable when you want to *decide* the direction rather than
|
|
264
|
-
let `flip()` pick it. Either way this is not a `strategy="absolute"` vs `"fixed"` distinction —
|
|
265
|
-
switching strategy changes nothing. Both behaviours are pinned in
|
|
266
|
-
`tests/browser/sidebar-popover-submenu.browser.test.ts`.
|
|
290
|
+
A fixed sideways `placement` stays on screen on the mobile panel as of `@iyulab/components@1.37.1`
|
|
291
|
+
(`flip()` falls back across the axis); `tests/browser/sidebar-popover-submenu.browser.test.ts` pins it.
|
|
267
292
|
|
|
268
293
|
---
|
|
269
294
|
|