@xmesh/system-design 0.0.1 → 0.0.3

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.
@@ -0,0 +1,34 @@
1
+ /* ============================================
2
+ Popover — a click-triggered anchored panel of arbitrary content.
3
+
4
+ The popover surface chrome (inverse-surface fill, inverse-on-surface ink,
5
+ hairline border, level3 elevation, edge-flip, open/close motion) is owned by
6
+ the composed <xm-overlay> in the `menu` tier. This file styles only the
7
+ trigger wrapper and the content panel that rides inside the overlay body.
8
+
9
+ All ink is inverse-on-surface (AD-13) — the popover/card stack ink. The panel
10
+ is role="dialog" and receives initial focus, so its focus outline is
11
+ suppressed (the moved-in focus is the affordance, not a ring on the box).
12
+
13
+ BEM block: `popover`. Registered in scripts/check-bem.sh STRICT_BLOCKS.
14
+ Elements: popover__trigger, popover__panel.
15
+ ============================================ */
16
+
17
+ .popover__trigger {
18
+ display: inline-flex;
19
+ align-items: center;
20
+ }
21
+
22
+ .popover__panel {
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: var(--s-2);
26
+ min-width: 180px;
27
+ color: var(--md-sys-color-inverse-on-surface);
28
+ }
29
+
30
+ /* role="dialog" + tabindex=-1: we move focus here on open, so the box itself
31
+ never shows a focus ring — interactive children own their own focus states. */
32
+ .popover__panel:focus {
33
+ outline: none;
34
+ }
@@ -0,0 +1,29 @@
1
+ import { LitElement } from "lit";
2
+ import type { TemplateResult } from "lit";
3
+ import type { OverlayPlacement } from "../overlay/index.js";
4
+ export declare class XmPopover extends LitElement {
5
+ open: boolean;
6
+ placement: OverlayPlacement;
7
+ label: string;
8
+ private _overlay;
9
+ private _panel;
10
+ private _triggerWrap;
11
+ private readonly _panelId;
12
+ private _closeReason;
13
+ private _wasOpen;
14
+ connectedCallback(): void;
15
+ disconnectedCallback(): void;
16
+ render(): TemplateResult;
17
+ private _onTriggerClick;
18
+ private _onDocPointer;
19
+ private _onOverlayClose;
20
+ private _requestClose;
21
+ protected updated(changed: Map<string, unknown>): void;
22
+ private get _triggerEl();
23
+ private _wireTriggerAria;
24
+ }
25
+ declare global {
26
+ interface HTMLElementTagNameMap {
27
+ "xm-popover": XmPopover;
28
+ }
29
+ }
@@ -0,0 +1,204 @@
1
+ /*
2
+ popover/index.ts — <xm-popover>, a click-triggered anchored panel of
3
+ arbitrary content.
4
+
5
+ The generalized sibling of <xm-menu>: where the menu projects a list of
6
+ <xm-menu-item> children and owns the APG `menu` roving model, the popover
7
+ projects ANY content (a column toggle list, a small form, a filter panel) and
8
+ leaves interaction to that content. Both compose the same xm-overlay
9
+ foundation (Story 1.4) for anchored positioning, top-layer stacking, Esc
10
+ dismissal, and focus-restore — neither hand-rolls z-index or anchoring math.
11
+ We drive the overlay through its PUBLIC API only (mode / tier / placement /
12
+ .anchor / .opener / show / hide / xm-overlay-close) and never reach into its
13
+ shadow root (AD-12).
14
+
15
+ Behaviour:
16
+ • clicking the trigger opens an anchored panel; clicking it again, clicking
17
+ outside, or Esc closes it
18
+ • the panel is role="dialog" and takes initial focus on open; focus
19
+ restores to the trigger on close (delegated to the overlay via .opener)
20
+ • outside-click dismiss is owned here: the overlay runs popover="manual"
21
+ (no native light-dismiss) so a click whose composed path excludes this
22
+ host closes the panel
23
+
24
+ Events (AD-8 / AD-8a, tier (b) — component-specific signals):
25
+ xm-popover-open detail: {}
26
+ xm-popover-close detail: { reason } reason ∈ "trigger"|"outside"|"overlay"
27
+ both bubbles:true, composed:true.
28
+
29
+ Authoring:
30
+ <xm-popover placement="bottom-end" label="Columns">
31
+ <xm-button slot="trigger" variant="ghost" size="sm">Columns</xm-button>
32
+ <div class="my-panel">…arbitrary content…</div>
33
+ </xm-popover>
34
+
35
+ Shadow DOM. Depends on xm-overlay + tokens (AD-12). Lit is a bare `import`.
36
+ */
37
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
38
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
39
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
40
+ 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;
41
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
42
+ };
43
+ import { LitElement, html } from "lit";
44
+ import { customElement, property, query } from "lit/decorators.js";
45
+ const POPOVER_CSS = new URL("../popover/index.css", import.meta.url).href;
46
+ let popoverSeq = 0;
47
+ let XmPopover = class XmPopover extends LitElement {
48
+ constructor() {
49
+ super(...arguments);
50
+ this.open = false;
51
+ this.placement = "bottom-start";
52
+ this.label = "";
53
+ this._panelId = `xm-popover-${++popoverSeq}`;
54
+ this._closeReason = "api";
55
+ this._wasOpen = false;
56
+ // ── Open / close ──────────────────────────────────────────────────────
57
+ this._onTriggerClick = () => {
58
+ if (this.open)
59
+ this._requestClose("trigger");
60
+ else
61
+ this.open = true;
62
+ };
63
+ this._onDocPointer = (e) => {
64
+ if (!this.open)
65
+ return;
66
+ if (!e.composedPath().includes(this))
67
+ this._requestClose("outside");
68
+ };
69
+ this._onOverlayClose = () => {
70
+ // Overlay self-dismissed (Esc); reflect it into our state. updated() owns
71
+ // the single overlay hide + xm-popover-close emission.
72
+ if (this.open)
73
+ this._requestClose("escape");
74
+ };
75
+ this._wireTriggerAria = (e) => {
76
+ const slot = e.target;
77
+ const el = slot.assignedElements()[0];
78
+ if (el) {
79
+ el.setAttribute("aria-haspopup", "dialog");
80
+ el.setAttribute("aria-expanded", this.open ? "true" : "false");
81
+ el.setAttribute("aria-controls", this._panelId);
82
+ }
83
+ };
84
+ }
85
+ connectedCallback() {
86
+ super.connectedCallback();
87
+ document.addEventListener("pointerdown", this._onDocPointer, true);
88
+ }
89
+ disconnectedCallback() {
90
+ super.disconnectedCallback();
91
+ document.removeEventListener("pointerdown", this._onDocPointer, true);
92
+ }
93
+ render() {
94
+ return html `
95
+ <link rel="stylesheet" href="${POPOVER_CSS}" />
96
+ <style>
97
+ :host { display: inline-flex; }
98
+ </style>
99
+ <span class="popover__trigger" @click=${this._onTriggerClick}>
100
+ <slot name="trigger" @slotchange=${this._wireTriggerAria}></slot>
101
+ </span>
102
+
103
+ <xm-overlay
104
+ mode="non-modal"
105
+ tier="menu"
106
+ placement=${this.placement}
107
+ label=${this.label || "Popover"}
108
+ @xm-overlay-close=${this._onOverlayClose}
109
+ >
110
+ <div
111
+ class="popover__panel"
112
+ id=${this._panelId}
113
+ role="dialog"
114
+ aria-label=${this.label || "Popover"}
115
+ tabindex="-1"
116
+ >
117
+ <slot></slot>
118
+ </div>
119
+ </xm-overlay>
120
+ `;
121
+ }
122
+ // Record the reason and flip `open`; the actual overlay hide + the single
123
+ // xm-popover-close emission happen in updated(), so every close path
124
+ // (trigger, outside, Esc, programmatic open=false) is one code path that
125
+ // fires exactly once.
126
+ _requestClose(reason) {
127
+ if (!this.open)
128
+ return;
129
+ this._closeReason = reason;
130
+ this.open = false;
131
+ }
132
+ updated(changed) {
133
+ if (!changed.has("open"))
134
+ return;
135
+ const ov = this._overlay;
136
+ const trigger = this._triggerEl;
137
+ if (this.open) {
138
+ if (ov && trigger) {
139
+ ov.anchor = trigger;
140
+ ov.opener = trigger;
141
+ ov.show();
142
+ // Land focus in the panel so the content is reachable; the overlay
143
+ // restores focus to the trigger on close.
144
+ requestAnimationFrame(() => {
145
+ if (this.open)
146
+ this._panel?.focus();
147
+ });
148
+ }
149
+ this._wasOpen = true;
150
+ this.dispatchEvent(new CustomEvent("xm-popover-open", {
151
+ bubbles: true,
152
+ composed: true,
153
+ detail: {},
154
+ }));
155
+ }
156
+ else {
157
+ if (ov?.open)
158
+ ov.hide("api");
159
+ // Guard the initial render (open=false from the start) so it never emits
160
+ // a phantom close; only a real open→close transition fires the event.
161
+ if (this._wasOpen) {
162
+ this._wasOpen = false;
163
+ this.dispatchEvent(new CustomEvent("xm-popover-close", {
164
+ bubbles: true,
165
+ composed: true,
166
+ detail: { reason: this._closeReason },
167
+ }));
168
+ }
169
+ this._closeReason = "api";
170
+ }
171
+ if (trigger) {
172
+ trigger.setAttribute("aria-expanded", this.open ? "true" : "false");
173
+ }
174
+ }
175
+ // Prefer the slotted trigger; fall back to the wrapper so an `open` set before
176
+ // the trigger element is assigned still anchors + shows (no stuck state with
177
+ // aria-expanded="true" and no panel).
178
+ get _triggerEl() {
179
+ const slot = this.renderRoot.querySelector('slot[name="trigger"]');
180
+ return (slot?.assignedElements()[0] ?? this._triggerWrap ?? null);
181
+ }
182
+ };
183
+ __decorate([
184
+ property({ type: Boolean, reflect: true })
185
+ ], XmPopover.prototype, "open", void 0);
186
+ __decorate([
187
+ property({ type: String })
188
+ ], XmPopover.prototype, "placement", void 0);
189
+ __decorate([
190
+ property({ type: String })
191
+ ], XmPopover.prototype, "label", void 0);
192
+ __decorate([
193
+ query("xm-overlay")
194
+ ], XmPopover.prototype, "_overlay", void 0);
195
+ __decorate([
196
+ query(".popover__panel")
197
+ ], XmPopover.prototype, "_panel", void 0);
198
+ __decorate([
199
+ query(".popover__trigger")
200
+ ], XmPopover.prototype, "_triggerWrap", void 0);
201
+ XmPopover = __decorate([
202
+ customElement("xm-popover")
203
+ ], XmPopover);
204
+ export { XmPopover };
@@ -2,10 +2,9 @@
2
2
  table/index.ts — <xm-table>.
3
3
 
4
4
  Static, semantic, hairline-ruled table. Renders a real <table> in
5
- its shadow root. Two authoring modes:
5
+ its shadow root from a data-driven { columns, rows } model (the
6
+ shape xm-data-table composes in Epic 6):
6
7
 
7
- 1. Data-driven (preferred; this is the shape xm-data-table composes
8
- in Epic 6):
9
8
  const t = document.querySelector("xm-table");
10
9
  t.columns = [
11
10
  { key: "name", label: "File name" },
@@ -16,10 +15,7 @@
16
15
  { name: "routing.yaml", type: "YAML", size: "8.4 KB" },
17
16
  { name: "schema.json", type: "JSON", size: "412 KB" },
18
17
  ];
19
- Or set columns/rows as JSON via the `columns` / `rows` attributes.
20
-
21
- 2. Slotted (escape hatch for fully bespoke markup): author your own
22
- <table> structure inside the element; it is projected verbatim.
18
+ Or set columns/rows as JSON via the `columns` / `rows` attributes.
23
19
 
24
20
  Column model: { key, label, numeric?, align?, muted? }.
25
21
  numeric → right-align + tabular figures (NFR-22). Put the unit in
@@ -75,7 +71,6 @@ let XmTable = class XmTable extends LitElement {
75
71
  return Boolean(col.numeric);
76
72
  }
77
73
  render() {
78
- const dataMode = this.columns.length > 0;
79
74
  return html `
80
75
  <link rel="stylesheet" href="${TABLE_CSS}" />
81
76
  <style>
@@ -86,7 +81,7 @@ let XmTable = class XmTable extends LitElement {
86
81
  display: none;
87
82
  }
88
83
  </style>
89
- ${dataMode ? this._renderData() : html `<slot></slot>`}
84
+ ${this._renderData()}
90
85
  `;
91
86
  }
92
87
  _renderData() {
@@ -18,6 +18,7 @@ export * from "./components/chip-group/index.js";
18
18
  export * from "./components/code/index.js";
19
19
  export * from "./components/composer/index.js";
20
20
  export * from "./components/data-table/index.js";
21
+ export * from "./components/date-range/index.js";
21
22
  export * from "./components/dialog/index.js";
22
23
  export * from "./components/divider/index.js";
23
24
  export * from "./components/empty-state/index.js";
@@ -33,6 +34,7 @@ export * from "./components/menu/index.js";
33
34
  export * from "./components/navigation-drawer/index.js";
34
35
  export * from "./components/overlay/index.js";
35
36
  export * from "./components/pagination/index.js";
37
+ export * from "./components/popover/index.js";
36
38
  export * from "./components/progress/index.js";
37
39
  export * from "./components/radio-group/index.js";
38
40
  export * from "./components/select/index.js";
package/dist/lit/index.js CHANGED
@@ -27,6 +27,7 @@ export * from "./components/chip-group/index.js";
27
27
  export * from "./components/code/index.js";
28
28
  export * from "./components/composer/index.js";
29
29
  export * from "./components/data-table/index.js";
30
+ export * from "./components/date-range/index.js";
30
31
  export * from "./components/dialog/index.js";
31
32
  export * from "./components/divider/index.js";
32
33
  export * from "./components/empty-state/index.js";
@@ -42,6 +43,7 @@ export * from "./components/menu/index.js";
42
43
  export * from "./components/navigation-drawer/index.js";
43
44
  export * from "./components/overlay/index.js";
44
45
  export * from "./components/pagination/index.js";
46
+ export * from "./components/popover/index.js";
45
47
  export * from "./components/progress/index.js";
46
48
  export * from "./components/radio-group/index.js";
47
49
  export * from "./components/select/index.js";
package/package.json CHANGED
@@ -1,12 +1,30 @@
1
1
  {
2
2
  "name": "@xmesh/system-design",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
8
8
  "type": "module",
9
9
  "description": "Design-system reference and Lit component library for the xmesh chat product.",
10
+ "license": "Apache-2.0",
11
+ "author": "xmesh",
12
+ "homepage": "https://sergeytkachenko.github.io/xmesh-system-design/",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/sergeytkachenko/xmesh-system-design.git"
16
+ },
17
+ "bugs": "https://github.com/sergeytkachenko/xmesh-system-design/issues",
18
+ "keywords": [
19
+ "design-system",
20
+ "lit",
21
+ "web-components",
22
+ "material-design",
23
+ "md3",
24
+ "design-tokens",
25
+ "xmesh",
26
+ "chat-ui"
27
+ ],
10
28
  "exports": {
11
29
  "./lit": {
12
30
  "types": "./dist/lit/index.d.ts",