@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.
- package/README.md +16 -2
- package/custom-elements.json +1071 -79
- package/dist/action-bar.d.ts +24 -0
- package/dist/action-bar.d.ts.map +1 -0
- package/dist/action-bar.js +67 -0
- package/dist/action-bar.js.map +1 -0
- package/dist/app-shell.d.ts +86 -0
- package/dist/app-shell.d.ts.map +1 -0
- package/dist/app-shell.js +460 -0
- package/dist/app-shell.js.map +1 -0
- package/dist/app-sidebar.d.ts +44 -0
- package/dist/app-sidebar.d.ts.map +1 -0
- package/dist/app-sidebar.js +211 -0
- package/dist/app-sidebar.js.map +1 -0
- package/dist/content-divider.d.ts +23 -0
- package/dist/content-divider.d.ts.map +1 -0
- package/dist/content-divider.js +86 -0
- package/dist/content-divider.js.map +1 -0
- package/dist/form-actions.d.ts +27 -0
- package/dist/form-actions.d.ts.map +1 -0
- package/dist/form-actions.js +65 -0
- package/dist/form-actions.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.js +82 -5
- package/dist/mcp-server.js.map +1 -1
- package/dist/page-header.d.ts +27 -0
- package/dist/page-header.d.ts.map +1 -0
- package/dist/page-header.js +106 -0
- package/dist/page-header.js.map +1 -0
- package/dist/pagination-nav.d.ts +34 -0
- package/dist/pagination-nav.d.ts.map +1 -0
- package/dist/pagination-nav.js +144 -0
- package/dist/pagination-nav.js.map +1 -0
- package/dist/timeline-container.d.ts +22 -0
- package/dist/timeline-container.d.ts.map +1 -0
- package/dist/timeline-container.js +42 -0
- package/dist/timeline-container.js.map +1 -0
- package/dist/timeline-entry.d.ts +31 -0
- package/dist/timeline-entry.d.ts.map +1 -0
- package/dist/timeline-entry.js +170 -0
- package/dist/timeline-entry.js.map +1 -0
- package/docs/action-bar.md +41 -0
- package/docs/app-shell.md +85 -0
- package/docs/app-sidebar.md +69 -0
- package/docs/content-divider.md +47 -0
- package/docs/design-language.md +74 -1
- package/docs/form-actions.md +43 -0
- package/docs/layouts/detail-only.md +52 -0
- package/docs/layouts/form-page.md +52 -0
- package/docs/layouts/list-detail.md +52 -0
- package/docs/layouts/list-only.md +50 -0
- package/docs/layouts/record-detail.md +88 -0
- package/docs/mcp-evaluation.md +21 -0
- package/docs/page-header.md +47 -0
- package/docs/pagination-nav.md +54 -0
- package/docs/timeline-container.md +44 -0
- package/docs/timeline-entry.md +52 -0
- package/llms.txt +251 -0
- 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` |
|
package/docs/design-language.md
CHANGED
|
@@ -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`,
|
|
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
|