@iyulab/desktop-patterns 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 iyulab
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 ADDED
@@ -0,0 +1,134 @@
1
+ # desktop-patterns
2
+
3
+ App-shell and structural layout patterns assembled on top of
4
+ [`desktop-compact`](https://github.com/iyulab/desktop-compact)'s primitives. Framework-neutral custom
5
+ elements ([Lit](https://lit.dev)), zero built-in strings, every content region a slot.
6
+
7
+ Structure only — atomic controls (buttons, inputs, dialogs) belong in `desktop-compact`; native
8
+ platform integration (file dialogs, tray, window control) belongs in
9
+ [`electron-kit`](https://github.com/iyulab/electron-kit). This package doesn't call any platform API
10
+ itself, and knows nothing about any specific consuming application.
11
+
12
+ ## Install
13
+
14
+ Requires Node ≥22.
15
+
16
+ ```bash
17
+ npm install @iyulab/desktop-patterns @iyulab/desktop-compact
18
+ ```
19
+
20
+ Import only the components you use — each ships as its own subpath for tree-shaking:
21
+
22
+ ```ts
23
+ import '@iyulab/desktop-patterns/shell'
24
+ import '@iyulab/desktop-patterns/sidebar'
25
+ ```
26
+
27
+ Or import everything via the barrel:
28
+
29
+ ```ts
30
+ import '@iyulab/desktop-patterns'
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ```html
36
+ <dp-shell sidebar-open>
37
+ <dp-sidebar slot="sidebar" active-id="overview" header="My App" id="nav"></dp-sidebar>
38
+ <dp-toolbar slot="toolbar" heading="My App" show-toggle></dp-toolbar>
39
+ <dp-page>
40
+ <h2>Overview</h2>
41
+ <p>Main content.</p>
42
+ </dp-page>
43
+ </dp-shell>
44
+ ```
45
+
46
+ `items`/`bottomItems` are arrays, so they're set as JS properties (not plain HTML attributes) — same
47
+ as any Lit custom element:
48
+
49
+ ```ts
50
+ document.querySelector<HTMLElementTagNameMap['dp-sidebar']>('#nav')!.items = [
51
+ { id: 'overview', icon: '■', label: 'Overview' },
52
+ ]
53
+
54
+ document.querySelector('dp-sidebar')!.addEventListener('dp-sidebar-select', (e) => {
55
+ // consumer owns activeId / routing
56
+ })
57
+ document.querySelector('dp-toolbar')!.addEventListener('dp-toolbar-toggle', () => {
58
+ // consumer owns sidebar-open state on dp-shell
59
+ })
60
+ ```
61
+
62
+ Every component takes its user-facing text as a plain attribute/property or slot — there is no
63
+ built-in i18n layer. See each component's Storybook story (`npm run storybook`) for its full API.
64
+
65
+ ## Theming
66
+
67
+ Import both token sheets once in your app's global stylesheet:
68
+
69
+ ```ts
70
+ import '@iyulab/desktop-compact/tokens.css'
71
+ import '@iyulab/desktop-patterns/tokens.css'
72
+ ```
73
+
74
+ Color/spacing/radius/typography all come from `desktop-compact`'s `--dc-*` tokens — this package adds
75
+ only the structural dimensions `desktop-compact` has no concept of (`--dp-sidebar-width`,
76
+ `--dp-sidebar-width-collapsed`, `--dp-header-height`). Every component ships with sane fallback
77
+ values, so it still renders correctly even without either stylesheet.
78
+
79
+ ## Platform boundary — drag regions
80
+
81
+ `dp-toolbar` exposes a neutral `drag-region` flag (not the native `draggable`, which already means
82
+ something else) but never emits any platform-specific CSS itself. The flag reflects to the host
83
+ attribute, but the actual drag surface is a dedicated `part="drag-handle"` spacer between the start
84
+ group and the `actions` slot — never the whole host — so the mapping can never reach the toggle
85
+ button or slotted actions. Mapping that part to an actual window-drag behavior (e.g.
86
+ `-webkit-app-region: drag` in Electron/Chromium-based webviews) is `electron-kit`'s job:
87
+
88
+ ```css
89
+ dp-toolbar[drag-region]::part(drag-handle) {
90
+ -webkit-app-region: drag;
91
+ }
92
+ ```
93
+
94
+ `dp-shell` does not mark any drag region of its own — it has no opinion on what's slotted into
95
+ `toolbar`, so ownership of the drag handle stays entirely with whatever renders there (typically
96
+ `dp-toolbar`).
97
+
98
+ ## Components (v1 — complete, 5/5)
99
+
100
+ | Component | Description |
101
+ |---|---|
102
+ | `dp-page` | Scrollable content region with a centered, max-width column — the page-body every view renders into |
103
+ | `dp-sidebar` | App-shell navigation rail — expanded/collapsed states, an optional pinned bottom group, a `footer` slot for whatever a consumer wants to anchor there |
104
+ | `dp-toolbar` | App-shell header bar — `heading`/`subtitle`, an optional collapse toggle, a right-side `actions` slot |
105
+ | `dp-shell` | Top-level layout composing `sidebar`/`toolbar`/`banner`/main-content regions, with a responsive drawer (backdrop + overlay sidebar) below a 1024px breakpoint |
106
+ | `dp-shortcut-overlay` | Display-only keyboard-shortcuts help panel — a `shortcuts` list + `open` flag; no registration/binding infrastructure (that stays the consumer's) |
107
+
108
+ Run `npm run storybook` to browse every component interactively, including `dp-shell` composing real
109
+ `dp-sidebar`+`dp-toolbar`+`dp-page` together.
110
+
111
+ ## Accessibility
112
+
113
+ Every component ships with an axe accessibility test as part of its test suite (`npm test`). Landmarks
114
+ follow ARIA APG conventions — `dp-sidebar` uses a semantic `<nav>` (no redundant host-level role),
115
+ `dp-shortcut-overlay` uses `role="dialog"` with `aria-labelledby` pointing at its visible heading.
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ npm install
121
+ npm test # @web/test-runner, real Chromium
122
+ npm run typecheck
123
+ npm run guard # forge-ignorance + platform-agnosticism scan (this package must stay domain-neutral and platform-neutral)
124
+ npm run build # per-component ESM output, type declarations
125
+ npm run storybook # interactive component browser
126
+ ```
127
+
128
+ Until `@iyulab/desktop-compact` publishes its first npm release, this package resolves it via
129
+ `file:../desktop-compact` for local development (a sibling checkout at that relative path) — this
130
+ switches to a registry semver range once `desktop-compact` publishes.
131
+
132
+ ## License
133
+
134
+ MIT
@@ -0,0 +1,17 @@
1
+ import { LitElement } from 'lit';
2
+ export type PageMaxWidth = 'sm' | 'md' | 'lg' | 'xl' | 'full';
3
+ /**
4
+ * Scrollable content region with a centered, max-width column — the page-body
5
+ * pattern every view in an app shell renders into. Pure layout: it has no
6
+ * opinion on what it contains (default slot only).
7
+ */
8
+ export declare class DpPage extends LitElement {
9
+ static styles: import('lit').CSSResult;
10
+ maxWidth: PageMaxWidth;
11
+ render(): import('lit-html').TemplateResult<1>;
12
+ }
13
+ declare global {
14
+ interface HTMLElementTagNameMap {
15
+ 'dp-page': DpPage;
16
+ }
17
+ }
@@ -0,0 +1,38 @@
1
+ import { LitElement } from 'lit';
2
+ export declare class DpShellSidebarCloseEvent extends Event {
3
+ constructor();
4
+ }
5
+ /**
6
+ * Top-level app-shell layout: a responsive sidebar region (a fixed
7
+ * backdrop-covered drawer below the desktop breakpoint, static beside the
8
+ * content above it), a toolbar row, an optional banner row, and the main
9
+ * content area. Pure composition — every region is a slot, this component
10
+ * has no opinion on what's inside any of them.
11
+ *
12
+ * The original component this generalizes rendered a fixed status banner
13
+ * unconditionally above its `banner` slot; that hardcoded element is gone
14
+ * here — the existing `banner` slot alone is enough for a consumer to put
15
+ * anything there, and this repo has no business knowing what that banner
16
+ * should be.
17
+ *
18
+ * Drag region: this component does not mark any drag region of its own.
19
+ * The `toolbar-row` wrapper is sized exactly to whatever is slotted into
20
+ * `toolbar` — it has no empty space that is safely distinct from the
21
+ * slotted content's own interactive children, so a wrapper-level flag here
22
+ * could only ever mark the same box `dp-toolbar` already marks (redundant)
23
+ * or, worse, extend the drag region across content this component has no
24
+ * visibility into (unsafe — this component "has no opinion on what's
25
+ * inside any [slot]", per the class doc above). Drag-region ownership
26
+ * belongs entirely to whatever renders into the `toolbar` slot — see
27
+ * `dp-toolbar`'s own `part="drag-handle"`.
28
+ */
29
+ export declare class DpShell extends LitElement {
30
+ static styles: import('lit').CSSResult;
31
+ sidebarOpen: boolean;
32
+ render(): import('lit-html').TemplateResult<1>;
33
+ }
34
+ declare global {
35
+ interface HTMLElementTagNameMap {
36
+ 'dp-shell': DpShell;
37
+ }
38
+ }
@@ -0,0 +1,37 @@
1
+ import { LitElement, nothing } from 'lit';
2
+ export interface ShortcutEntry {
3
+ combo: string;
4
+ description: string;
5
+ }
6
+ export declare class DpShortcutOverlayDismissEvent extends Event {
7
+ constructor();
8
+ }
9
+ /**
10
+ * Display-only keyboard-shortcuts help panel: a `shortcuts` list + an `open`
11
+ * flag, nothing else. This component owns no registry and no trigger
12
+ * keybinding — the original component this generalizes bound its own
13
+ * `shift+/` shortcut and read from a module-level, framework-hook-specific
14
+ * registry; both are the consumer's responsibility here (see
15
+ * docs/superpowers/specs/2026-09-02-desktop-patterns-design.md, the
16
+ * dp-shortcut-overlay row). This component only dismisses itself (outside
17
+ * click / Escape) by asking — it dispatches `dp-shortcut-overlay-dismiss`
18
+ * rather than setting its own `open` to false, since `open` is the
19
+ * consumer's state to own, not this component's.
20
+ */
21
+ export declare class DpShortcutOverlay extends LitElement {
22
+ #private;
23
+ static styles: import('lit').CSSResult;
24
+ shortcuts: ShortcutEntry[];
25
+ open: boolean;
26
+ heading: string;
27
+ emptyMessage: string;
28
+ private headingId;
29
+ updated(changed: Map<string, unknown>): void;
30
+ disconnectedCallback(): void;
31
+ render(): import('lit-html').TemplateResult<1> | typeof nothing;
32
+ }
33
+ declare global {
34
+ interface HTMLElementTagNameMap {
35
+ 'dp-shortcut-overlay': DpShortcutOverlay;
36
+ }
37
+ }
@@ -0,0 +1,31 @@
1
+ import { LitElement } from 'lit';
2
+ export interface SidebarItem {
3
+ id: string;
4
+ icon: string;
5
+ label: string;
6
+ }
7
+ export declare class DpSidebarSelectEvent extends Event {
8
+ readonly itemId: string;
9
+ constructor(itemId: string);
10
+ }
11
+ /**
12
+ * App-shell navigation rail: an icon+label list with an expanded/collapsed
13
+ * state, an optional pinned bottom group, and a `footer` slot for whatever
14
+ * the consumer wants to anchor at the bottom — a status badge, a version
15
+ * string, anything; this repo doesn't know what.
16
+ */
17
+ export declare class DpSidebar extends LitElement {
18
+ #private;
19
+ static styles: import('lit').CSSResult;
20
+ items: SidebarItem[];
21
+ bottomItems: SidebarItem[];
22
+ activeId: string;
23
+ header: string;
24
+ collapsed: boolean;
25
+ render(): import('lit-html').TemplateResult<1>;
26
+ }
27
+ declare global {
28
+ interface HTMLElementTagNameMap {
29
+ 'dp-sidebar': DpSidebar;
30
+ }
31
+ }
@@ -0,0 +1,52 @@
1
+ import { LitElement } from 'lit';
2
+ export declare class DpToolbarToggleEvent extends Event {
3
+ constructor();
4
+ }
5
+ /**
6
+ * App-shell header bar: a `heading` (optionally overridden per-view by
7
+ * `subtitle`), an optional collapse/expand toggle, and a right-side
8
+ * `actions` slot. Pure layout — this repo does not call any platform API.
9
+ *
10
+ * Named `heading`, not `title` — matches dc-section-heading's own
11
+ * established convention for this kind of headline text, and avoids
12
+ * shadowing the native HTMLElement.prototype.title (tooltip) accessor with
13
+ * an unrelated meaning, the same reasoning already applied to `drag-region`
14
+ * below.
15
+ *
16
+ * Drag region: the `drag-region` attribute reflects intent only, mirroring
17
+ * dp-shell — this repo never emits any platform-specific CSS itself (see
18
+ * docs/superpowers/specs/2026-09-02-desktop-patterns-design.md, "플랫폼 API
19
+ * 경계"). Named `drag-region`, not the native `draggable` — the platform HTML5
20
+ * Drag-and-Drop attribute already owns that name/IDL property with unrelated
21
+ * semantics (HTMLElement.prototype.draggable); reusing it here would shadow
22
+ * the native accessor on this custom element for a completely different
23
+ * concept (a window-chrome drag handle, not drag-and-drop). This component's
24
+ * original source also hardcoded extra end-padding to reserve room for
25
+ * native window-chrome controls — a platform assumption this repo can't make
26
+ * either. `--dp-toolbar-inset-start`/`--dp-toolbar-inset-end` (default 0)
27
+ * generalize that into a neutral extension point a platform-adapter layer
28
+ * can set.
29
+ *
30
+ * The drag CSS a consumer maps onto `drag-region` must never target the host
31
+ * itself — the host also contains the `.toggle` button and the `actions`
32
+ * slot, both interactive, and neither carries any no-drag exception. Instead
33
+ * a dedicated `part="drag-handle"` spacer sits between the start group and
34
+ * the actions group; it renders no content of its own (nothing is ever
35
+ * slotted into it), so it is structurally impossible for it to overlap an
36
+ * interactive child. Consumers scope the platform CSS to that one part, not
37
+ * to `dp-toolbar[drag-region]` alone — see this package's README (platform
38
+ * boundary — drag regions) for the exact selector.
39
+ */
40
+ export declare class DpToolbar extends LitElement {
41
+ static styles: import('lit').CSSResult;
42
+ heading: string;
43
+ subtitle: string;
44
+ showToggle: boolean;
45
+ dragRegion: boolean;
46
+ render(): import('lit-html').TemplateResult<1>;
47
+ }
48
+ declare global {
49
+ interface HTMLElementTagNameMap {
50
+ 'dp-toolbar': DpToolbar;
51
+ }
52
+ }
@@ -0,0 +1,5 @@
1
+ export * from './components/page/dp-page.js';
2
+ export * from './components/sidebar/dp-sidebar.js';
3
+ export * from './components/toolbar/dp-toolbar.js';
4
+ export * from './components/shell/dp-shell.js';
5
+ export * from './components/shortcut-overlay/dp-shortcut-overlay.js';
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ import { DpPage as o } from "./page.js";
2
+ import { DpSidebar as t, DpSidebarSelectEvent as l } from "./sidebar.js";
3
+ import { DpToolbar as a, DpToolbarToggleEvent as S } from "./toolbar.js";
4
+ import { DpShell as v, DpShellSidebarCloseEvent as b } from "./shell.js";
5
+ import { DpShortcutOverlay as i, DpShortcutOverlayDismissEvent as x } from "./shortcut-overlay.js";
6
+ export {
7
+ o as DpPage,
8
+ v as DpShell,
9
+ b as DpShellSidebarCloseEvent,
10
+ i as DpShortcutOverlay,
11
+ x as DpShortcutOverlayDismissEvent,
12
+ t as DpSidebar,
13
+ l as DpSidebarSelectEvent,
14
+ a as DpToolbar,
15
+ S as DpToolbarToggleEvent
16
+ };
package/dist/page.js ADDED
@@ -0,0 +1,48 @@
1
+ import { css as a, LitElement as m, html as x } from "lit";
2
+ import { property as d, customElement as c } from "lit/decorators.js";
3
+ var h = Object.defineProperty, u = Object.getOwnPropertyDescriptor, n = (l, e, s, r) => {
4
+ for (var t = r > 1 ? void 0 : r ? u(e, s) : e, p = l.length - 1, i; p >= 0; p--)
5
+ (i = l[p]) && (t = (r ? i(e, s, t) : i(t)) || t);
6
+ return r && t && h(e, s, t), t;
7
+ };
8
+ const b = {
9
+ sm: "672px",
10
+ md: "768px",
11
+ lg: "896px",
12
+ xl: "1152px",
13
+ full: "none"
14
+ };
15
+ let o = class extends m {
16
+ constructor() {
17
+ super(...arguments), this.maxWidth = "md";
18
+ }
19
+ render() {
20
+ return x`
21
+ <div class="inner" style="max-width: ${b[this.maxWidth]}">
22
+ <slot></slot>
23
+ </div>
24
+ `;
25
+ }
26
+ };
27
+ o.styles = a`
28
+ :host {
29
+ display: block;
30
+ height: 100%;
31
+ overflow-y: auto;
32
+ box-sizing: border-box;
33
+ }
34
+ .inner {
35
+ margin: 0 auto;
36
+ padding: var(--dc-space-6, 24px);
37
+ box-sizing: border-box;
38
+ }
39
+ `;
40
+ n([
41
+ d({ attribute: "max-width", reflect: !0 })
42
+ ], o.prototype, "maxWidth", 2);
43
+ o = n([
44
+ c("dp-page")
45
+ ], o);
46
+ export {
47
+ o as DpPage
48
+ };
package/dist/shell.js ADDED
@@ -0,0 +1,93 @@
1
+ import { css as c, LitElement as p, nothing as b, html as a } from "lit";
2
+ import { property as f, customElement as m } from "lit/decorators.js";
3
+ var v = Object.defineProperty, u = Object.getOwnPropertyDescriptor, d = (r, o, t, s) => {
4
+ for (var e = s > 1 ? void 0 : s ? u(o, t) : o, l = r.length - 1, n; l >= 0; l--)
5
+ (n = r[l]) && (e = (s ? n(o, t, e) : n(e)) || e);
6
+ return s && e && v(o, t, e), e;
7
+ };
8
+ class h extends Event {
9
+ constructor() {
10
+ super("dp-shell-sidebar-close", { bubbles: !0, composed: !0 });
11
+ }
12
+ }
13
+ let i = class extends p {
14
+ constructor() {
15
+ super(...arguments), this.sidebarOpen = !1;
16
+ }
17
+ render() {
18
+ return a`
19
+ ${this.sidebarOpen ? a`<div class="backdrop" @click=${() => this.dispatchEvent(new h())}></div>` : b}
20
+ <div class="sidebar-region"><slot name="sidebar"></slot></div>
21
+ <div class="main-column">
22
+ <div class="toolbar-row">
23
+ <slot name="toolbar"></slot>
24
+ </div>
25
+ <slot name="banner"></slot>
26
+ <main><slot></slot></main>
27
+ </div>
28
+ `;
29
+ }
30
+ };
31
+ i.styles = c`
32
+ :host {
33
+ display: flex;
34
+ width: 100vw;
35
+ height: 100vh;
36
+ overflow: hidden;
37
+ box-sizing: border-box;
38
+ background: var(--dc-color-bg, #ffffff);
39
+ color: var(--dc-color-text, #1a1a1e);
40
+ font-family: var(--dc-font-family, system-ui, sans-serif);
41
+ }
42
+ .backdrop {
43
+ position: fixed;
44
+ inset: 0;
45
+ z-index: 30;
46
+ background: var(--dc-color-backdrop, rgba(0, 0, 0, 0.4));
47
+ }
48
+ .sidebar-region {
49
+ flex-shrink: 0;
50
+ display: none;
51
+ }
52
+ :host([sidebar-open]) .sidebar-region {
53
+ display: block;
54
+ position: fixed;
55
+ inset-block: 0;
56
+ inset-inline-start: 0;
57
+ z-index: 40;
58
+ }
59
+ @media (min-width: 1024px) {
60
+ .sidebar-region {
61
+ display: block;
62
+ position: relative;
63
+ inset: auto;
64
+ z-index: auto;
65
+ }
66
+ .backdrop {
67
+ display: none;
68
+ }
69
+ }
70
+ .main-column {
71
+ display: flex;
72
+ flex-direction: column;
73
+ flex: 1;
74
+ min-width: 0;
75
+ }
76
+ .toolbar-row {
77
+ flex-shrink: 0;
78
+ }
79
+ main {
80
+ flex: 1;
81
+ overflow: auto;
82
+ }
83
+ `;
84
+ d([
85
+ f({ type: Boolean, reflect: !0, attribute: "sidebar-open" })
86
+ ], i.prototype, "sidebarOpen", 2);
87
+ i = d([
88
+ m("dp-shell")
89
+ ], i);
90
+ export {
91
+ i as DpShell,
92
+ h as DpShellSidebarCloseEvent
93
+ };
@@ -0,0 +1,172 @@
1
+ import { css as x, LitElement as b, nothing as w, html as d } from "lit";
2
+ import { property as l, state as _, customElement as k } from "lit/decorators.js";
3
+ var $ = Object.defineProperty, E = Object.getOwnPropertyDescriptor, g = (e) => {
4
+ throw TypeError(e);
5
+ }, a = (e, t, r, c) => {
6
+ for (var s = c > 1 ? void 0 : c ? E(t, r) : t, h = e.length - 1, v; h >= 0; h--)
7
+ (v = e[h]) && (s = (c ? v(t, r, s) : v(s)) || s);
8
+ return c && s && $(t, r, s), s;
9
+ }, m = (e, t, r) => t.has(e) || g("Cannot " + r), u = (e, t, r) => (m(e, t, "read from private field"), t.get(e)), y = (e, t, r) => t.has(e) ? g("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(e) : t.set(e, r), f = (e, t, r) => (m(e, t, "access private method"), r), i, n, p;
10
+ class z extends Event {
11
+ constructor() {
12
+ super("dp-shortcut-overlay-dismiss", { bubbles: !0, composed: !0 });
13
+ }
14
+ }
15
+ let C = 0, o = class extends b {
16
+ constructor() {
17
+ super(...arguments), y(this, n), this.shortcuts = [], this.open = !1, this.heading = "Keyboard shortcuts", this.emptyMessage = "No shortcuts registered.", this.headingId = `dp-shortcut-overlay-heading-${++C}`, y(this, i, (e) => {
18
+ e.key === "Escape" && (e.preventDefault(), f(this, n, p).call(this));
19
+ });
20
+ }
21
+ updated(e) {
22
+ e.has("open") && (this.open ? window.addEventListener("keydown", u(this, i)) : window.removeEventListener("keydown", u(this, i)));
23
+ }
24
+ disconnectedCallback() {
25
+ super.disconnectedCallback(), window.removeEventListener("keydown", u(this, i));
26
+ }
27
+ render() {
28
+ return this.open ? d`
29
+ <div class="backdrop" @click=${() => f(this, n, p).call(this)}>
30
+ <div
31
+ class="panel"
32
+ role="dialog"
33
+ aria-modal="true"
34
+ aria-labelledby=${this.headingId}
35
+ @click=${(e) => e.stopPropagation()}
36
+ >
37
+ <div class="header">
38
+ <h2 id=${this.headingId}>${this.heading}</h2>
39
+ <button class="close" aria-label="Close" @click=${() => f(this, n, p).call(this)}>×</button>
40
+ </div>
41
+ <div class="body">
42
+ ${this.shortcuts.length === 0 ? d`<p class="empty">${this.emptyMessage}</p>` : d`
43
+ <ul>
44
+ ${this.shortcuts.map(
45
+ (e) => d`
46
+ <li>
47
+ <span>${e.description}</span>
48
+ <kbd>${e.combo}</kbd>
49
+ </li>
50
+ `
51
+ )}
52
+ </ul>
53
+ `}
54
+ </div>
55
+ </div>
56
+ </div>
57
+ ` : w;
58
+ }
59
+ };
60
+ i = /* @__PURE__ */ new WeakMap();
61
+ n = /* @__PURE__ */ new WeakSet();
62
+ p = function() {
63
+ this.dispatchEvent(new z());
64
+ };
65
+ o.styles = x`
66
+ :host {
67
+ display: contents;
68
+ font-family: var(--dc-font-family, system-ui, sans-serif);
69
+ }
70
+ .backdrop {
71
+ position: fixed;
72
+ inset: 0;
73
+ z-index: 100;
74
+ display: flex;
75
+ align-items: center;
76
+ justify-content: center;
77
+ background: var(--dc-color-backdrop, rgba(0, 0, 0, 0.4));
78
+ }
79
+ .panel {
80
+ width: 420px;
81
+ max-height: 70vh;
82
+ overflow-y: auto;
83
+ background: var(--dc-color-surface, #f7f7f8);
84
+ border: 1px solid var(--dc-color-border, #e2e2e4);
85
+ border-radius: var(--dc-radius-lg, 10px);
86
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
87
+ }
88
+ .header {
89
+ display: flex;
90
+ align-items: center;
91
+ justify-content: space-between;
92
+ padding: var(--dc-space-3, 12px) var(--dc-space-4, 16px);
93
+ border-bottom: 1px solid var(--dc-color-border, #e2e2e4);
94
+ }
95
+ h2 {
96
+ margin: 0;
97
+ font-size: var(--dc-font-size-sm, 12px);
98
+ font-weight: var(--dc-font-weight-semibold, 600);
99
+ color: var(--dc-color-text, #1a1a1e);
100
+ }
101
+ .close {
102
+ appearance: none;
103
+ background: none;
104
+ border: none;
105
+ padding: 0;
106
+ font-size: var(--dc-font-size-lg, 15px);
107
+ line-height: 1;
108
+ color: var(--dc-color-text-muted, #8a8a92);
109
+ cursor: pointer;
110
+ }
111
+ .close:hover {
112
+ color: var(--dc-color-text, #1a1a1e);
113
+ }
114
+ .close:focus-visible {
115
+ outline: 2px solid var(--dc-color-accent, #2563eb);
116
+ outline-offset: 2px;
117
+ }
118
+ .body {
119
+ padding: var(--dc-space-3, 12px) var(--dc-space-4, 16px);
120
+ }
121
+ .empty {
122
+ margin: 0;
123
+ font-size: var(--dc-font-size-xs, 11px);
124
+ color: var(--dc-color-text-muted, #8a8a92);
125
+ }
126
+ ul {
127
+ list-style: none;
128
+ margin: 0;
129
+ padding: 0;
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: var(--dc-space-1, 4px);
133
+ }
134
+ li {
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: space-between;
138
+ gap: var(--dc-space-3, 12px);
139
+ font-size: var(--dc-font-size-xs, 11px);
140
+ }
141
+ kbd {
142
+ padding: 2px 6px;
143
+ border-radius: var(--dc-radius-sm, 4px);
144
+ background: var(--dc-color-surface-hover, #ececed);
145
+ border: 1px solid var(--dc-color-border, #e2e2e4);
146
+ color: var(--dc-color-text-secondary, #55555c);
147
+ font-family: ui-monospace, monospace;
148
+ font-size: 10px;
149
+ }
150
+ `;
151
+ a([
152
+ l({ type: Array })
153
+ ], o.prototype, "shortcuts", 2);
154
+ a([
155
+ l({ type: Boolean, reflect: !0 })
156
+ ], o.prototype, "open", 2);
157
+ a([
158
+ l()
159
+ ], o.prototype, "heading", 2);
160
+ a([
161
+ l({ attribute: "empty-message" })
162
+ ], o.prototype, "emptyMessage", 2);
163
+ a([
164
+ _()
165
+ ], o.prototype, "headingId", 2);
166
+ o = a([
167
+ k("dp-shortcut-overlay")
168
+ ], o);
169
+ export {
170
+ o as DpShortcutOverlay,
171
+ z as DpShortcutOverlayDismissEvent
172
+ };
@@ -0,0 +1,154 @@
1
+ import { css as m, LitElement as x, nothing as c, html as i } from "lit";
2
+ import { property as n, customElement as g } from "lit/decorators.js";
3
+ var y = Object.defineProperty, _ = Object.getOwnPropertyDescriptor, b = (e) => {
4
+ throw TypeError(e);
5
+ }, s = (e, t, r, p) => {
6
+ for (var a = p > 1 ? void 0 : p ? _(t, r) : t, l = e.length - 1, h; l >= 0; l--)
7
+ (h = e[l]) && (a = (p ? h(t, r, a) : h(a)) || a);
8
+ return p && a && y(t, r, a), a;
9
+ }, w = (e, t, r) => t.has(e) || b("Cannot " + r), I = (e, t, r) => t.has(e) ? b("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(e) : t.set(e, r), v = (e, t, r) => (w(e, t, "access private method"), r), d, f, u;
10
+ class $ extends Event {
11
+ constructor(t) {
12
+ super("dp-sidebar-select", { bubbles: !0, composed: !0 }), this.itemId = t;
13
+ }
14
+ }
15
+ let o = class extends x {
16
+ constructor() {
17
+ super(...arguments), I(this, d), this.items = [], this.bottomItems = [], this.activeId = "", this.header = "", this.collapsed = !1;
18
+ }
19
+ render() {
20
+ return i`
21
+ <div class="header">
22
+ <slot name="icon"></slot>
23
+ ${!this.collapsed && this.header ? i`<span class="header-label">${this.header}</span>` : c}
24
+ </div>
25
+ <nav>${this.items.map((e) => v(this, d, f).call(this, e))}</nav>
26
+ ${this.bottomItems.length > 0 ? i`<div class="bottom-group">${this.bottomItems.map((e) => v(this, d, f).call(this, e))}</div>` : c}
27
+ <div class="footer"><slot name="footer"></slot></div>
28
+ `;
29
+ }
30
+ };
31
+ d = /* @__PURE__ */ new WeakSet();
32
+ f = function(e) {
33
+ const t = e.id === this.activeId;
34
+ return i`
35
+ <button
36
+ title=${this.collapsed ? e.label : c}
37
+ aria-current=${t ? "page" : c}
38
+ @click=${() => v(this, d, u).call(this, e.id)}
39
+ >
40
+ <span class="icon" aria-hidden="true">${e.icon}</span>
41
+ ${this.collapsed ? c : i`<span>${e.label}</span>`}
42
+ </button>
43
+ `;
44
+ };
45
+ u = function(e) {
46
+ e !== this.activeId && (this.activeId = e, this.dispatchEvent(new $(e)));
47
+ };
48
+ o.styles = m`
49
+ :host {
50
+ display: flex;
51
+ flex-direction: column;
52
+ height: 100%;
53
+ box-sizing: border-box;
54
+ width: var(--dp-sidebar-width, 220px);
55
+ background: var(--dc-color-surface, #f7f7f8);
56
+ border-right: 1px solid var(--dc-color-border, #e2e2e4);
57
+ font-family: var(--dc-font-family, system-ui, sans-serif);
58
+ }
59
+ :host([collapsed]) {
60
+ width: var(--dp-sidebar-width-collapsed, 52px);
61
+ }
62
+ .header {
63
+ height: var(--dp-header-height, 52px);
64
+ display: flex;
65
+ align-items: center;
66
+ gap: var(--dc-space-3, 12px);
67
+ padding: 0 var(--dc-space-3, 12px);
68
+ border-bottom: 1px solid var(--dc-color-border, #e2e2e4);
69
+ box-sizing: border-box;
70
+ flex-shrink: 0;
71
+ }
72
+ :host([collapsed]) .header {
73
+ justify-content: center;
74
+ padding: 0;
75
+ }
76
+ .header-label {
77
+ font-size: var(--dc-font-size-xs, 11px);
78
+ font-weight: var(--dc-font-weight-medium, 500);
79
+ color: var(--dc-color-text-secondary, #55555c);
80
+ text-transform: uppercase;
81
+ letter-spacing: 0.02em;
82
+ }
83
+ nav {
84
+ flex: 1;
85
+ overflow-y: auto;
86
+ padding: var(--dc-space-1, 4px) 0;
87
+ }
88
+ .bottom-group {
89
+ border-top: 1px solid var(--dc-color-border, #e2e2e4);
90
+ padding: var(--dc-space-1, 4px) 0;
91
+ }
92
+ button {
93
+ appearance: none;
94
+ background: none;
95
+ border: none;
96
+ border-left: 2px solid transparent;
97
+ width: 100%;
98
+ display: flex;
99
+ align-items: center;
100
+ gap: var(--dc-space-3, 12px);
101
+ padding: var(--dc-space-2, 8px) var(--dc-space-3, 12px);
102
+ font: inherit;
103
+ font-size: var(--dc-font-size-sm, 12px);
104
+ color: var(--dc-color-text-secondary, #55555c);
105
+ text-align: left;
106
+ cursor: pointer;
107
+ }
108
+ :host([collapsed]) button {
109
+ justify-content: center;
110
+ padding: var(--dc-space-3, 12px) 0;
111
+ }
112
+ button[aria-current='page'] {
113
+ border-left-color: var(--dc-color-accent, #2563eb);
114
+ color: var(--dc-color-accent, #2563eb);
115
+ font-weight: var(--dc-font-weight-medium, 500);
116
+ }
117
+ button:hover {
118
+ background: var(--dc-color-surface-hover, #ececed);
119
+ }
120
+ button:focus-visible {
121
+ outline: 2px solid var(--dc-color-accent, #2563eb);
122
+ outline-offset: -2px;
123
+ }
124
+ .icon {
125
+ flex-shrink: 0;
126
+ width: 1.25em;
127
+ text-align: center;
128
+ }
129
+ .footer {
130
+ border-top: 1px solid var(--dc-color-border, #e2e2e4);
131
+ }
132
+ `;
133
+ s([
134
+ n({ type: Array })
135
+ ], o.prototype, "items", 2);
136
+ s([
137
+ n({ type: Array, attribute: "bottom-items" })
138
+ ], o.prototype, "bottomItems", 2);
139
+ s([
140
+ n({ attribute: "active-id" })
141
+ ], o.prototype, "activeId", 2);
142
+ s([
143
+ n()
144
+ ], o.prototype, "header", 2);
145
+ s([
146
+ n({ type: Boolean, reflect: !0 })
147
+ ], o.prototype, "collapsed", 2);
148
+ o = s([
149
+ g("dp-sidebar")
150
+ ], o);
151
+ export {
152
+ o as DpSidebar,
153
+ $ as DpSidebarSelectEvent
154
+ };
@@ -0,0 +1,108 @@
1
+ import { css as p, LitElement as g, nothing as h, html as d } from "lit";
2
+ import { property as s, customElement as v } from "lit/decorators.js";
3
+ var f = Object.defineProperty, b = Object.getOwnPropertyDescriptor, o = (i, r, l, a) => {
4
+ for (var e = a > 1 ? void 0 : a ? b(r, l) : r, n = i.length - 1, c; n >= 0; n--)
5
+ (c = i[n]) && (e = (a ? c(r, l, e) : c(e)) || e);
6
+ return a && e && f(r, l, e), e;
7
+ };
8
+ class u extends Event {
9
+ constructor() {
10
+ super("dp-toolbar-toggle", { bubbles: !0, composed: !0 });
11
+ }
12
+ }
13
+ let t = class extends g {
14
+ constructor() {
15
+ super(...arguments), this.heading = "", this.subtitle = "", this.showToggle = !1, this.dragRegion = !1;
16
+ }
17
+ render() {
18
+ return d`
19
+ <div class="start">
20
+ ${this.showToggle ? d`<button
21
+ class="toggle"
22
+ aria-label="Toggle sidebar"
23
+ @click=${() => this.dispatchEvent(new u())}
24
+ >
25
+ ☰
26
+ </button>` : h}
27
+ <span class="title">${this.subtitle || this.heading}</span>
28
+ </div>
29
+ <div class="drag-handle" part="drag-handle"></div>
30
+ <div class="actions"><slot name="actions"></slot></div>
31
+ `;
32
+ }
33
+ };
34
+ t.styles = p`
35
+ :host {
36
+ display: flex;
37
+ align-items: center;
38
+ height: var(--dp-header-height, 52px);
39
+ box-sizing: border-box;
40
+ padding: 0 var(--dc-space-4, 16px);
41
+ padding-inline-start: calc(var(--dc-space-4, 16px) + var(--dp-toolbar-inset-start, 0px));
42
+ padding-inline-end: calc(var(--dc-space-4, 16px) + var(--dp-toolbar-inset-end, 0px));
43
+ background: var(--dc-color-surface, #f7f7f8);
44
+ border-bottom: 1px solid var(--dc-color-border, #e2e2e4);
45
+ font-family: var(--dc-font-family, system-ui, sans-serif);
46
+ flex-shrink: 0;
47
+ }
48
+ .start {
49
+ display: flex;
50
+ align-items: center;
51
+ gap: var(--dc-space-4, 16px);
52
+ min-width: 0;
53
+ }
54
+ .title {
55
+ font-size: var(--dc-font-size-sm, 12px);
56
+ font-weight: var(--dc-font-weight-medium, 500);
57
+ color: var(--dc-color-text, #1a1a1e);
58
+ overflow: hidden;
59
+ text-overflow: ellipsis;
60
+ white-space: nowrap;
61
+ }
62
+ .toggle {
63
+ appearance: none;
64
+ background: none;
65
+ border: none;
66
+ padding: 0;
67
+ font-size: var(--dc-font-size-lg, 15px);
68
+ color: var(--dc-color-text-secondary, #55555c);
69
+ cursor: pointer;
70
+ line-height: 1;
71
+ }
72
+ .toggle:hover {
73
+ color: var(--dc-color-text, #1a1a1e);
74
+ }
75
+ .toggle:focus-visible {
76
+ outline: 2px solid var(--dc-color-accent, #2563eb);
77
+ outline-offset: 2px;
78
+ }
79
+ .drag-handle {
80
+ flex: 1;
81
+ align-self: stretch;
82
+ min-width: var(--dc-space-4, 16px);
83
+ }
84
+ .actions {
85
+ display: flex;
86
+ align-items: center;
87
+ gap: var(--dc-space-3, 12px);
88
+ }
89
+ `;
90
+ o([
91
+ s()
92
+ ], t.prototype, "heading", 2);
93
+ o([
94
+ s()
95
+ ], t.prototype, "subtitle", 2);
96
+ o([
97
+ s({ type: Boolean, attribute: "show-toggle" })
98
+ ], t.prototype, "showToggle", 2);
99
+ o([
100
+ s({ type: Boolean, reflect: !0, attribute: "drag-region" })
101
+ ], t.prototype, "dragRegion", 2);
102
+ t = o([
103
+ v("dp-toolbar")
104
+ ], t);
105
+ export {
106
+ t as DpToolbar,
107
+ u as DpToolbarToggleEvent
108
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@iyulab/desktop-patterns",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "tokens.css"
12
+ ],
13
+ "main": "./dist/index.js",
14
+ "module": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/index.d.ts",
19
+ "import": "./dist/index.js"
20
+ },
21
+ "./page": {
22
+ "types": "./dist/components/page/dp-page.d.ts",
23
+ "import": "./dist/page.js"
24
+ },
25
+ "./tokens.css": "./tokens.css"
26
+ },
27
+ "scripts": {
28
+ "build": "vite build",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "web-test-runner \"src/**/*.test.ts\"",
31
+ "test:watch": "web-test-runner \"src/**/*.test.ts\" --watch",
32
+ "guard": "node scripts/check-forge-tokens.mjs",
33
+ "guard:test": "node --test scripts/*.test.mjs",
34
+ "storybook": "storybook dev -p 6007",
35
+ "build-storybook": "storybook build"
36
+ },
37
+ "dependencies": {
38
+ "@iyulab/desktop-compact": "^0.1.0",
39
+ "lit": "^3.2.1"
40
+ },
41
+ "devDependencies": {
42
+ "@open-wc/testing": "^4.0.0",
43
+ "@storybook/addon-essentials": "^8.3.0",
44
+ "@storybook/web-components": "^8.3.0",
45
+ "@storybook/web-components-vite": "^8.3.0",
46
+ "@types/mocha": "^10.0.10",
47
+ "@web/dev-server-esbuild": "^1.0.2",
48
+ "@web/test-runner": "^0.19.0",
49
+ "storybook": "^8.3.0",
50
+ "typescript": "^5.6.3",
51
+ "vite": "^5.4.10",
52
+ "vite-plugin-dts": "^4.2.4"
53
+ }
54
+ }
package/tokens.css ADDED
@@ -0,0 +1,14 @@
1
+ /*
2
+ * Structural tokens for @iyulab/desktop-patterns.
3
+ *
4
+ * This file adds ONLY the dimensions this layer introduces that
5
+ * @iyulab/desktop-compact has no concept of (sidebar/header sizing). Color,
6
+ * spacing-scale, radius, and typography tokens are NOT duplicated here —
7
+ * import @iyulab/desktop-compact/tokens.css alongside this file; every
8
+ * desktop-patterns component consumes --dc-* directly.
9
+ */
10
+ :root {
11
+ --dp-sidebar-width: 220px;
12
+ --dp-sidebar-width-collapsed: 52px;
13
+ --dp-header-height: 52px;
14
+ }