@f-ewald/components 1.2.0 → 1.3.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.
Files changed (63) hide show
  1. package/README.md +15 -2
  2. package/custom-elements.json +1020 -76
  3. package/dist/action-bar.d.ts +24 -0
  4. package/dist/action-bar.d.ts.map +1 -0
  5. package/dist/action-bar.js +67 -0
  6. package/dist/action-bar.js.map +1 -0
  7. package/dist/app-shell.d.ts +86 -0
  8. package/dist/app-shell.d.ts.map +1 -0
  9. package/dist/app-shell.js +460 -0
  10. package/dist/app-shell.js.map +1 -0
  11. package/dist/app-sidebar.d.ts +44 -0
  12. package/dist/app-sidebar.d.ts.map +1 -0
  13. package/dist/app-sidebar.js +211 -0
  14. package/dist/app-sidebar.js.map +1 -0
  15. package/dist/form-actions.d.ts +27 -0
  16. package/dist/form-actions.d.ts.map +1 -0
  17. package/dist/form-actions.js +65 -0
  18. package/dist/form-actions.js.map +1 -0
  19. package/dist/icons.d.ts +1 -0
  20. package/dist/icons.d.ts.map +1 -1
  21. package/dist/icons.js +1 -0
  22. package/dist/icons.js.map +1 -1
  23. package/dist/index.d.ts +8 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +8 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/mcp-server.js +82 -5
  28. package/dist/mcp-server.js.map +1 -1
  29. package/dist/page-header.d.ts +27 -0
  30. package/dist/page-header.d.ts.map +1 -0
  31. package/dist/page-header.js +106 -0
  32. package/dist/page-header.js.map +1 -0
  33. package/dist/pagination-nav.d.ts +34 -0
  34. package/dist/pagination-nav.d.ts.map +1 -0
  35. package/dist/pagination-nav.js +144 -0
  36. package/dist/pagination-nav.js.map +1 -0
  37. package/dist/popover-panel.js +1 -1
  38. package/dist/popover-panel.js.map +1 -1
  39. package/dist/timeline-container.d.ts +22 -0
  40. package/dist/timeline-container.d.ts.map +1 -0
  41. package/dist/timeline-container.js +42 -0
  42. package/dist/timeline-container.js.map +1 -0
  43. package/dist/timeline-entry.d.ts +31 -0
  44. package/dist/timeline-entry.d.ts.map +1 -0
  45. package/dist/timeline-entry.js +170 -0
  46. package/dist/timeline-entry.js.map +1 -0
  47. package/docs/action-bar.md +41 -0
  48. package/docs/app-shell.md +85 -0
  49. package/docs/app-sidebar.md +69 -0
  50. package/docs/design-language.md +74 -1
  51. package/docs/form-actions.md +43 -0
  52. package/docs/layouts/detail-only.md +52 -0
  53. package/docs/layouts/form-page.md +52 -0
  54. package/docs/layouts/list-detail.md +52 -0
  55. package/docs/layouts/list-only.md +50 -0
  56. package/docs/layouts/record-detail.md +88 -0
  57. package/docs/mcp-evaluation.md +21 -0
  58. package/docs/page-header.md +47 -0
  59. package/docs/pagination-nav.md +54 -0
  60. package/docs/timeline-container.md +44 -0
  61. package/docs/timeline-entry.md +52 -0
  62. package/llms.txt +229 -0
  63. package/package.json +1 -1
@@ -0,0 +1,24 @@
1
+ import { LitElement } from "lit";
2
+ /**
3
+ * Toolbar that sits directly above a list or table: a left cluster for
4
+ * search and filters and a right cluster for record actions (create, delete,
5
+ * bulk actions). It's a presentational layout container only — drop any
6
+ * controls (`autocomplete-input`, `multi-select`, `ui-button`, …) into the
7
+ * `start` and `end` slots; the bar owns none of their behavior and adds no
8
+ * search field of its own. The two clusters wrap onto separate rows when the
9
+ * bar is too narrow.
10
+ *
11
+ * @element action-bar
12
+ * @slot start - Left cluster: search and filter controls.
13
+ * @slot end - Right cluster: record actions (create, delete, …).
14
+ */
15
+ export declare class ActionBar extends LitElement {
16
+ static styles: import("lit").CSSResult[];
17
+ render(): import("lit-html").TemplateResult<1>;
18
+ }
19
+ declare global {
20
+ interface HTMLElementTagNameMap {
21
+ "action-bar": ActionBar;
22
+ }
23
+ }
24
+ //# sourceMappingURL=action-bar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-bar.d.ts","sourceRoot":"","sources":["../src/action-bar.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,KAAK,CAAC;AAI5C;;;;;;;;;;;;GAYG;AACH,qBACa,SAAU,SAAQ,UAAU;IACvC,OAAgB,MAAM,4BA6BpB;IAEO,MAAM,yCAOd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,YAAY,EAAE,SAAS,CAAC;KACzB;CACF"}
@@ -0,0 +1,67 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { LitElement, css, html } from "lit";
8
+ import { customElement } from "lit/decorators.js";
9
+ import { tokens } from "./tokens.js";
10
+ /**
11
+ * Toolbar that sits directly above a list or table: a left cluster for
12
+ * search and filters and a right cluster for record actions (create, delete,
13
+ * bulk actions). It's a presentational layout container only — drop any
14
+ * controls (`autocomplete-input`, `multi-select`, `ui-button`, …) into the
15
+ * `start` and `end` slots; the bar owns none of their behavior and adds no
16
+ * search field of its own. The two clusters wrap onto separate rows when the
17
+ * bar is too narrow.
18
+ *
19
+ * @element action-bar
20
+ * @slot start - Left cluster: search and filter controls.
21
+ * @slot end - Right cluster: record actions (create, delete, …).
22
+ */
23
+ let ActionBar = class ActionBar extends LitElement {
24
+ static { this.styles = [
25
+ tokens,
26
+ css `
27
+ :host {
28
+ display: block;
29
+ }
30
+ .bar {
31
+ display: flex;
32
+ flex-wrap: wrap;
33
+ align-items: center;
34
+ gap: 0.5rem;
35
+ }
36
+ .start {
37
+ display: flex;
38
+ flex-wrap: wrap;
39
+ align-items: center;
40
+ gap: 0.5rem;
41
+ flex: 1 1 16rem;
42
+ min-width: 0;
43
+ }
44
+ .end {
45
+ display: flex;
46
+ flex-wrap: wrap;
47
+ align-items: center;
48
+ gap: 0.5rem;
49
+ flex: 0 0 auto;
50
+ margin-left: auto;
51
+ }
52
+ `,
53
+ ]; }
54
+ render() {
55
+ return html `
56
+ <div class="bar">
57
+ <div class="start"><slot name="start"></slot></div>
58
+ <div class="end"><slot name="end"></slot></div>
59
+ </div>
60
+ `;
61
+ }
62
+ };
63
+ ActionBar = __decorate([
64
+ customElement("action-bar")
65
+ ], ActionBar);
66
+ export { ActionBar };
67
+ //# sourceMappingURL=action-bar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action-bar.js","sourceRoot":"","sources":["../src/action-bar.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,KAAK,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;;;;;GAYG;AAEI,IAAM,SAAS,GAAf,MAAM,SAAU,SAAQ,UAAU;aACvB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;KA0BF;KACF,AA7BqB,CA6BpB;IAEO,MAAM;QACb,OAAO,IAAI,CAAA;;;;;KAKV,CAAC;IACJ,CAAC;CACF,CAAA;AAxCY,SAAS;IADrB,aAAa,CAAC,YAAY,CAAC;GACf,SAAS,CAwCrB","sourcesContent":["import { LitElement, css, html } from \"lit\";\nimport { customElement } from \"lit/decorators.js\";\nimport { tokens } from \"./tokens.js\";\n\n/**\n * Toolbar that sits directly above a list or table: a left cluster for\n * search and filters and a right cluster for record actions (create, delete,\n * bulk actions). It's a presentational layout container only — drop any\n * controls (`autocomplete-input`, `multi-select`, `ui-button`, …) into the\n * `start` and `end` slots; the bar owns none of their behavior and adds no\n * search field of its own. The two clusters wrap onto separate rows when the\n * bar is too narrow.\n *\n * @element action-bar\n * @slot start - Left cluster: search and filter controls.\n * @slot end - Right cluster: record actions (create, delete, …).\n */\n@customElement(\"action-bar\")\nexport class ActionBar extends LitElement {\n static override styles = [\n tokens,\n css`\n :host {\n display: block;\n }\n .bar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem;\n }\n .start {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem;\n flex: 1 1 16rem;\n min-width: 0;\n }\n .end {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem;\n flex: 0 0 auto;\n margin-left: auto;\n }\n `,\n ];\n\n override render() {\n return html`\n <div class=\"bar\">\n <div class=\"start\"><slot name=\"start\"></slot></div>\n <div class=\"end\"><slot name=\"end\"></slot></div>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"action-bar\": ActionBar;\n }\n}\n"]}
@@ -0,0 +1,86 @@
1
+ import { LitElement, type PropertyValues } from "lit";
2
+ import "./kbd-hint.js";
3
+ /** Fired when the built-in toggle changes the sidebar state. */
4
+ export interface SidebarToggleDetail {
5
+ /** Desktop rail state. */
6
+ collapsed: boolean;
7
+ /** Mobile drawer open state. */
8
+ mobileOpen: boolean;
9
+ }
10
+ /**
11
+ * The dashboard page shell: a slot-based CSS-grid backbone that arranges a
12
+ * full-height sidebar, a top bar, the main content, an optional right-hand
13
+ * detail column, and an optional footer. It owns the responsive behavior so
14
+ * consumers don't re-implement it — above the shared 48rem breakpoint the
15
+ * sidebar collapses to an icon rail and the detail region is an inline column;
16
+ * at or below it the sidebar becomes an off-canvas drawer and the detail region
17
+ * an overlay, both dismissed by a scrim or Escape.
18
+ *
19
+ * Widths are tunable per instance via `--component-sidebar-width` (16rem),
20
+ * `--component-sidebar-rail-width` (3.5rem), and `--component-topbar-height`
21
+ * (3rem); the detail column reuses the 20rem/25rem panel widths. The main
22
+ * content area is white by default — override it with
23
+ * `--component-main-background`. Give the shell a height (e.g. `height: 100vh`)
24
+ * so the sidebar and main can size and scroll.
25
+ *
26
+ * The built-in top-bar button toggles the sidebar, and so does pressing `[`
27
+ * anywhere on the page (ignored while typing in a text field or with a
28
+ * modifier held). Hovering or keyboard-focusing the toggle reveals a tooltip
29
+ * naming the action and its `[` shortcut — the shortcut is not shown as
30
+ * permanent chrome.
31
+ *
32
+ * @element app-shell
33
+ * @slot - Main content area.
34
+ * @slot sidebar - Full-height navigation (typically `app-sidebar`).
35
+ * @slot topbar - Top bar content, right of the built-in toggle.
36
+ * @slot detail - Optional right-hand detail; shown when `detail-open` is set.
37
+ * @slot footer - Optional footer beneath the main content.
38
+ * @fires sidebar-toggle - The built-in toggle changed the sidebar state.
39
+ * @fires detail-close - The scrim or Escape dismissed the mobile detail overlay.
40
+ */
41
+ export declare class AppShell extends LitElement {
42
+ /** Collapses the sidebar to an icon rail on desktop. */
43
+ sidebarCollapsed: boolean;
44
+ /** Shows the right-hand detail region (inline column, or overlay on mobile). */
45
+ detailOpen: boolean;
46
+ /** Detail width: `compact` (20rem) or `comfortable` (25rem). */
47
+ detailWidth: "compact" | "comfortable";
48
+ /** Whether the viewport is at/below the 48rem breakpoint. */
49
+ private _mobile;
50
+ /** Whether the mobile off-canvas nav drawer is open. */
51
+ private _mobileNavOpen;
52
+ /** Whether the footer slot has assigned content. */
53
+ private _hasFooter;
54
+ static styles: import("lit").CSSResult[];
55
+ private _mediaQuery;
56
+ private _previousFocus;
57
+ connectedCallback(): void;
58
+ disconnectedCallback(): void;
59
+ /** Tracks the breakpoint and closes the mobile drawer when returning to desktop. */
60
+ private _onMediaChange;
61
+ /** Handles the `[` sidebar shortcut and Escape dismissal of mobile overlays. */
62
+ private _onKeydown;
63
+ /** Whether the keydown originated in a text field, so the shortcut defers to typing. */
64
+ private _isEditableTarget;
65
+ /** Toggles the rail (desktop) or the off-canvas drawer (mobile) and announces it. */
66
+ private _toggleSidebar;
67
+ /** Closes the detail overlay and notifies the consumer that owns `detail-open`. */
68
+ private _closeDetail;
69
+ /** Scrim click dismisses whichever overlay is open. */
70
+ private _onScrim;
71
+ protected updated(changed: PropertyValues): void;
72
+ /**
73
+ * Mirrors the shell's rail state onto the slotted sidebar so it hides labels
74
+ * when collapsed on desktop, and always shows them in the mobile drawer.
75
+ */
76
+ private _syncSidebar;
77
+ render(): import("lit-html").TemplateResult<1>;
78
+ /** Collapses the footer row when nothing is slotted into it. */
79
+ private _onFooterSlotChange;
80
+ }
81
+ declare global {
82
+ interface HTMLElementTagNameMap {
83
+ "app-shell": AppShell;
84
+ }
85
+ }
86
+ //# sourceMappingURL=app-shell.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-shell.d.ts","sourceRoot":"","sources":["../src/app-shell.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,KAAK,cAAc,EAAE,MAAM,KAAK,CAAC;AAG1E,OAAO,eAAe,CAAC;AAGvB,gEAAgE;AAChE,MAAM,WAAW,mBAAmB;IAClC,0BAA0B;IAC1B,SAAS,EAAE,OAAO,CAAC;IACnB,gCAAgC;IAChC,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,qBACa,QAAS,SAAQ,UAAU;IACtC,wDAAwD;IAExD,gBAAgB,UAAS;IACzB,gFAAgF;IACV,UAAU,UAAS;IACzF,gEAAgE;IACvB,WAAW,EAAE,SAAS,GAAG,aAAa,CAAa;IAE5F,6DAA6D;IACpD,OAAO,CAAC,OAAO,CAAS;IACjC,wDAAwD;IAC/C,OAAO,CAAC,cAAc,CAAS;IACxC,oDAAoD;IAC3C,OAAO,CAAC,UAAU,CAAS;IAEpC,OAAgB,MAAM,4BAkNpB;IAEF,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,cAAc,CAA4B;IAEzC,iBAAiB,IAAI,IAAI,CAMjC;IAEQ,oBAAoB,IAAI,IAAI,CAKpC;IAED,oFAAoF;IACpF,OAAO,CAAC,cAAc,CAGpB;IAEF,gFAAgF;IAChF,OAAO,CAAC,UAAU,CAsBhB;IAEF,wFAAwF;IACxF,OAAO,CAAC,iBAAiB;IAQzB,qFAAqF;IACrF,OAAO,CAAC,cAAc;IAgBtB,mFAAmF;IACnF,OAAO,CAAC,YAAY;IAKpB,uDAAuD;IACvD,OAAO,CAAC,QAAQ;IAKhB,UAAmB,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,CASxD;IAED;;;OAGG;IACH,OAAO,CAAC,YAAY;IAQX,MAAM,yCAuDd;IAED,gEAAgE;IAChE,OAAO,CAAC,mBAAmB;CAG5B;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,WAAW,EAAE,QAAQ,CAAC;KACvB;CACF"}
@@ -0,0 +1,460 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { LitElement, css, html, nothing } from "lit";
8
+ import { customElement, property, state } from "lit/decorators.js";
9
+ import { iconBars3 } from "./icons.js";
10
+ import "./kbd-hint.js";
11
+ import { tokens } from "./tokens.js";
12
+ /**
13
+ * The dashboard page shell: a slot-based CSS-grid backbone that arranges a
14
+ * full-height sidebar, a top bar, the main content, an optional right-hand
15
+ * detail column, and an optional footer. It owns the responsive behavior so
16
+ * consumers don't re-implement it — above the shared 48rem breakpoint the
17
+ * sidebar collapses to an icon rail and the detail region is an inline column;
18
+ * at or below it the sidebar becomes an off-canvas drawer and the detail region
19
+ * an overlay, both dismissed by a scrim or Escape.
20
+ *
21
+ * Widths are tunable per instance via `--component-sidebar-width` (16rem),
22
+ * `--component-sidebar-rail-width` (3.5rem), and `--component-topbar-height`
23
+ * (3rem); the detail column reuses the 20rem/25rem panel widths. The main
24
+ * content area is white by default — override it with
25
+ * `--component-main-background`. Give the shell a height (e.g. `height: 100vh`)
26
+ * so the sidebar and main can size and scroll.
27
+ *
28
+ * The built-in top-bar button toggles the sidebar, and so does pressing `[`
29
+ * anywhere on the page (ignored while typing in a text field or with a
30
+ * modifier held). Hovering or keyboard-focusing the toggle reveals a tooltip
31
+ * naming the action and its `[` shortcut — the shortcut is not shown as
32
+ * permanent chrome.
33
+ *
34
+ * @element app-shell
35
+ * @slot - Main content area.
36
+ * @slot sidebar - Full-height navigation (typically `app-sidebar`).
37
+ * @slot topbar - Top bar content, right of the built-in toggle.
38
+ * @slot detail - Optional right-hand detail; shown when `detail-open` is set.
39
+ * @slot footer - Optional footer beneath the main content.
40
+ * @fires sidebar-toggle - The built-in toggle changed the sidebar state.
41
+ * @fires detail-close - The scrim or Escape dismissed the mobile detail overlay.
42
+ */
43
+ let AppShell = class AppShell extends LitElement {
44
+ constructor() {
45
+ super(...arguments);
46
+ /** Collapses the sidebar to an icon rail on desktop. */
47
+ this.sidebarCollapsed = false;
48
+ /** Shows the right-hand detail region (inline column, or overlay on mobile). */
49
+ this.detailOpen = false;
50
+ /** Detail width: `compact` (20rem) or `comfortable` (25rem). */
51
+ this.detailWidth = "compact";
52
+ /** Whether the viewport is at/below the 48rem breakpoint. */
53
+ this._mobile = false;
54
+ /** Whether the mobile off-canvas nav drawer is open. */
55
+ this._mobileNavOpen = false;
56
+ /** Whether the footer slot has assigned content. */
57
+ this._hasFooter = false;
58
+ this._mediaQuery = null;
59
+ this._previousFocus = null;
60
+ /** Tracks the breakpoint and closes the mobile drawer when returning to desktop. */
61
+ this._onMediaChange = (event) => {
62
+ this._mobile = event.matches;
63
+ if (!event.matches)
64
+ this._mobileNavOpen = false;
65
+ };
66
+ /** Handles the `[` sidebar shortcut and Escape dismissal of mobile overlays. */
67
+ this._onKeydown = (event) => {
68
+ if (event.defaultPrevented)
69
+ return;
70
+ if (event.key === "Escape" && this._mobile) {
71
+ if (this.detailOpen) {
72
+ event.preventDefault();
73
+ this._closeDetail();
74
+ }
75
+ else if (this._mobileNavOpen) {
76
+ event.preventDefault();
77
+ this._mobileNavOpen = false;
78
+ }
79
+ return;
80
+ }
81
+ if (event.key === "[" &&
82
+ !event.ctrlKey &&
83
+ !event.metaKey &&
84
+ !event.altKey &&
85
+ !this._isEditableTarget(event)) {
86
+ event.preventDefault();
87
+ this._toggleSidebar();
88
+ }
89
+ };
90
+ }
91
+ static { this.styles = [
92
+ tokens,
93
+ css `
94
+ :host {
95
+ display: block;
96
+ block-size: 100%;
97
+ }
98
+ .shell {
99
+ --_sidebar-w: var(--component-sidebar-width, 16rem);
100
+ --_detail-w: 0px;
101
+ display: grid;
102
+ grid-template-columns: var(--_sidebar-w) minmax(0, 1fr) var(--_detail-w);
103
+ grid-template-rows: auto minmax(0, 1fr) auto;
104
+ grid-template-areas:
105
+ "sidebar topbar topbar"
106
+ "sidebar main detail"
107
+ "sidebar footer footer";
108
+ block-size: 100%;
109
+ min-height: 0;
110
+ background: var(--ui-surface, #ffffff);
111
+ }
112
+ :host([sidebar-collapsed]) .shell {
113
+ --_sidebar-w: var(--component-sidebar-rail-width, 3.5rem);
114
+ }
115
+ :host([detail-open]) .shell {
116
+ --_detail-w: 20rem;
117
+ }
118
+ :host([detail-open][detail-width="comfortable"]) .shell {
119
+ --_detail-w: 25rem;
120
+ }
121
+ .sidebar {
122
+ grid-area: sidebar;
123
+ min-height: 0;
124
+ overflow: hidden;
125
+ border-right: 1px solid var(--ui-border, #e2e8f0);
126
+ background: var(--ui-surface, #ffffff);
127
+ }
128
+ .topbar {
129
+ grid-area: topbar;
130
+ display: flex;
131
+ align-items: center;
132
+ gap: 0.5rem;
133
+ min-height: var(--component-topbar-height, 3rem);
134
+ padding: 0.5rem 0.75rem;
135
+ border-bottom: 1px solid var(--ui-border, #e2e8f0);
136
+ background: var(--ui-surface, #ffffff);
137
+ }
138
+ .nav-toggle {
139
+ display: inline-flex;
140
+ align-items: center;
141
+ justify-content: center;
142
+ flex: 0 0 auto;
143
+ width: 2rem;
144
+ height: 2rem;
145
+ padding: 0;
146
+ background: none;
147
+ border: none;
148
+ cursor: pointer;
149
+ color: var(--ui-text-muted, #64748b);
150
+ border-radius: var(--ui-radius-sm, 0.25rem);
151
+ }
152
+ .nav-group {
153
+ position: relative;
154
+ display: inline-flex;
155
+ align-items: center;
156
+ gap: 0.25rem;
157
+ flex: 0 0 auto;
158
+ }
159
+ .nav-tip {
160
+ position: absolute;
161
+ top: 100%;
162
+ left: 0;
163
+ margin-top: 0.25rem;
164
+ z-index: var(--component-layer-z, 100);
165
+ display: inline-flex;
166
+ align-items: center;
167
+ gap: 0.25rem;
168
+ white-space: nowrap;
169
+ padding: 0.25rem 0.5rem;
170
+ border-radius: var(--ui-radius-sm, 0.25rem);
171
+ background: var(--ui-tooltip, #0f172a);
172
+ color: var(--ui-on-accent, #ffffff);
173
+ box-shadow: var(--ui-shadow, 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1));
174
+ font-size: var(--ui-font-size-sm, 0.75rem);
175
+ line-height: var(--ui-line-height-tight, 1.25);
176
+ opacity: 0;
177
+ visibility: hidden;
178
+ pointer-events: none;
179
+ transition: opacity 120ms ease;
180
+ }
181
+ .nav-toggle:hover + .nav-tip,
182
+ .nav-toggle:focus-visible + .nav-tip {
183
+ opacity: 1;
184
+ visibility: visible;
185
+ }
186
+ .nav-toggle:hover {
187
+ background: var(--ui-surface-muted, #f8fafc);
188
+ }
189
+ .nav-toggle:focus-visible {
190
+ outline: none;
191
+ box-shadow: var(--ui-focus-ring, 0 0 0 3px rgb(79 70 229 / 0.35));
192
+ }
193
+ .topbar-content {
194
+ flex: 1 1 auto;
195
+ min-width: 0;
196
+ }
197
+ .main {
198
+ grid-area: main;
199
+ min-height: 0;
200
+ min-width: 0;
201
+ overflow: auto;
202
+ padding: 1rem;
203
+ background: var(--component-main-background, var(--ui-surface, #ffffff));
204
+ }
205
+ .detail {
206
+ grid-area: detail;
207
+ display: none;
208
+ min-height: 0;
209
+ overflow: auto;
210
+ border-left: 1px solid var(--ui-border, #e2e8f0);
211
+ background: var(--ui-surface, #ffffff);
212
+ }
213
+ :host([detail-open]) .detail {
214
+ display: block;
215
+ }
216
+ .footer {
217
+ grid-area: footer;
218
+ padding: 0.5rem 0.75rem;
219
+ border-top: 1px solid var(--ui-border, #e2e8f0);
220
+ background: var(--ui-surface, #ffffff);
221
+ }
222
+ .footer.empty {
223
+ display: none;
224
+ }
225
+ .scrim {
226
+ display: none;
227
+ }
228
+ @media (max-width: 48rem) {
229
+ .shell {
230
+ grid-template-columns: minmax(0, 1fr);
231
+ grid-template-rows: auto minmax(0, 1fr) auto;
232
+ grid-template-areas:
233
+ "topbar"
234
+ "main"
235
+ "footer";
236
+ }
237
+ .sidebar {
238
+ position: fixed;
239
+ top: 0;
240
+ bottom: 0;
241
+ left: 0;
242
+ width: var(--component-sidebar-width, 16rem);
243
+ z-index: var(--component-layer-z, 100);
244
+ transform: translateX(-100%);
245
+ transition: transform 250ms ease;
246
+ }
247
+ .shell.nav-open .sidebar {
248
+ transform: translateX(0);
249
+ box-shadow: var(
250
+ --ui-shadow-lg,
251
+ 0 20px 25px -5px rgb(0 0 0 / 0.1),
252
+ 0 8px 10px -6px rgb(0 0 0 / 0.1)
253
+ );
254
+ }
255
+ .detail {
256
+ position: fixed;
257
+ top: 0;
258
+ right: 0;
259
+ bottom: 0;
260
+ width: var(--_detail-w, 20rem);
261
+ max-width: calc(100vw - 3rem);
262
+ z-index: var(--component-layer-z, 100);
263
+ transform: translateX(110%);
264
+ transition: transform 250ms ease;
265
+ }
266
+ :host([detail-open]) .detail {
267
+ transform: translateX(0);
268
+ box-shadow: var(
269
+ --ui-shadow-lg,
270
+ 0 20px 25px -5px rgb(0 0 0 / 0.1),
271
+ 0 8px 10px -6px rgb(0 0 0 / 0.1)
272
+ );
273
+ }
274
+ .scrim.show {
275
+ display: block;
276
+ position: fixed;
277
+ inset: 0;
278
+ z-index: calc(var(--component-layer-z, 100) - 1);
279
+ background: var(--ui-overlay, rgb(15 23 42 / 0.45));
280
+ border: none;
281
+ }
282
+ }
283
+ @media (prefers-reduced-motion: reduce) {
284
+ .sidebar,
285
+ .detail,
286
+ .nav-tip {
287
+ transition: none;
288
+ }
289
+ }
290
+ @media (forced-colors: active) {
291
+ .nav-toggle:focus-visible {
292
+ outline: 2px solid CanvasText;
293
+ outline-offset: 2px;
294
+ box-shadow: none;
295
+ }
296
+ .nav-tip {
297
+ border: 1px solid CanvasText;
298
+ }
299
+ }
300
+ `,
301
+ ]; }
302
+ connectedCallback() {
303
+ super.connectedCallback();
304
+ this._mediaQuery = window.matchMedia("(max-width: 48rem)");
305
+ this._mobile = this._mediaQuery.matches;
306
+ this._mediaQuery.addEventListener("change", this._onMediaChange);
307
+ window.addEventListener("keydown", this._onKeydown);
308
+ }
309
+ disconnectedCallback() {
310
+ super.disconnectedCallback();
311
+ this._mediaQuery?.removeEventListener("change", this._onMediaChange);
312
+ this._mediaQuery = null;
313
+ window.removeEventListener("keydown", this._onKeydown);
314
+ }
315
+ /** Whether the keydown originated in a text field, so the shortcut defers to typing. */
316
+ _isEditableTarget(event) {
317
+ const target = event.composedPath()[0];
318
+ if (!target)
319
+ return false;
320
+ if (target.isContentEditable)
321
+ return true;
322
+ const tag = target.tagName;
323
+ return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT";
324
+ }
325
+ /** Toggles the rail (desktop) or the off-canvas drawer (mobile) and announces it. */
326
+ _toggleSidebar() {
327
+ if (this._mobile) {
328
+ this._mobileNavOpen = !this._mobileNavOpen;
329
+ if (this._mobileNavOpen)
330
+ this._previousFocus = document.activeElement;
331
+ }
332
+ else {
333
+ this.sidebarCollapsed = !this.sidebarCollapsed;
334
+ }
335
+ this.dispatchEvent(new CustomEvent("sidebar-toggle", {
336
+ detail: { collapsed: this.sidebarCollapsed, mobileOpen: this._mobileNavOpen },
337
+ bubbles: true,
338
+ composed: true,
339
+ }));
340
+ }
341
+ /** Closes the detail overlay and notifies the consumer that owns `detail-open`. */
342
+ _closeDetail() {
343
+ this.detailOpen = false;
344
+ this.dispatchEvent(new CustomEvent("detail-close", { bubbles: true, composed: true }));
345
+ }
346
+ /** Scrim click dismisses whichever overlay is open. */
347
+ _onScrim() {
348
+ if (this.detailOpen)
349
+ this._closeDetail();
350
+ if (this._mobileNavOpen)
351
+ this._mobileNavOpen = false;
352
+ }
353
+ updated(changed) {
354
+ if (changed.has("sidebarCollapsed") || changed.has("_mobile"))
355
+ this._syncSidebar();
356
+ if (!changed.has("_mobileNavOpen"))
357
+ return;
358
+ if (this._mobileNavOpen) {
359
+ this.shadowRoot?.querySelector(".sidebar")?.focus();
360
+ return;
361
+ }
362
+ if (this._previousFocus?.isConnected)
363
+ this._previousFocus.focus();
364
+ this._previousFocus = null;
365
+ }
366
+ /**
367
+ * Mirrors the shell's rail state onto the slotted sidebar so it hides labels
368
+ * when collapsed on desktop, and always shows them in the mobile drawer.
369
+ */
370
+ _syncSidebar() {
371
+ const rail = this.sidebarCollapsed && !this._mobile;
372
+ const slot = this.shadowRoot?.querySelector('slot[name="sidebar"]');
373
+ for (const element of slot?.assignedElements() ?? []) {
374
+ element.toggleAttribute("collapsed", rail);
375
+ }
376
+ }
377
+ render() {
378
+ const scrimActive = this._mobile && (this._mobileNavOpen || this.detailOpen);
379
+ return html `
380
+ <div class="shell ${this._mobileNavOpen ? "nav-open" : ""}">
381
+ <aside class="sidebar" tabindex="-1" aria-label="Primary">
382
+ <slot name="sidebar" @slotchange=${this._syncSidebar}></slot>
383
+ </aside>
384
+ <header class="topbar">
385
+ <div class="nav-group">
386
+ <button
387
+ class="nav-toggle"
388
+ type="button"
389
+ aria-label="Toggle navigation"
390
+ aria-keyshortcuts="["
391
+ aria-describedby="nav-tip"
392
+ aria-expanded=${this._mobile ? String(this._mobileNavOpen) : String(!this.sidebarCollapsed)}
393
+ @click=${this._toggleSidebar}
394
+ >
395
+ ${iconBars3(18)}
396
+ </button>
397
+ <span class="nav-tip" id="nav-tip" role="tooltip">
398
+ <span
399
+ >${this._mobile
400
+ ? "Navigation"
401
+ : this.sidebarCollapsed
402
+ ? "Expand sidebar"
403
+ : "Collapse sidebar"}</span
404
+ >
405
+ ${this._mobile ? nothing : html `<kbd-hint keys="["></kbd-hint>`}
406
+ </span>
407
+ </div>
408
+ <div class="topbar-content"><slot name="topbar"></slot></div>
409
+ </header>
410
+ <main class="main"><slot></slot></main>
411
+ <aside
412
+ class="detail"
413
+ aria-label="Detail"
414
+ ?inert=${!this.detailOpen}
415
+ aria-hidden=${String(!this.detailOpen)}
416
+ >
417
+ <slot name="detail"></slot>
418
+ </aside>
419
+ <footer class="footer ${this._hasFooter ? "" : "empty"}">
420
+ <slot name="footer" @slotchange=${this._onFooterSlotChange}></slot>
421
+ </footer>
422
+ <button
423
+ class="scrim ${scrimActive ? "show" : ""}"
424
+ type="button"
425
+ tabindex="-1"
426
+ aria-label="Close"
427
+ @click=${this._onScrim}
428
+ ?hidden=${!scrimActive}
429
+ ></button>
430
+ </div>
431
+ `;
432
+ }
433
+ /** Collapses the footer row when nothing is slotted into it. */
434
+ _onFooterSlotChange(event) {
435
+ this._hasFooter = event.target.assignedNodes({ flatten: true }).length > 0;
436
+ }
437
+ };
438
+ __decorate([
439
+ property({ type: Boolean, reflect: true, attribute: "sidebar-collapsed" })
440
+ ], AppShell.prototype, "sidebarCollapsed", void 0);
441
+ __decorate([
442
+ property({ type: Boolean, reflect: true, attribute: "detail-open" })
443
+ ], AppShell.prototype, "detailOpen", void 0);
444
+ __decorate([
445
+ property({ attribute: "detail-width" })
446
+ ], AppShell.prototype, "detailWidth", void 0);
447
+ __decorate([
448
+ state()
449
+ ], AppShell.prototype, "_mobile", void 0);
450
+ __decorate([
451
+ state()
452
+ ], AppShell.prototype, "_mobileNavOpen", void 0);
453
+ __decorate([
454
+ state()
455
+ ], AppShell.prototype, "_hasFooter", void 0);
456
+ AppShell = __decorate([
457
+ customElement("app-shell")
458
+ ], AppShell);
459
+ export { AppShell };
460
+ //# sourceMappingURL=app-shell.js.map