@iyulab/desktop-patterns 0.3.4 → 0.5.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/README.md CHANGED
@@ -33,7 +33,7 @@ import '@iyulab/desktop-patterns'
33
33
  ## Usage
34
34
 
35
35
  ```html
36
- <dp-shell sidebar-open>
36
+ <dp-shell>
37
37
  <dp-sidebar slot="sidebar" active-id="overview" header="My App" id="nav"></dp-sidebar>
38
38
  <dp-toolbar slot="toolbar" heading="My App" show-toggle></dp-toolbar>
39
39
  <dp-page>
@@ -51,14 +51,40 @@ document.querySelector<HTMLElementTagNameMap['dp-sidebar']>('#nav')!.items = [
51
51
  { id: 'overview', icon: '■', label: 'Overview' },
52
52
  ]
53
53
 
54
- document.querySelector('dp-sidebar')!.addEventListener('dp-sidebar-select', (e) => {
54
+ import { desktopMinWidth } from '@iyulab/desktop-patterns'
55
+
56
+ const shell = document.querySelector('dp-shell')!
57
+ const sidebar = document.querySelector('dp-sidebar')!
58
+ const toolbar = document.querySelector('dp-toolbar')!
59
+ const wide = matchMedia(`(min-width: ${desktopMinWidth}px)`)
60
+ let collapsed = false // the consumer's own state: a wide window's sidebar folded to its rail
61
+
62
+ function sync() {
63
+ sidebar.collapsed = collapsed && wide.matches // a drawer always shows the whole sidebar
64
+ toolbar.expanded = wide.matches ? !collapsed : shell.sidebarOpen
65
+ }
66
+ wide.addEventListener('change', sync)
67
+ sidebar.addEventListener('dp-sidebar-select', () => {
55
68
  // consumer owns activeId / routing
69
+ shell.sidebarOpen = false // a drawer gives way to what was picked
70
+ sync()
56
71
  })
57
- document.querySelector('dp-toolbar')!.addEventListener('dp-toolbar-toggle', () => {
58
- // consumer owns sidebar-open state on dp-shell
72
+ toolbar.addEventListener('dp-toolbar-toggle', () => {
73
+ if (wide.matches) collapsed = !collapsed
74
+ else shell.sidebarOpen = !shell.sidebarOpen
75
+ sync()
59
76
  })
77
+ shell.addEventListener('dp-shell-sidebar-close', () => { shell.sidebarOpen = false; sync() }) // backdrop, Escape
60
78
  ```
61
79
 
80
+ Below the desktop breakpoint (`desktopMinWidth`, 1024px — `desktopMedia` for a component's styles)
81
+ the sidebar is a drawer over the content, open while `sidebar-open` is set; at and above it the
82
+ sidebar sits beside the content and `sidebar-open` changes nothing. The same toolbar toggle serves
83
+ both widths: in a narrow window it opens the drawer, in a wide one it folds the sidebar to its
84
+ collapsed rail (`dp-sidebar` `collapsed`). Give the toggle `expanded` so assistive technology hears
85
+ which state it is in. The consumer owns both states — start the drawer closed, close it when the
86
+ shell asks (backdrop click, Escape) and when a place is picked, or a narrow window stays covered.
87
+
62
88
  ### Sidebar items: links, groups, icons, actions
63
89
 
64
90
  ```ts
@@ -125,9 +151,9 @@ dp-toolbar[drag-region]::part(drag-handle) {
125
151
 
126
152
  | Component | Description |
127
153
  |---|---|
128
- | `dp-page` | Scrollable content region with a centered, max-width column — the page-body every view renders into |
154
+ | `dp-page` | Scrollable content region with a centered, max-width column — the page-body every view renders into. With `fill` it does not scroll and its column is as tall as the region, for a view that scrolls its own parts (a list beside the item open) |
129
155
  | `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 |
130
- | `dp-toolbar` | App-shell header bar — `heading`/`subtitle`, an optional collapse toggle, a right-side `actions` slot |
156
+ | `dp-toolbar` | App-shell header bar — `heading`/`subtitle`, an optional sidebar toggle (drawer in a narrow window, collapse in a wide one), a right-side `actions` slot |
131
157
  | `dp-shell` | Top-level layout composing `sidebar`/`toolbar`/`banner`/main-content regions, with a responsive drawer (backdrop + overlay sidebar) below a 1024px breakpoint |
132
158
  | `dp-shortcut-overlay` | Display-only keyboard-shortcuts help panel — a `shortcuts` list + `open` flag; no registration/binding infrastructure (that stays the consumer's) |
133
159
 
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The desktop breakpoint, in CSS pixels. Below it `dp-shell`'s sidebar is a drawer over the
3
+ * content; at and above it the sidebar sits beside the content, where a consumer may fold
4
+ * `dp-sidebar` to its collapsed rail instead.
5
+ */
6
+ export declare const desktopMinWidth = 1024;
7
+ /** The media query for {@link desktopMinWidth}, for a component's styles. */
8
+ export declare const desktopMedia: import('lit').CSSResult;
@@ -4,10 +4,17 @@ export type PageMaxWidth = 'sm' | 'md' | 'lg' | 'xl' | 'full';
4
4
  * Scrollable content region with a centered, max-width column — the page-body
5
5
  * pattern every view in an app shell renders into. Pure layout: it has no
6
6
  * opinion on what it contains (default slot only).
7
+ *
8
+ * With `fill`, the page does not scroll: its column fills the region's height
9
+ * (a flex column), for a view that divides its own scrolling — a list beside
10
+ * the item open, where the list scrolls and the item stays in view. The
11
+ * slotted view sizes itself within that height (e.g. `flex: 1; min-height: 0`).
7
12
  */
8
13
  export declare class DpPage extends LitElement {
9
14
  static styles: import('lit').CSSResult;
10
15
  maxWidth: PageMaxWidth;
16
+ /** Fill the region's height instead of scrolling: the view inside divides its own scrolling. */
17
+ fill: boolean;
11
18
  render(): import('lit-html').TemplateResult<1>;
12
19
  }
13
20
  declare global {
@@ -28,7 +28,16 @@ export declare class DpShellSidebarCloseEvent extends Event {
28
28
  */
29
29
  export declare class DpShell extends LitElement {
30
30
  static styles: import('lit').CSSResult;
31
+ /**
32
+ * Whether the sidebar is open as a drawer below the desktop breakpoint. Above it the sidebar is
33
+ * always shown beside the content and this changes nothing. The consumer owns it: start it closed,
34
+ * and set it back to false on `dp-shell-sidebar-close` (backdrop click or Escape) and after a
35
+ * place is picked in the drawer.
36
+ */
31
37
  sidebarOpen: boolean;
38
+ private readonly onKeydown;
39
+ connectedCallback(): void;
40
+ disconnectedCallback(): void;
32
41
  render(): import('lit-html').TemplateResult<1>;
33
42
  }
34
43
  declare global {
@@ -4,8 +4,9 @@ export declare class DpToolbarToggleEvent extends Event {
4
4
  }
5
5
  /**
6
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.
7
+ * `subtitle`), an optional sidebar toggle — the consumer decides what it does
8
+ * (open `dp-shell`'s drawer in a narrow window, fold `dp-sidebar` to its rail
9
+ * in a wide one) — and a right-side `actions` slot. Pure layout — this repo does not call any platform API.
9
10
  *
10
11
  * Named `heading`, not `title` — matches dc-section-heading's own
11
12
  * established convention for this kind of headline text, and avoids
@@ -42,6 +43,8 @@ export declare class DpToolbar extends LitElement {
42
43
  heading: string;
43
44
  subtitle: string;
44
45
  showToggle: boolean;
46
+ /** Whether the sidebar the toggle controls is shown in full — announced as the toggle's `aria-expanded`. */
47
+ expanded: boolean;
45
48
  /** Accessible name of the sidebar toggle — supply one in the app's language. */
46
49
  toggleLabel: string;
47
50
  dragRegion: boolean;
@@ -0,0 +1,108 @@
1
+ import { t as e } from "./decorate-CafwLXg5.js";
2
+ import { LitElement as t, css as n, html as r, nothing as i, unsafeCSS as a } from "lit";
3
+ import { customElement as o, property as s } from "lit/decorators.js";
4
+ //#region src/breakpoints.ts
5
+ var c = 1024, l = a(`(min-width: ${c}px)`), u = class extends Event {
6
+ constructor() {
7
+ super("dp-shell-sidebar-close", {
8
+ bubbles: !0,
9
+ composed: !0
10
+ });
11
+ }
12
+ }, d = class extends t {
13
+ constructor(...e) {
14
+ super(...e), this.sidebarOpen = !1, this.onKeydown = (e) => {
15
+ e.key === "Escape" && !e.defaultPrevented && this.sidebarOpen && this.dispatchEvent(new u());
16
+ };
17
+ }
18
+ static {
19
+ this.styles = n`
20
+ :host {
21
+ display: flex;
22
+ width: 100vw;
23
+ height: 100vh;
24
+ overflow: hidden;
25
+ box-sizing: border-box;
26
+ background: var(--dc-color-bg, #ffffff);
27
+ color: var(--dc-color-text, #1a1a1e);
28
+ font-family: var(--dc-font-family, system-ui, sans-serif);
29
+ }
30
+ .backdrop {
31
+ position: fixed;
32
+ inset: 0;
33
+ z-index: 30;
34
+ background: var(--dc-color-backdrop, rgba(0, 0, 0, 0.4));
35
+ }
36
+ .sidebar-region {
37
+ flex-shrink: 0;
38
+ display: none;
39
+ }
40
+ :host([sidebar-open]) .sidebar-region {
41
+ display: block;
42
+ position: fixed;
43
+ inset-block: 0;
44
+ inset-inline-start: 0;
45
+ z-index: 40;
46
+ }
47
+ @media ${l} {
48
+ /* Must match (or exceed) \`:host([sidebar-open]) .sidebar-region\`'s
49
+ specificity above (0,3,0) — a bare \`.sidebar-region\` here is only
50
+ (0,1,0), so on equal-or-narrower cascades the desktop override would
51
+ lose to the sidebar-open rule regardless of this media query
52
+ matching, leaving the sidebar permanently \`position: fixed\` (full-
53
+ height overlay) at any width once \`sidebar-open\` is set. Repeating
54
+ the \`:host([sidebar-open])\` prefix here, rather than only the bare
55
+ selector, is what actually lets desktop width win. */
56
+ :host([sidebar-open]) .sidebar-region,
57
+ .sidebar-region {
58
+ display: block;
59
+ position: relative;
60
+ inset: auto;
61
+ z-index: auto;
62
+ }
63
+ .backdrop {
64
+ display: none;
65
+ }
66
+ }
67
+ .main-column {
68
+ display: flex;
69
+ flex-direction: column;
70
+ flex: 1;
71
+ min-width: 0;
72
+ }
73
+ .toolbar-row {
74
+ flex-shrink: 0;
75
+ }
76
+ main {
77
+ flex: 1;
78
+ overflow: auto;
79
+ }
80
+ `;
81
+ }
82
+ connectedCallback() {
83
+ super.connectedCallback(), document.addEventListener("keydown", this.onKeydown);
84
+ }
85
+ disconnectedCallback() {
86
+ document.removeEventListener("keydown", this.onKeydown), super.disconnectedCallback();
87
+ }
88
+ render() {
89
+ return r`
90
+ ${this.sidebarOpen ? r`<div class="backdrop" @click=${() => this.dispatchEvent(new u())}></div>` : i}
91
+ <div class="sidebar-region"><slot name="sidebar"></slot></div>
92
+ <div class="main-column">
93
+ <div class="toolbar-row">
94
+ <slot name="toolbar"></slot>
95
+ </div>
96
+ <slot name="banner"></slot>
97
+ <main><slot></slot></main>
98
+ </div>
99
+ `;
100
+ }
101
+ };
102
+ e([s({
103
+ type: Boolean,
104
+ reflect: !0,
105
+ attribute: "sidebar-open"
106
+ })], d.prototype, "sidebarOpen", void 0), d = e([o("dp-shell")], d);
107
+ //#endregion
108
+ export { c as i, u as n, l as r, d as t };
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './breakpoints.js';
1
2
  export * from './components/page/dp-page.js';
2
3
  export * from './components/sidebar/dp-sidebar.js';
3
4
  export * from './components/toolbar/dp-toolbar.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { DpPage as e } from "./page.js";
2
- import { DpSidebar as t, DpSidebarActionEvent as n, DpSidebarSelectEvent as r } from "./sidebar.js";
3
- import { DpToolbar as i, DpToolbarToggleEvent as a } from "./toolbar.js";
4
- import { DpShell as o, DpShellSidebarCloseEvent as s } from "./shell.js";
5
- import { DpShortcutOverlay as c, DpShortcutOverlayDismissEvent as l } from "./shortcut-overlay.js";
6
- export { e as DpPage, o as DpShell, s as DpShellSidebarCloseEvent, c as DpShortcutOverlay, l as DpShortcutOverlayDismissEvent, t as DpSidebar, n as DpSidebarActionEvent, r as DpSidebarSelectEvent, i as DpToolbar, a as DpToolbarToggleEvent };
1
+ import { i as e, n as t, r as n, t as r } from "./dp-shell-CvV92B8B.js";
2
+ import { DpPage as i } from "./page.js";
3
+ import { DpSidebar as a, DpSidebarActionEvent as o, DpSidebarSelectEvent as s } from "./sidebar.js";
4
+ import { DpToolbar as c, DpToolbarToggleEvent as l } from "./toolbar.js";
5
+ import { DpShortcutOverlay as u, DpShortcutOverlayDismissEvent as d } from "./shortcut-overlay.js";
6
+ export { i as DpPage, r as DpShell, t as DpShellSidebarCloseEvent, u as DpShortcutOverlay, d as DpShortcutOverlayDismissEvent, a as DpSidebar, o as DpSidebarActionEvent, s as DpSidebarSelectEvent, c as DpToolbar, l as DpToolbarToggleEvent, n as desktopMedia, e as desktopMinWidth };
package/dist/page.js CHANGED
@@ -10,7 +10,7 @@ var o = {
10
10
  full: "none"
11
11
  }, s = class extends t {
12
12
  constructor(...e) {
13
- super(...e), this.maxWidth = "md";
13
+ super(...e), this.maxWidth = "md", this.fill = !1;
14
14
  }
15
15
  static {
16
16
  this.styles = n`
@@ -25,6 +25,14 @@ var o = {
25
25
  padding: var(--dc-space-6, 24px);
26
26
  box-sizing: border-box;
27
27
  }
28
+ :host([fill]) {
29
+ overflow: hidden;
30
+ }
31
+ :host([fill]) .inner {
32
+ display: flex;
33
+ flex-direction: column;
34
+ height: 100%;
35
+ }
28
36
  `;
29
37
  }
30
38
  render() {
@@ -38,6 +46,9 @@ var o = {
38
46
  e([a({
39
47
  attribute: "max-width",
40
48
  reflect: !0
41
- })], s.prototype, "maxWidth", void 0), s = e([i("dp-page")], s);
49
+ })], s.prototype, "maxWidth", void 0), e([a({
50
+ type: Boolean,
51
+ reflect: !0
52
+ })], s.prototype, "fill", void 0), s = e([i("dp-page")], s);
42
53
  //#endregion
43
54
  export { s as DpPage };
package/dist/shell.js CHANGED
@@ -1,100 +1,2 @@
1
- import { t as e } from "./decorate-CafwLXg5.js";
2
- import { LitElement as t, css as n, html as r, nothing as i } from "lit";
3
- import { customElement as a, property as o } from "lit/decorators.js";
4
- //#region src/components/shell/dp-shell.ts
5
- var s = class extends Event {
6
- constructor() {
7
- super("dp-shell-sidebar-close", {
8
- bubbles: !0,
9
- composed: !0
10
- });
11
- }
12
- }, c = class extends t {
13
- constructor(...e) {
14
- super(...e), this.sidebarOpen = !1;
15
- }
16
- static {
17
- this.styles = n`
18
- :host {
19
- display: flex;
20
- width: 100vw;
21
- height: 100vh;
22
- overflow: hidden;
23
- box-sizing: border-box;
24
- background: var(--dc-color-bg, #ffffff);
25
- color: var(--dc-color-text, #1a1a1e);
26
- font-family: var(--dc-font-family, system-ui, sans-serif);
27
- }
28
- .backdrop {
29
- position: fixed;
30
- inset: 0;
31
- z-index: 30;
32
- background: var(--dc-color-backdrop, rgba(0, 0, 0, 0.4));
33
- }
34
- .sidebar-region {
35
- flex-shrink: 0;
36
- display: none;
37
- }
38
- :host([sidebar-open]) .sidebar-region {
39
- display: block;
40
- position: fixed;
41
- inset-block: 0;
42
- inset-inline-start: 0;
43
- z-index: 40;
44
- }
45
- @media (min-width: 1024px) {
46
- /* Must match (or exceed) \`:host([sidebar-open]) .sidebar-region\`'s
47
- specificity above (0,3,0) — a bare \`.sidebar-region\` here is only
48
- (0,1,0), so on equal-or-narrower cascades the desktop override would
49
- lose to the sidebar-open rule regardless of this media query
50
- matching, leaving the sidebar permanently \`position: fixed\` (full-
51
- height overlay) at any width once \`sidebar-open\` is set. Repeating
52
- the \`:host([sidebar-open])\` prefix here, rather than only the bare
53
- selector, is what actually lets desktop width win. */
54
- :host([sidebar-open]) .sidebar-region,
55
- .sidebar-region {
56
- display: block;
57
- position: relative;
58
- inset: auto;
59
- z-index: auto;
60
- }
61
- .backdrop {
62
- display: none;
63
- }
64
- }
65
- .main-column {
66
- display: flex;
67
- flex-direction: column;
68
- flex: 1;
69
- min-width: 0;
70
- }
71
- .toolbar-row {
72
- flex-shrink: 0;
73
- }
74
- main {
75
- flex: 1;
76
- overflow: auto;
77
- }
78
- `;
79
- }
80
- render() {
81
- return r`
82
- ${this.sidebarOpen ? r`<div class="backdrop" @click=${() => this.dispatchEvent(new s())}></div>` : i}
83
- <div class="sidebar-region"><slot name="sidebar"></slot></div>
84
- <div class="main-column">
85
- <div class="toolbar-row">
86
- <slot name="toolbar"></slot>
87
- </div>
88
- <slot name="banner"></slot>
89
- <main><slot></slot></main>
90
- </div>
91
- `;
92
- }
93
- };
94
- e([o({
95
- type: Boolean,
96
- reflect: !0,
97
- attribute: "sidebar-open"
98
- })], c.prototype, "sidebarOpen", void 0), c = e([a("dp-shell")], c);
99
- //#endregion
100
- export { c as DpShell, s as DpShellSidebarCloseEvent };
1
+ import { n as e, t } from "./dp-shell-CvV92B8B.js";
2
+ export { t as DpShell, e as DpShellSidebarCloseEvent };
package/dist/toolbar.js CHANGED
@@ -11,7 +11,7 @@ var s = class extends Event {
11
11
  }
12
12
  }, c = class extends t {
13
13
  constructor(...e) {
14
- super(...e), this.heading = "", this.subtitle = "", this.showToggle = !1, this.toggleLabel = "Toggle sidebar", this.dragRegion = !1;
14
+ super(...e), this.heading = "", this.subtitle = "", this.showToggle = !1, this.expanded = !1, this.toggleLabel = "Toggle sidebar", this.dragRegion = !1;
15
15
  }
16
16
  static {
17
17
  this.styles = n`
@@ -80,6 +80,7 @@ var s = class extends Event {
80
80
  ${this.showToggle ? r`<button
81
81
  class="toggle"
82
82
  aria-label=${this.toggleLabel}
83
+ aria-expanded=${this.expanded ? "true" : "false"}
83
84
  @click=${() => this.dispatchEvent(new s())}
84
85
  >
85
86
  ☰
@@ -94,7 +95,7 @@ var s = class extends Event {
94
95
  e([o()], c.prototype, "heading", void 0), e([o()], c.prototype, "subtitle", void 0), e([o({
95
96
  type: Boolean,
96
97
  attribute: "show-toggle"
97
- })], c.prototype, "showToggle", void 0), e([o({ attribute: "toggle-label" })], c.prototype, "toggleLabel", void 0), e([o({
98
+ })], c.prototype, "showToggle", void 0), e([o({ type: Boolean })], c.prototype, "expanded", void 0), e([o({ attribute: "toggle-label" })], c.prototype, "toggleLabel", void 0), e([o({
98
99
  type: Boolean,
99
100
  reflect: !0,
100
101
  attribute: "drag-region"
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "@iyulab/desktop-patterns",
3
- "version": "0.3.4",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/iyulab/desktop-patterns.git"
9
+ },
10
+ "homepage": "https://github.com/iyulab/desktop-patterns#readme",
11
+ "bugs": "https://github.com/iyulab/desktop-patterns/issues",
6
12
  "engines": {
7
13
  "node": ">=22"
8
14
  },
@@ -48,6 +54,7 @@
48
54
  },
49
55
  "scripts": {
50
56
  "build": "vite build",
57
+ "prepack": "npm run build",
51
58
  "typecheck": "tsc --noEmit",
52
59
  "test": "web-test-runner \"src/**/*.test.ts\"",
53
60
  "test:watch": "web-test-runner \"src/**/*.test.ts\" --watch",
@@ -63,7 +70,7 @@
63
70
  "@iyulab/desktop-compact": "^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0"
64
71
  },
65
72
  "devDependencies": {
66
- "@iyulab/desktop-compact": "^0.8.0",
73
+ "@iyulab/desktop-compact": "^0.8.2",
67
74
  "@open-wc/testing": "^5.0.0",
68
75
  "@storybook/web-components": "^10.6.0",
69
76
  "@storybook/web-components-vite": "^10.6.0",