@f-ewald/components 1.2.1 → 1.4.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 (62) hide show
  1. package/README.md +16 -2
  2. package/custom-elements.json +1071 -79
  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/content-divider.d.ts +23 -0
  16. package/dist/content-divider.d.ts.map +1 -0
  17. package/dist/content-divider.js +86 -0
  18. package/dist/content-divider.js.map +1 -0
  19. package/dist/form-actions.d.ts +27 -0
  20. package/dist/form-actions.d.ts.map +1 -0
  21. package/dist/form-actions.js +65 -0
  22. package/dist/form-actions.js.map +1 -0
  23. package/dist/index.d.ts +9 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +9 -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/timeline-container.d.ts +22 -0
  38. package/dist/timeline-container.d.ts.map +1 -0
  39. package/dist/timeline-container.js +42 -0
  40. package/dist/timeline-container.js.map +1 -0
  41. package/dist/timeline-entry.d.ts +31 -0
  42. package/dist/timeline-entry.d.ts.map +1 -0
  43. package/dist/timeline-entry.js +170 -0
  44. package/dist/timeline-entry.js.map +1 -0
  45. package/docs/action-bar.md +41 -0
  46. package/docs/app-shell.md +85 -0
  47. package/docs/app-sidebar.md +69 -0
  48. package/docs/content-divider.md +47 -0
  49. package/docs/design-language.md +74 -1
  50. package/docs/form-actions.md +43 -0
  51. package/docs/layouts/detail-only.md +52 -0
  52. package/docs/layouts/form-page.md +52 -0
  53. package/docs/layouts/list-detail.md +52 -0
  54. package/docs/layouts/list-only.md +50 -0
  55. package/docs/layouts/record-detail.md +88 -0
  56. package/docs/mcp-evaluation.md +21 -0
  57. package/docs/page-header.md +47 -0
  58. package/docs/pagination-nav.md +54 -0
  59. package/docs/timeline-container.md +44 -0
  60. package/docs/timeline-entry.md +52 -0
  61. package/llms.txt +251 -0
  62. package/package.json +1 -1
@@ -0,0 +1,31 @@
1
+ import { LitElement } from "lit";
2
+ import "./relative-time.js";
3
+ /**
4
+ * One event on a `timeline-container`: a dot on the vertical line, an optional
5
+ * headline, a relative timestamp ("3 hours ago"), and freely nested content.
6
+ * The connecting line is drawn here — its segment above the dot is hidden on
7
+ * the first entry and the segment below is hidden on the last, so the line caps
8
+ * exactly at the first and last dots. Only meaningful inside a
9
+ * `timeline-container`; demonstrated through it.
10
+ *
11
+ * @element timeline-entry
12
+ * @slot headline - Optional headline/title for the event.
13
+ * @slot - The event content; nest any elements here.
14
+ */
15
+ export declare class TimelineEntry extends LitElement {
16
+ /** ISO 8601 or SQLite datetime string, rendered as a relative time. */
17
+ datetime: string | null;
18
+ /** Whether the headline slot currently has assigned content. */
19
+ private _hasHeadline;
20
+ static styles: import("lit").CSSResult[];
21
+ connectedCallback(): void;
22
+ /** Collapses the headline when nothing is slotted so the time sits alone. */
23
+ private _onHeadlineSlotChange;
24
+ render(): import("lit-html").TemplateResult<1>;
25
+ }
26
+ declare global {
27
+ interface HTMLElementTagNameMap {
28
+ "timeline-entry": TimelineEntry;
29
+ }
30
+ }
31
+ //# sourceMappingURL=timeline-entry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeline-entry.d.ts","sourceRoot":"","sources":["../src/timeline-entry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAsB,MAAM,KAAK,CAAC;AAErD,OAAO,oBAAoB,CAAC;AAG5B;;;;;;;;;;;GAWG;AACH,qBACa,aAAc,SAAQ,UAAU;IAC3C,uEAAuE;IAC3D,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE3C,gEAAgE;IACvD,OAAO,CAAC,YAAY,CAAS;IAEtC,OAAgB,MAAM,4BAgGpB;IAEO,iBAAiB,IAAI,IAAI,CAGjC;IAED,6EAA6E;IAC7E,OAAO,CAAC,qBAAqB;IAIpB,MAAM,yCAqBd;CACF;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,gBAAgB,EAAE,aAAa,CAAC;KACjC;CACF"}
@@ -0,0 +1,170 @@
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 "./relative-time.js";
10
+ import { tokens } from "./tokens.js";
11
+ /**
12
+ * One event on a `timeline-container`: a dot on the vertical line, an optional
13
+ * headline, a relative timestamp ("3 hours ago"), and freely nested content.
14
+ * The connecting line is drawn here — its segment above the dot is hidden on
15
+ * the first entry and the segment below is hidden on the last, so the line caps
16
+ * exactly at the first and last dots. Only meaningful inside a
17
+ * `timeline-container`; demonstrated through it.
18
+ *
19
+ * @element timeline-entry
20
+ * @slot headline - Optional headline/title for the event.
21
+ * @slot - The event content; nest any elements here.
22
+ */
23
+ let TimelineEntry = class TimelineEntry extends LitElement {
24
+ constructor() {
25
+ super(...arguments);
26
+ /** ISO 8601 or SQLite datetime string, rendered as a relative time. */
27
+ this.datetime = null;
28
+ /** Whether the headline slot currently has assigned content. */
29
+ this._hasHeadline = false;
30
+ }
31
+ static { this.styles = [
32
+ tokens,
33
+ css `
34
+ :host {
35
+ display: block;
36
+ font-family: var(
37
+ --ui-font,
38
+ ui-sans-serif,
39
+ system-ui,
40
+ sans-serif,
41
+ "Apple Color Emoji",
42
+ "Segoe UI Emoji",
43
+ "Segoe UI Symbol",
44
+ "Noto Color Emoji"
45
+ );
46
+ }
47
+ .entry {
48
+ display: flex;
49
+ gap: 0.75rem;
50
+ }
51
+ .rail {
52
+ position: relative;
53
+ flex: 0 0 auto;
54
+ width: 0.75rem;
55
+ }
56
+ .line {
57
+ position: absolute;
58
+ left: 50%;
59
+ width: 2px;
60
+ transform: translateX(-50%);
61
+ background: var(--ui-border, #e2e8f0);
62
+ }
63
+ .line-top {
64
+ top: 0;
65
+ height: 0.5rem;
66
+ }
67
+ .line-bottom {
68
+ top: 0.5rem;
69
+ bottom: 0;
70
+ }
71
+ .dot {
72
+ position: absolute;
73
+ top: 0.125rem;
74
+ left: 50%;
75
+ width: 0.75rem;
76
+ height: 0.75rem;
77
+ transform: translateX(-50%);
78
+ border-radius: 9999px;
79
+ /* Same primary-token gradient as map-circle/user-avatar, lighter on top. */
80
+ background: linear-gradient(
81
+ to bottom,
82
+ color-mix(in srgb, var(--ui-primary, #4f46e5) 70%, #ffffff) 0%,
83
+ color-mix(in srgb, var(--ui-primary, #4f46e5) 70%, #000000) 100%
84
+ );
85
+ }
86
+ :host(:first-child) .line-top {
87
+ display: none;
88
+ }
89
+ :host(:last-child) .line-bottom {
90
+ display: none;
91
+ }
92
+ .body {
93
+ flex: 1 1 auto;
94
+ min-width: 0;
95
+ padding-bottom: 1.5rem;
96
+ }
97
+ :host(:last-child) .body {
98
+ padding-bottom: 0;
99
+ }
100
+ .head {
101
+ display: flex;
102
+ align-items: baseline;
103
+ gap: 0.5rem;
104
+ flex-wrap: wrap;
105
+ }
106
+ .headline {
107
+ font-weight: var(--ui-font-weight-semibold, 600);
108
+ font-size: var(--ui-font-size, 0.875rem);
109
+ line-height: var(--ui-line-height-tight, 1.25);
110
+ color: var(--ui-text, #0f172a);
111
+ }
112
+ .headline.empty {
113
+ display: none;
114
+ }
115
+ .time {
116
+ font-size: var(--ui-font-size-sm, 0.75rem);
117
+ line-height: var(--ui-line-height-tight, 1.25);
118
+ color: var(--ui-text-muted, #64748b);
119
+ }
120
+ .content {
121
+ margin-top: 0.25rem;
122
+ font-size: var(--ui-font-size, 0.875rem);
123
+ line-height: var(--ui-line-height-normal, 1.5);
124
+ color: var(--ui-text, #0f172a);
125
+ }
126
+ `,
127
+ ]; }
128
+ connectedCallback() {
129
+ super.connectedCallback();
130
+ if (!this.hasAttribute("role"))
131
+ this.setAttribute("role", "listitem");
132
+ }
133
+ /** Collapses the headline when nothing is slotted so the time sits alone. */
134
+ _onHeadlineSlotChange(event) {
135
+ this._hasHeadline = event.target.assignedNodes({ flatten: true }).length > 0;
136
+ }
137
+ render() {
138
+ return html `
139
+ <div class="entry">
140
+ <div class="rail" aria-hidden="true">
141
+ <span class="line line-top"></span>
142
+ <span class="line line-bottom"></span>
143
+ <span class="dot"></span>
144
+ </div>
145
+ <div class="body">
146
+ <div class="head">
147
+ <span class="headline ${this._hasHeadline ? "" : "empty"}">
148
+ <slot name="headline" @slotchange=${this._onHeadlineSlotChange}></slot>
149
+ </span>
150
+ ${this.datetime
151
+ ? html `<relative-time class="time" datetime=${this.datetime}></relative-time>`
152
+ : nothing}
153
+ </div>
154
+ <div class="content"><slot></slot></div>
155
+ </div>
156
+ </div>
157
+ `;
158
+ }
159
+ };
160
+ __decorate([
161
+ property()
162
+ ], TimelineEntry.prototype, "datetime", void 0);
163
+ __decorate([
164
+ state()
165
+ ], TimelineEntry.prototype, "_hasHeadline", void 0);
166
+ TimelineEntry = __decorate([
167
+ customElement("timeline-entry")
168
+ ], TimelineEntry);
169
+ export { TimelineEntry };
170
+ //# sourceMappingURL=timeline-entry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeline-entry.js","sourceRoot":"","sources":["../src/timeline-entry.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,oBAAoB,CAAC;AAC5B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC;;;;;;;;;;;GAWG;AAEI,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,UAAU;IAAtC;;QACL,uEAAuE;QAC3D,aAAQ,GAAkB,IAAI,CAAC;QAE3C,gEAAgE;QAC/C,iBAAY,GAAG,KAAK,CAAC;IAoIxC,CAAC;aAlIiB,WAAM,GAAG;QACvB,MAAM;QACN,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA6FF;KACF,AAhGqB,CAgGpB;IAEO,iBAAiB;QACxB,KAAK,CAAC,iBAAiB,EAAE,CAAC;QAC1B,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC;YAAE,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACxE,CAAC;IAED,6EAA6E;IACrE,qBAAqB,CAAC,KAAY;QACxC,IAAI,CAAC,YAAY,GAAI,KAAK,CAAC,MAA0B,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IACpG,CAAC;IAEQ,MAAM;QACb,OAAO,IAAI,CAAA;;;;;;;;;oCASqB,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO;kDAClB,IAAI,CAAC,qBAAqB;;cAE9D,IAAI,CAAC,QAAQ;YACb,CAAC,CAAC,IAAI,CAAA,wCAAwC,IAAI,CAAC,QAAQ,mBAAmB;YAC9E,CAAC,CAAC,OAAO;;;;;KAKlB,CAAC;IACJ,CAAC;CACF,CAAA;AAvIa;IAAX,QAAQ,EAAE;+CAAgC;AAG1B;IAAhB,KAAK,EAAE;mDAA8B;AAL3B,aAAa;IADzB,aAAa,CAAC,gBAAgB,CAAC;GACnB,aAAa,CAyIzB","sourcesContent":["import { LitElement, css, html, nothing } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport \"./relative-time.js\";\nimport { tokens } from \"./tokens.js\";\n\n/**\n * One event on a `timeline-container`: a dot on the vertical line, an optional\n * headline, a relative timestamp (\"3 hours ago\"), and freely nested content.\n * The connecting line is drawn here — its segment above the dot is hidden on\n * the first entry and the segment below is hidden on the last, so the line caps\n * exactly at the first and last dots. Only meaningful inside a\n * `timeline-container`; demonstrated through it.\n *\n * @element timeline-entry\n * @slot headline - Optional headline/title for the event.\n * @slot - The event content; nest any elements here.\n */\n@customElement(\"timeline-entry\")\nexport class TimelineEntry extends LitElement {\n /** ISO 8601 or SQLite datetime string, rendered as a relative time. */\n @property() datetime: string | null = null;\n\n /** Whether the headline slot currently has assigned content. */\n @state() private _hasHeadline = false;\n\n static override styles = [\n tokens,\n css`\n :host {\n display: block;\n font-family: var(\n --ui-font,\n ui-sans-serif,\n system-ui,\n sans-serif,\n \"Apple Color Emoji\",\n \"Segoe UI Emoji\",\n \"Segoe UI Symbol\",\n \"Noto Color Emoji\"\n );\n }\n .entry {\n display: flex;\n gap: 0.75rem;\n }\n .rail {\n position: relative;\n flex: 0 0 auto;\n width: 0.75rem;\n }\n .line {\n position: absolute;\n left: 50%;\n width: 2px;\n transform: translateX(-50%);\n background: var(--ui-border, #e2e8f0);\n }\n .line-top {\n top: 0;\n height: 0.5rem;\n }\n .line-bottom {\n top: 0.5rem;\n bottom: 0;\n }\n .dot {\n position: absolute;\n top: 0.125rem;\n left: 50%;\n width: 0.75rem;\n height: 0.75rem;\n transform: translateX(-50%);\n border-radius: 9999px;\n /* Same primary-token gradient as map-circle/user-avatar, lighter on top. */\n background: linear-gradient(\n to bottom,\n color-mix(in srgb, var(--ui-primary, #4f46e5) 70%, #ffffff) 0%,\n color-mix(in srgb, var(--ui-primary, #4f46e5) 70%, #000000) 100%\n );\n }\n :host(:first-child) .line-top {\n display: none;\n }\n :host(:last-child) .line-bottom {\n display: none;\n }\n .body {\n flex: 1 1 auto;\n min-width: 0;\n padding-bottom: 1.5rem;\n }\n :host(:last-child) .body {\n padding-bottom: 0;\n }\n .head {\n display: flex;\n align-items: baseline;\n gap: 0.5rem;\n flex-wrap: wrap;\n }\n .headline {\n font-weight: var(--ui-font-weight-semibold, 600);\n font-size: var(--ui-font-size, 0.875rem);\n line-height: var(--ui-line-height-tight, 1.25);\n color: var(--ui-text, #0f172a);\n }\n .headline.empty {\n display: none;\n }\n .time {\n font-size: var(--ui-font-size-sm, 0.75rem);\n line-height: var(--ui-line-height-tight, 1.25);\n color: var(--ui-text-muted, #64748b);\n }\n .content {\n margin-top: 0.25rem;\n font-size: var(--ui-font-size, 0.875rem);\n line-height: var(--ui-line-height-normal, 1.5);\n color: var(--ui-text, #0f172a);\n }\n `,\n ];\n\n override connectedCallback(): void {\n super.connectedCallback();\n if (!this.hasAttribute(\"role\")) this.setAttribute(\"role\", \"listitem\");\n }\n\n /** Collapses the headline when nothing is slotted so the time sits alone. */\n private _onHeadlineSlotChange(event: Event): void {\n this._hasHeadline = (event.target as HTMLSlotElement).assignedNodes({ flatten: true }).length > 0;\n }\n\n override render() {\n return html`\n <div class=\"entry\">\n <div class=\"rail\" aria-hidden=\"true\">\n <span class=\"line line-top\"></span>\n <span class=\"line line-bottom\"></span>\n <span class=\"dot\"></span>\n </div>\n <div class=\"body\">\n <div class=\"head\">\n <span class=\"headline ${this._hasHeadline ? \"\" : \"empty\"}\">\n <slot name=\"headline\" @slotchange=${this._onHeadlineSlotChange}></slot>\n </span>\n ${this.datetime\n ? html`<relative-time class=\"time\" datetime=${this.datetime}></relative-time>`\n : nothing}\n </div>\n <div class=\"content\"><slot></slot></div>\n </div>\n </div>\n `;\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"timeline-entry\": TimelineEntry;\n }\n}\n"]}
@@ -0,0 +1,41 @@
1
+ # `<action-bar>`
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
+ ## Install
12
+
13
+ ```js
14
+ import "@f-ewald/components/action-bar.js";
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```html
20
+ <action-bar>
21
+ <autocomplete-input slot="start" placeholder="Search…"></autocomplete-input>
22
+ <ui-button slot="end" variant="secondary">Delete</ui-button>
23
+ <ui-button slot="end" variant="primary">Create</ui-button>
24
+ </action-bar>
25
+ ```
26
+
27
+ ## Attributes / properties
28
+
29
+ _None._
30
+
31
+ ## Events
32
+
33
+ _None._
34
+
35
+ ## Slots
36
+
37
+ _None._
38
+
39
+ ## CSS custom properties
40
+
41
+ _None._
@@ -0,0 +1,85 @@
1
+ # `<app-shell>`
2
+
3
+ The dashboard page shell: a slot-based CSS-grid backbone that arranges a
4
+ full-height sidebar, a top bar, the main content, an optional right-hand
5
+ detail column, and an optional footer. It owns the responsive behavior so
6
+ consumers don't re-implement it — above the shared 48rem breakpoint the
7
+ sidebar collapses to an icon rail and the detail region is an inline column;
8
+ at or below it the sidebar becomes an off-canvas drawer and the detail region
9
+ an overlay, both dismissed by a scrim or Escape.
10
+
11
+ Widths are tunable per instance via `--component-sidebar-width` (16rem),
12
+ `--component-sidebar-rail-width` (3.5rem), and `--component-topbar-height`
13
+ (3rem); the detail column reuses the 20rem/25rem panel widths. The main
14
+ content area is white by default — override it with
15
+ `--component-main-background`. Give the shell a height (e.g. `height: 100vh`)
16
+ so the sidebar and main can size and scroll.
17
+
18
+ The built-in top-bar button toggles the sidebar, and so does pressing `[`
19
+ anywhere on the page (ignored while typing in a text field or with a
20
+ modifier held). Hovering or keyboard-focusing the toggle reveals a tooltip
21
+ naming the action and its `[` shortcut — the shortcut is not shown as
22
+ permanent chrome.
23
+
24
+ ## Install
25
+
26
+ ```js
27
+ import "@f-ewald/components/app-shell.js";
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```html
33
+ <app-shell detail-open style="height: 100vh">
34
+ <app-sidebar slot="sidebar">
35
+ <a href="/dashboard" aria-current="page" aria-label="Dashboard">
36
+ <!-- icon --><span style="display: var(--app-sidebar-label, inline)">Dashboard</span>
37
+ </a>
38
+ </app-sidebar>
39
+ <page-header slot="topbar" heading="Members"></page-header>
40
+ <action-bar>
41
+ <autocomplete-input slot="start" placeholder="Search…"></autocomplete-input>
42
+ <ui-button slot="end" variant="primary">Create</ui-button>
43
+ </action-bar>
44
+ <data-table></data-table>
45
+ <div slot="detail">Selected record…</div>
46
+ <pagination-nav slot="footer" total-pages="5"></pagination-nav>
47
+ </app-shell>
48
+ ```
49
+
50
+ ## Attributes / properties
51
+
52
+ | Property | Attribute | Type | Default | Description |
53
+ | --- | --- | --- | --- | --- |
54
+ | `sidebarCollapsed` | `sidebar-collapsed` | `boolean` | `false` | Collapses the sidebar to an icon rail on desktop. |
55
+ | `detailOpen` | `detail-open` | `boolean` | `false` | Shows the right-hand detail region (inline column, or overlay on mobile). |
56
+ | `detailWidth` | `detail-width` | `"compact" | "comfortable"` | `"compact"` | Detail width: `compact` (20rem) or `comfortable` (25rem). |
57
+
58
+ ## Events
59
+
60
+ | Event | Description |
61
+ | --- | --- |
62
+ | `sidebar-toggle` | The built-in toggle changed the sidebar state. |
63
+ | `detail-close` | The scrim or Escape dismissed the mobile detail overlay. |
64
+
65
+ ## Slots
66
+
67
+ _None._
68
+
69
+ ## CSS custom properties
70
+
71
+ | Custom property |
72
+ | --- |
73
+ | `--ui-border` |
74
+ | `--ui-focus-ring` |
75
+ | `--ui-font-size-sm` |
76
+ | `--ui-line-height-tight` |
77
+ | `--ui-on-accent` |
78
+ | `--ui-overlay` |
79
+ | `--ui-radius-sm` |
80
+ | `--ui-shadow` |
81
+ | `--ui-shadow-lg` |
82
+ | `--ui-surface` |
83
+ | `--ui-surface-muted` |
84
+ | `--ui-text-muted` |
85
+ | `--ui-tooltip` |
@@ -0,0 +1,69 @@
1
+ # `<app-sidebar>`
2
+
3
+ Collapsible navigation sidebar for the `app-shell` sidebar slot. It is
4
+ deliberately presentational and router-agnostic: the consumer supplies the
5
+ nav items as plain `<a>`/`<button>` elements (each an icon followed by a
6
+ label), optional `<p>` group headings, and marks the active item with
7
+ `aria-current="page"`. The sidebar styles them, tracks hover/active/focus,
8
+ and — in collapsed "rail" mode — centers the icons and hides the labels.
9
+
10
+ Rail mode is driven by the `collapsed` attribute (the parent `app-shell`
11
+ toggles it). Labels are hidden by the inherited `--app-sidebar-label` custom
12
+ property, so each label element should read it, e.g.
13
+ `<span style="display: var(--app-sidebar-label, inline)">Reports</span>`;
14
+ keep an `aria-label` on the item so its accessible name survives collapse.
15
+ The sidebar fills the width and height of its container — `app-shell` owns
16
+ the actual rail/expanded width.
17
+
18
+ ## Install
19
+
20
+ ```js
21
+ import "@f-ewald/components/app-sidebar.js";
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```html
27
+ <app-sidebar>
28
+ <a href="/dashboard" aria-current="page" aria-label="Dashboard">
29
+ <!-- icon --><span style="display: var(--app-sidebar-label, inline)">Dashboard</span>
30
+ </a>
31
+ <a href="/members" aria-label="Members">
32
+ <!-- icon --><span style="display: var(--app-sidebar-label, inline)">Members</span>
33
+ </a>
34
+ </app-sidebar>
35
+ ```
36
+
37
+ ## Attributes / properties
38
+
39
+ | Property | Attribute | Type | Default | Description |
40
+ | --- | --- | --- | --- | --- |
41
+ | `collapsed` | `collapsed` | `boolean` | `false` | Collapses the sidebar to an icon rail (centers icons, hides labels). |
42
+
43
+ ## Events
44
+
45
+ _None._
46
+
47
+ ## Slots
48
+
49
+ _None._
50
+
51
+ ## CSS custom properties
52
+
53
+ | Custom property |
54
+ | --- |
55
+ | `--ui-border` |
56
+ | `--ui-focus-ring` |
57
+ | `--ui-font` |
58
+ | `--ui-font-size-sm` |
59
+ | `--ui-font-size-xs` |
60
+ | `--ui-font-weight-medium` |
61
+ | `--ui-font-weight-semibold` |
62
+ | `--ui-line-height-tight` |
63
+ | `--ui-primary` |
64
+ | `--ui-radius-sm` |
65
+ | `--ui-surface` |
66
+ | `--ui-surface-muted` |
67
+ | `--ui-text` |
68
+ | `--ui-text-muted` |
69
+ | `--ui-tracking-wide` |
@@ -0,0 +1,47 @@
1
+ # `<content-divider>`
2
+
3
+ A horizontal divider: a thin rule that visually separates two pieces of
4
+ content that are not otherwise contained in a box or frame and would bleed
5
+ into each other. With a `label` it renders the common "─── OR ───"
6
+ pattern — text centered between two line segments; without one it is a
7
+ single full-width rule. Exposed to assistive technology as a horizontal
8
+ separator, so it renders correctly with zero external CSS.
9
+
10
+ ## Install
11
+
12
+ ```js
13
+ import "@f-ewald/components/content-divider.js";
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```html
19
+ <content-divider></content-divider>
20
+
21
+ <content-divider label="OR"></content-divider>
22
+ ```
23
+
24
+ ## Attributes / properties
25
+
26
+ | Property | Attribute | Type | Default | Description |
27
+ | --- | --- | --- | --- | --- |
28
+ | `label` | `label` | `string` | `""` | Optional centered label; empty renders a plain full-width rule. |
29
+
30
+ ## Events
31
+
32
+ _None._
33
+
34
+ ## Slots
35
+
36
+ _None._
37
+
38
+ ## CSS custom properties
39
+
40
+ | Custom property |
41
+ | --- |
42
+ | `--ui-border` |
43
+ | `--ui-font` |
44
+ | `--ui-font-size-sm` |
45
+ | `--ui-font-weight-medium` |
46
+ | `--ui-line-height-tight` |
47
+ | `--ui-text-muted` |
@@ -134,13 +134,86 @@ literal values are migrated onto them.
134
134
 
135
135
  - Metadata-only tags may omit standalone playground sections when their full
136
136
  behavior is demonstrated through a parent:
137
- `calendar-entry`, `gallery-item`, and `gallery-item-variant`.
137
+ `calendar-entry`, `gallery-item`, `gallery-item-variant`, `kanban-card`,
138
+ `kanban-column`, and `timeline-entry` (shown through `timeline-container`).
138
139
  - Styleless inline formatters may omit empty `static styles`/token imports:
139
140
  `distance-value`, `live-timer`, `relative-time`, and `roman-numeral`.
140
141
  - Domain visuals may deliberately diverge in geometry and data color, but their
141
142
  surrounding typography, focus, motion, and accessibility still follow this
142
143
  contract.
143
144
 
145
+ ## Layout and page templates
146
+
147
+ The layout components compose into dashboard pages: `app-shell` is the grid
148
+ backbone, and `app-sidebar`, `action-bar`, `page-header`, `pagination-nav`, and
149
+ `form-actions` fill its slots and content.
150
+
151
+ ### Shell grid and metrics
152
+
153
+ - `app-shell` owns a CSS grid with `sidebar`, `topbar`, `main` (default slot),
154
+ `detail`, and `footer` areas plus the responsive behavior. It is the only
155
+ component that coordinates the sidebar rail and the detail overlay.
156
+ - Layout dimensions are documented literal rems, not `--ui-*` tokens (widths and
157
+ spacing are constants in this system). A few are exposed as overridable
158
+ `--component-*` custom properties for per-instance tuning, mirroring
159
+ `--component-layer-z`:
160
+ - sidebar expanded `--component-sidebar-width` = `16rem`;
161
+ - sidebar rail `--component-sidebar-rail-width` = `3.5rem`;
162
+ - top bar `--component-topbar-height` = `3rem`;
163
+ - detail column reuses the `20rem` / `25rem` panel widths (the `detail-width`
164
+ `compact` / `comfortable` attribute);
165
+ - main content is white (`--component-main-background`, default `--ui-surface`)
166
+ and fluid; constrain reading or form pages with a `max-width` on your own
167
+ wrapper (roughly `40rem`–`48rem`).
168
+ - The shared `48rem` breakpoint remains the only responsive breakpoint. At or
169
+ below it the sidebar becomes an off-canvas drawer and the detail an overlay,
170
+ both raised with `--component-layer-z` and dismissed by a scrim or Escape.
171
+
172
+ ### Sidebar
173
+
174
+ - Collapsing is a two-mode design: on desktop the sidebar condenses to an icon
175
+ rail (labels hide, icons stay) via `app-shell`'s `sidebar-collapsed`, which
176
+ propagates `collapsed` to the slotted `app-sidebar`; below `48rem` it is a
177
+ drawer with labels shown.
178
+ - The built-in top-bar toggle and the `[` keyboard shortcut (ignored while a
179
+ text field is focused or a modifier is held) both drive `sidebar-collapsed`.
180
+ The shortcut is surfaced in the toggle's hover/focus tooltip, not as permanent
181
+ chrome — reveal shortcuts on interaction rather than displaying them inline.
182
+ - Nav items are the consumer's `<a>`/`<button>` elements with an icon and a
183
+ label; mark the active one with `aria-current="page"`. Each label reads the
184
+ inherited `--app-sidebar-label` custom property so rail mode can hide it while
185
+ its `aria-label` keeps the accessible name.
186
+ - The `header` slot holds the brand: a small logo plus a name. Wrap the name in
187
+ a `--app-sidebar-label` element so rail mode shows only the logo, centered in
188
+ line with the nav icons.
189
+
190
+ ### Forms and actions
191
+
192
+ - The action row above a list uses `action-bar` (`start` = search/filters,
193
+ `end` = record actions). Pagination below a list uses `pagination-nav`, which
194
+ is controlled: the consumer owns the data and moves the page on `page-change`.
195
+ - Form button order is fixed: the primary/submit button is rightmost, the
196
+ secondary/cancel button is immediately to its left, and any tertiary or
197
+ destructive action is pinned to the far left. Use `form-actions`, which
198
+ enforces this regardless of source order — never hand-place form buttons in a
199
+ different order.
200
+ - Page and record actions live in the `page-header` actions slot, not a footer
201
+ bar: the primary action is the prominent, rightmost button, and destructive or
202
+ rare actions are de-emphasized (a secondary button or an overflow menu, gated
203
+ by a `confirm-dialog`). Never duplicate the primary action in both a header and
204
+ a footer.
205
+ - Reserve `form-actions` for the bottom of an actual editable form. Read (detail)
206
+ and edit (form) are distinct modes: a read view acts through its header and has
207
+ no Save/Cancel footer; entering edit shows the form with a `form-actions` bar.
208
+ - Fence clearly distinct sections of a detail or form page with `frame-box`.
209
+
210
+ ### Templates
211
+
212
+ The recipes under `docs/layouts/` compose these into four pages — list-only,
213
+ list + detail, detail-only, and form — and the MCP `list_layouts` /
214
+ `get_layout` tools serve them. Add a page by adding a `docs/layouts/<name>.md`
215
+ recipe and, ideally, a matching full-page demo under `demo/layouts/`.
216
+
144
217
  ## Playground and documentation
145
218
 
146
219
  - Visual/interactive nav links and complete sections are alphabetical and have
@@ -0,0 +1,43 @@
1
+ # `<form-actions>`
2
+
3
+ Form footer button bar with a fixed action order for internal apps: the
4
+ primary (submit) button is always rightmost, the secondary (cancel) button
5
+ sits to its immediate left, and an optional tertiary/destructive action is
6
+ pinned to the far left. The order is enforced by the component regardless of
7
+ the source order the buttons are authored in, so every form in a product
8
+ reads the same way.
9
+
10
+ Purely presentational: it lays out whatever controls (usually `ui-button`)
11
+ are slotted and never intercepts their clicks, `type="submit"`, or events.
12
+
13
+ ## Install
14
+
15
+ ```js
16
+ import "@f-ewald/components/form-actions.js";
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```html
22
+ <form-actions>
23
+ <ui-button slot="start" variant="danger">Delete</ui-button>
24
+ <ui-button slot="secondary" variant="secondary">Cancel</ui-button>
25
+ <ui-button slot="primary" type="submit" variant="primary">Save</ui-button>
26
+ </form-actions>
27
+ ```
28
+
29
+ ## Attributes / properties
30
+
31
+ _None._
32
+
33
+ ## Events
34
+
35
+ _None._
36
+
37
+ ## Slots
38
+
39
+ _None._
40
+
41
+ ## CSS custom properties
42
+
43
+ _None._
@@ -0,0 +1,52 @@
1
+ # Detail-only page
2
+
3
+ A single record's read view: navigation, a titled header carrying the record
4
+ actions, and the record body grouped into framed sections.
5
+
6
+ **When to use:** viewing one record on its own route (linked to from a list).
7
+ Read and edit are distinct modes — keep this page read-only and let its primary
8
+ action navigate to the form page for editing.
9
+
10
+ ## Shell slots
11
+
12
+ - `sidebar` → `app-sidebar`.
13
+ - `topbar` → `page-header` with a linked `breadcrumb` and the record `actions`.
14
+ - default (`main`) → the record body; group distinct sections with `frame-box`.
15
+
16
+ ## Markup
17
+
18
+ ```html
19
+ <app-shell style="height: 100vh">
20
+ <app-sidebar slot="sidebar"><!-- nav items --></app-sidebar>
21
+
22
+ <page-header slot="topbar" heading="Ada Lovelace">
23
+ <nav slot="breadcrumb" aria-label="Breadcrumb"><a href="/members">Members</a> / Ada Lovelace</nav>
24
+ <!-- Record actions: primary rightmost, destructive de-emphasized. -->
25
+ <ui-button slot="actions" variant="secondary">Deactivate</ui-button>
26
+ <ui-button slot="actions" variant="primary">Edit</ui-button>
27
+ </page-header>
28
+
29
+ <div style="max-width: 48rem;">
30
+ <frame-box label="Profile"><!-- fields --></frame-box>
31
+ <frame-box label="Access"><!-- fields --></frame-box>
32
+ </div>
33
+ </app-shell>
34
+ ```
35
+
36
+ ## Notes
37
+
38
+ - Put record actions in the header, not a footer bar. The primary action (Edit)
39
+ is the rightmost, prominent button; keep destructive actions (Deactivate,
40
+ Delete) de-emphasized — a secondary button, or an overflow menu — and gate
41
+ them with a `confirm-dialog`.
42
+ - Do **not** add a `form-actions` footer here. `form-actions` (Save/Cancel) is
43
+ for the bottom of an editable form; a read view has nothing to save. Reserve
44
+ it for the form page — which is where Edit navigates.
45
+ - Back navigation comes from the breadcrumb link, so there is no separate Close
46
+ button.
47
+ - Constrain the record body with a `max-width` (about `40rem`–`48rem`) and fence
48
+ distinct field groups with `frame-box`.
49
+
50
+ **Live demo:** `demo/layouts/detail-only.html`
51
+
52
+ **Components:** app-shell, app-sidebar, page-header, frame-box, ui-button