@aceshooting/lyra-ui 0.1.0 → 0.1.1
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/dist/components/empty/empty.d.ts +31 -0
- package/dist/components/empty/empty.js +78 -0
- package/dist/components/empty/empty.styles.d.ts +1 -0
- package/dist/components/empty/empty.styles.js +42 -0
- package/dist/components/skeleton/skeleton.d.ts +26 -0
- package/dist/components/skeleton/skeleton.js +65 -0
- package/dist/components/skeleton/skeleton.styles.d.ts +1 -0
- package/dist/components/skeleton/skeleton.styles.js +57 -0
- package/dist/components/stat/stat.d.ts +27 -0
- package/dist/components/stat/stat.js +72 -0
- package/dist/components/stat/stat.styles.d.ts +1 -0
- package/dist/components/stat/stat.styles.js +76 -0
- package/dist/components/table/table.d.ts +42 -0
- package/dist/components/table/table.js +149 -0
- package/dist/components/table/table.styles.d.ts +1 -0
- package/dist/components/table/table.styles.js +66 -0
- package/package.json +9 -9
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type TemplateResult } from 'lit';
|
|
2
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
/**
|
|
4
|
+
* `<lyra-empty>` — a generic empty/no-data state. First-party invention (no
|
|
5
|
+
* Web Awesome equivalent); fills the gap identified across ~30 surveyed repos.
|
|
6
|
+
*
|
|
7
|
+
* @customElement lyra-empty
|
|
8
|
+
* @slot - Custom icon or illustration (defaults to none).
|
|
9
|
+
* @slot actions - Buttons/links shown below the description.
|
|
10
|
+
* @csspart base, icon, heading, description, actions
|
|
11
|
+
*/
|
|
12
|
+
export declare class LyraEmpty extends LyraElement {
|
|
13
|
+
static styles: import("lit").CSSResultGroup[];
|
|
14
|
+
/** Short heading, e.g. "No results". */
|
|
15
|
+
heading: string;
|
|
16
|
+
/** Supporting copy, e.g. "Try a different search." */
|
|
17
|
+
description: string;
|
|
18
|
+
private hasIcon;
|
|
19
|
+
private hasActions;
|
|
20
|
+
firstUpdated(): void;
|
|
21
|
+
private checkIconSlot;
|
|
22
|
+
private checkActionsSlot;
|
|
23
|
+
private onIconSlotChange;
|
|
24
|
+
private onActionsSlotChange;
|
|
25
|
+
render(): TemplateResult;
|
|
26
|
+
}
|
|
27
|
+
declare global {
|
|
28
|
+
interface HTMLElementTagNameMap {
|
|
29
|
+
'lyra-empty': LyraEmpty;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
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 { html } from 'lit';
|
|
8
|
+
import { property, state } from 'lit/decorators.js';
|
|
9
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
+
import { styles } from './empty.styles.js';
|
|
12
|
+
/**
|
|
13
|
+
* `<lyra-empty>` — a generic empty/no-data state. First-party invention (no
|
|
14
|
+
* Web Awesome equivalent); fills the gap identified across ~30 surveyed repos.
|
|
15
|
+
*
|
|
16
|
+
* @customElement lyra-empty
|
|
17
|
+
* @slot - Custom icon or illustration (defaults to none).
|
|
18
|
+
* @slot actions - Buttons/links shown below the description.
|
|
19
|
+
* @csspart base, icon, heading, description, actions
|
|
20
|
+
*/
|
|
21
|
+
export class LyraEmpty extends LyraElement {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(...arguments);
|
|
24
|
+
/** Short heading, e.g. "No results". */
|
|
25
|
+
this.heading = '';
|
|
26
|
+
/** Supporting copy, e.g. "Try a different search." */
|
|
27
|
+
this.description = '';
|
|
28
|
+
// `[part='icon']:empty` never matches because the part always contains a
|
|
29
|
+
// `<slot>` element (CSS `:empty` only ignores text/comment nodes). Track
|
|
30
|
+
// real slot assignment in JS instead and key the CSS off these instead.
|
|
31
|
+
this.hasIcon = false;
|
|
32
|
+
this.hasActions = false;
|
|
33
|
+
this.onIconSlotChange = (e) => {
|
|
34
|
+
this.checkIconSlot(e.target);
|
|
35
|
+
};
|
|
36
|
+
this.onActionsSlotChange = (e) => {
|
|
37
|
+
this.checkActionsSlot(e.target);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
41
|
+
firstUpdated() {
|
|
42
|
+
// `slotchange` isn't guaranteed to fire for content already present at
|
|
43
|
+
// parse/upgrade time in every browser, so do an initial check here too.
|
|
44
|
+
this.checkIconSlot(this.shadowRoot.querySelector('slot:not([name])'));
|
|
45
|
+
this.checkActionsSlot(this.shadowRoot.querySelector('slot[name="actions"]'));
|
|
46
|
+
}
|
|
47
|
+
checkIconSlot(slot) {
|
|
48
|
+
this.hasIcon = slot.assignedElements({ flatten: true }).length > 0;
|
|
49
|
+
}
|
|
50
|
+
checkActionsSlot(slot) {
|
|
51
|
+
this.hasActions = slot.assignedElements({ flatten: true }).length > 0;
|
|
52
|
+
}
|
|
53
|
+
render() {
|
|
54
|
+
return html `
|
|
55
|
+
<div part="base">
|
|
56
|
+
<div part="icon" ?hidden=${!this.hasIcon}><slot @slotchange=${this.onIconSlotChange}></slot></div>
|
|
57
|
+
<p part="heading">${this.heading}</p>
|
|
58
|
+
<p part="description">${this.description}</p>
|
|
59
|
+
<div part="actions" ?hidden=${!this.hasActions}>
|
|
60
|
+
<slot name="actions" @slotchange=${this.onActionsSlotChange}></slot>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
`;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
__decorate([
|
|
67
|
+
property()
|
|
68
|
+
], LyraEmpty.prototype, "heading", void 0);
|
|
69
|
+
__decorate([
|
|
70
|
+
property()
|
|
71
|
+
], LyraEmpty.prototype, "description", void 0);
|
|
72
|
+
__decorate([
|
|
73
|
+
state()
|
|
74
|
+
], LyraEmpty.prototype, "hasIcon", void 0);
|
|
75
|
+
__decorate([
|
|
76
|
+
state()
|
|
77
|
+
], LyraEmpty.prototype, "hasActions", void 0);
|
|
78
|
+
defineElement('empty', LyraEmpty);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
export const styles = css `
|
|
3
|
+
:host {
|
|
4
|
+
display: flex;
|
|
5
|
+
}
|
|
6
|
+
[part='base'] {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
align-items: center;
|
|
10
|
+
text-align: center;
|
|
11
|
+
gap: var(--lyra-space-s);
|
|
12
|
+
padding: var(--lyra-space-l);
|
|
13
|
+
color: var(--lyra-color-text-quiet);
|
|
14
|
+
inline-size: 100%;
|
|
15
|
+
}
|
|
16
|
+
[part='icon'] {
|
|
17
|
+
font-size: 2rem;
|
|
18
|
+
line-height: 1;
|
|
19
|
+
color: var(--lyra-color-border);
|
|
20
|
+
}
|
|
21
|
+
[part='icon'][hidden] {
|
|
22
|
+
display: none;
|
|
23
|
+
}
|
|
24
|
+
[part='heading'] {
|
|
25
|
+
font-weight: 600;
|
|
26
|
+
color: var(--lyra-color-text);
|
|
27
|
+
margin: 0;
|
|
28
|
+
}
|
|
29
|
+
[part='description'] {
|
|
30
|
+
font-size: 0.875rem;
|
|
31
|
+
margin: 0;
|
|
32
|
+
}
|
|
33
|
+
[part='description']:empty {
|
|
34
|
+
display: none;
|
|
35
|
+
}
|
|
36
|
+
[part='actions'] {
|
|
37
|
+
margin-block-start: var(--lyra-space-s);
|
|
38
|
+
}
|
|
39
|
+
[part='actions'][hidden] {
|
|
40
|
+
display: none;
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type TemplateResult, type PropertyValues } from 'lit';
|
|
2
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
export type SkeletonVariant = 'text' | 'circle' | 'rect';
|
|
4
|
+
export type SkeletonEffect = 'pulse' | 'sheen';
|
|
5
|
+
/**
|
|
6
|
+
* `<lyra-skeleton>` — a loading placeholder. First-party invention; every
|
|
7
|
+
* surveyed repo reinvents this as a bespoke `animate-pulse` div.
|
|
8
|
+
*
|
|
9
|
+
* @customElement lyra-skeleton
|
|
10
|
+
* @csspart base - The placeholder shape.
|
|
11
|
+
*/
|
|
12
|
+
export declare class LyraSkeleton extends LyraElement {
|
|
13
|
+
static styles: import("lit").CSSResultGroup[];
|
|
14
|
+
variant: SkeletonVariant;
|
|
15
|
+
effect: SkeletonEffect;
|
|
16
|
+
width?: string;
|
|
17
|
+
height?: string;
|
|
18
|
+
protected willUpdate(): void;
|
|
19
|
+
protected updated(changed: PropertyValues): void;
|
|
20
|
+
render(): TemplateResult;
|
|
21
|
+
}
|
|
22
|
+
declare global {
|
|
23
|
+
interface HTMLElementTagNameMap {
|
|
24
|
+
'lyra-skeleton': LyraSkeleton;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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 { html } from 'lit';
|
|
8
|
+
import { property } from 'lit/decorators.js';
|
|
9
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
+
import { srOnly } from '../../internal/a11y.js';
|
|
12
|
+
import { styles } from './skeleton.styles.js';
|
|
13
|
+
/**
|
|
14
|
+
* `<lyra-skeleton>` — a loading placeholder. First-party invention; every
|
|
15
|
+
* surveyed repo reinvents this as a bespoke `animate-pulse` div.
|
|
16
|
+
*
|
|
17
|
+
* @customElement lyra-skeleton
|
|
18
|
+
* @csspart base - The placeholder shape.
|
|
19
|
+
*/
|
|
20
|
+
export class LyraSkeleton extends LyraElement {
|
|
21
|
+
constructor() {
|
|
22
|
+
super(...arguments);
|
|
23
|
+
this.variant = 'text';
|
|
24
|
+
this.effect = 'pulse';
|
|
25
|
+
}
|
|
26
|
+
static { this.styles = [LyraElement.styles, styles, srOnly]; }
|
|
27
|
+
willUpdate() {
|
|
28
|
+
this.setAttribute('role', 'status');
|
|
29
|
+
}
|
|
30
|
+
updated(changed) {
|
|
31
|
+
if (changed.has('width') || changed.has('height')) {
|
|
32
|
+
const base = this.renderRoot.querySelector('[part="base"]');
|
|
33
|
+
if (!base)
|
|
34
|
+
return;
|
|
35
|
+
if (this.width) {
|
|
36
|
+
base.style.setProperty('--lyra-skeleton-w', this.width);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
base.style.removeProperty('--lyra-skeleton-w');
|
|
40
|
+
}
|
|
41
|
+
if (this.height) {
|
|
42
|
+
base.style.setProperty('--lyra-skeleton-h', this.height);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
base.style.removeProperty('--lyra-skeleton-h');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
render() {
|
|
50
|
+
return html `<span part="base"><span class="sr-only">Loading…</span></span>`;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
__decorate([
|
|
54
|
+
property({ reflect: true })
|
|
55
|
+
], LyraSkeleton.prototype, "variant", void 0);
|
|
56
|
+
__decorate([
|
|
57
|
+
property({ reflect: true })
|
|
58
|
+
], LyraSkeleton.prototype, "effect", void 0);
|
|
59
|
+
__decorate([
|
|
60
|
+
property()
|
|
61
|
+
], LyraSkeleton.prototype, "width", void 0);
|
|
62
|
+
__decorate([
|
|
63
|
+
property()
|
|
64
|
+
], LyraSkeleton.prototype, "height", void 0);
|
|
65
|
+
defineElement('skeleton', LyraSkeleton);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
export const styles = css `
|
|
3
|
+
:host {
|
|
4
|
+
display: inline-block;
|
|
5
|
+
inline-size: var(--lyra-skeleton-w, 100%);
|
|
6
|
+
block-size: var(--lyra-skeleton-h, 1em);
|
|
7
|
+
}
|
|
8
|
+
[part='base'] {
|
|
9
|
+
inline-size: 100%;
|
|
10
|
+
block-size: 100%;
|
|
11
|
+
background: var(--lyra-color-border);
|
|
12
|
+
}
|
|
13
|
+
:host([variant='text']) [part='base'] {
|
|
14
|
+
border-radius: var(--lyra-radius);
|
|
15
|
+
}
|
|
16
|
+
:host([variant='circle']) [part='base'] {
|
|
17
|
+
border-radius: 50%;
|
|
18
|
+
}
|
|
19
|
+
:host([variant='rect']) [part='base'] {
|
|
20
|
+
border-radius: var(--lyra-radius);
|
|
21
|
+
}
|
|
22
|
+
:host([effect='pulse']) [part='base'] {
|
|
23
|
+
animation: lyra-skeleton-pulse 1.5s ease-in-out infinite;
|
|
24
|
+
}
|
|
25
|
+
:host([effect='sheen']) [part='base'] {
|
|
26
|
+
background-image: linear-gradient(
|
|
27
|
+
90deg,
|
|
28
|
+
var(--lyra-color-border) 0%,
|
|
29
|
+
var(--lyra-color-surface) 50%,
|
|
30
|
+
var(--lyra-color-border) 100%
|
|
31
|
+
);
|
|
32
|
+
background-size: 200% 100%;
|
|
33
|
+
animation: lyra-skeleton-sheen 1.5s ease-in-out infinite;
|
|
34
|
+
}
|
|
35
|
+
@keyframes lyra-skeleton-pulse {
|
|
36
|
+
0%,
|
|
37
|
+
100% {
|
|
38
|
+
opacity: 1;
|
|
39
|
+
}
|
|
40
|
+
50% {
|
|
41
|
+
opacity: 0.4;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
@keyframes lyra-skeleton-sheen {
|
|
45
|
+
0% {
|
|
46
|
+
background-position: 200% 0;
|
|
47
|
+
}
|
|
48
|
+
100% {
|
|
49
|
+
background-position: -200% 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
@media (prefers-reduced-motion: reduce) {
|
|
53
|
+
[part='base'] {
|
|
54
|
+
animation: none !important;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type TemplateResult } from 'lit';
|
|
2
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
export type StatVariant = 'neutral' | 'success' | 'warning' | 'danger';
|
|
4
|
+
/**
|
|
5
|
+
* `<lyra-stat>` — a KPI/stat card. First-party invention consolidating the
|
|
6
|
+
* "metric row" / "KPI card" pattern reinvented in every surveyed repo.
|
|
7
|
+
*
|
|
8
|
+
* @customElement lyra-stat
|
|
9
|
+
* @slot - Leading icon.
|
|
10
|
+
* @slot caption - Rich caption content (overrides the `caption` attribute).
|
|
11
|
+
* @csspart base, icon, label, value, unit, trend, caption
|
|
12
|
+
*/
|
|
13
|
+
export declare class LyraStat extends LyraElement {
|
|
14
|
+
static styles: import("lit").CSSResultGroup[];
|
|
15
|
+
label: string;
|
|
16
|
+
value: string;
|
|
17
|
+
unit: string;
|
|
18
|
+
variant: StatVariant;
|
|
19
|
+
trend: number;
|
|
20
|
+
caption: string;
|
|
21
|
+
render(): TemplateResult;
|
|
22
|
+
}
|
|
23
|
+
declare global {
|
|
24
|
+
interface HTMLElementTagNameMap {
|
|
25
|
+
'lyra-stat': LyraStat;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
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 { html, nothing } from 'lit';
|
|
8
|
+
import { property } from 'lit/decorators.js';
|
|
9
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
+
import { styles } from './stat.styles.js';
|
|
12
|
+
/**
|
|
13
|
+
* `<lyra-stat>` — a KPI/stat card. First-party invention consolidating the
|
|
14
|
+
* "metric row" / "KPI card" pattern reinvented in every surveyed repo.
|
|
15
|
+
*
|
|
16
|
+
* @customElement lyra-stat
|
|
17
|
+
* @slot - Leading icon.
|
|
18
|
+
* @slot caption - Rich caption content (overrides the `caption` attribute).
|
|
19
|
+
* @csspart base, icon, label, value, unit, trend, caption
|
|
20
|
+
*/
|
|
21
|
+
export class LyraStat extends LyraElement {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(...arguments);
|
|
24
|
+
this.label = '';
|
|
25
|
+
this.value = '';
|
|
26
|
+
this.unit = '';
|
|
27
|
+
this.variant = 'neutral';
|
|
28
|
+
this.trend = NaN;
|
|
29
|
+
this.caption = '';
|
|
30
|
+
}
|
|
31
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
32
|
+
render() {
|
|
33
|
+
const hasTrend = !isNaN(this.trend);
|
|
34
|
+
const direction = this.trend > 0 ? 'up' : this.trend < 0 ? 'down' : 'flat';
|
|
35
|
+
const arrow = direction === 'up' ? '▲' : direction === 'down' ? '▼' : '–';
|
|
36
|
+
return html `
|
|
37
|
+
<div part="base">
|
|
38
|
+
<span part="icon"><slot></slot></span>
|
|
39
|
+
<span part="label">${this.label}</span>
|
|
40
|
+
<div part="value-row">
|
|
41
|
+
<span part="value">${this.value}</span>
|
|
42
|
+
<span part="unit">${this.unit}</span>
|
|
43
|
+
</div>
|
|
44
|
+
${hasTrend
|
|
45
|
+
? html `<span part="trend" data-direction=${direction}>
|
|
46
|
+
${arrow} ${Math.abs(this.trend)}%
|
|
47
|
+
</span>`
|
|
48
|
+
: nothing}
|
|
49
|
+
<div part="caption">${this.caption}<slot name="caption"></slot></div>
|
|
50
|
+
</div>
|
|
51
|
+
`;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
__decorate([
|
|
55
|
+
property()
|
|
56
|
+
], LyraStat.prototype, "label", void 0);
|
|
57
|
+
__decorate([
|
|
58
|
+
property()
|
|
59
|
+
], LyraStat.prototype, "value", void 0);
|
|
60
|
+
__decorate([
|
|
61
|
+
property()
|
|
62
|
+
], LyraStat.prototype, "unit", void 0);
|
|
63
|
+
__decorate([
|
|
64
|
+
property({ reflect: true })
|
|
65
|
+
], LyraStat.prototype, "variant", void 0);
|
|
66
|
+
__decorate([
|
|
67
|
+
property({ type: Number })
|
|
68
|
+
], LyraStat.prototype, "trend", void 0);
|
|
69
|
+
__decorate([
|
|
70
|
+
property()
|
|
71
|
+
], LyraStat.prototype, "caption", void 0);
|
|
72
|
+
defineElement('stat', LyraStat);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
export const styles = css `
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
}
|
|
6
|
+
[part='base'] {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
gap: var(--lyra-space-xs);
|
|
10
|
+
padding: var(--lyra-space-m);
|
|
11
|
+
border: 1px solid var(--lyra-color-border);
|
|
12
|
+
border-radius: var(--lyra-radius);
|
|
13
|
+
background: var(--lyra-color-surface);
|
|
14
|
+
}
|
|
15
|
+
[part='icon'] {
|
|
16
|
+
color: var(--lyra-color-text-quiet);
|
|
17
|
+
}
|
|
18
|
+
[part='icon']:empty {
|
|
19
|
+
display: none;
|
|
20
|
+
}
|
|
21
|
+
[part='label'] {
|
|
22
|
+
font-size: 0.75rem;
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
text-transform: uppercase;
|
|
25
|
+
letter-spacing: 0.04em;
|
|
26
|
+
color: var(--lyra-color-text-quiet);
|
|
27
|
+
}
|
|
28
|
+
[part='value-row'] {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: baseline;
|
|
31
|
+
gap: var(--lyra-space-xs);
|
|
32
|
+
}
|
|
33
|
+
[part='value'] {
|
|
34
|
+
font-size: 1.75rem;
|
|
35
|
+
font-weight: 700;
|
|
36
|
+
font-family: ui-monospace, SFMono-Regular, monospace;
|
|
37
|
+
}
|
|
38
|
+
[part='unit'] {
|
|
39
|
+
font-size: 0.875rem;
|
|
40
|
+
color: var(--lyra-color-text-quiet);
|
|
41
|
+
}
|
|
42
|
+
[part='trend'] {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: 0.2rem;
|
|
46
|
+
font-size: 0.8125rem;
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
border-radius: var(--lyra-radius);
|
|
49
|
+
padding: 0.05rem 0.4rem;
|
|
50
|
+
align-self: flex-start;
|
|
51
|
+
}
|
|
52
|
+
[part='trend'][data-direction='up'] {
|
|
53
|
+
color: var(--lyra-color-success);
|
|
54
|
+
background: color-mix(in srgb, var(--lyra-color-success) 8%, transparent);
|
|
55
|
+
}
|
|
56
|
+
[part='trend'][data-direction='down'] {
|
|
57
|
+
color: var(--lyra-color-danger);
|
|
58
|
+
background: color-mix(in srgb, var(--lyra-color-danger) 8%, transparent);
|
|
59
|
+
}
|
|
60
|
+
:host([variant='success']) [part='value'] {
|
|
61
|
+
color: var(--lyra-color-success);
|
|
62
|
+
}
|
|
63
|
+
:host([variant='warning']) [part='value'] {
|
|
64
|
+
color: var(--lyra-color-warning);
|
|
65
|
+
}
|
|
66
|
+
:host([variant='danger']) [part='value'] {
|
|
67
|
+
color: var(--lyra-color-danger);
|
|
68
|
+
}
|
|
69
|
+
[part='caption'] {
|
|
70
|
+
font-size: 0.8125rem;
|
|
71
|
+
color: var(--lyra-color-text-quiet);
|
|
72
|
+
}
|
|
73
|
+
[part='caption']:empty {
|
|
74
|
+
display: none;
|
|
75
|
+
}
|
|
76
|
+
`;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type TemplateResult } from 'lit';
|
|
2
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
import '../empty/empty.js';
|
|
4
|
+
export interface TableColumn<T> {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
sortable?: boolean;
|
|
8
|
+
align?: 'start' | 'end';
|
|
9
|
+
cell: (row: T) => unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `<lyra-table>` — a presentational, sort/select-aware data table. The host
|
|
13
|
+
* owns actual sorting/filtering/pagination of `rows`; this component renders
|
|
14
|
+
* and emits intents (`lyra-sort`, `lyra-row-click`, `lyra-load-more`).
|
|
15
|
+
*
|
|
16
|
+
* @customElement lyra-table
|
|
17
|
+
* @event lyra-sort - A sortable header was activated. `detail: { key }`.
|
|
18
|
+
* @event lyra-row-click - A row was activated. `detail: { row }`.
|
|
19
|
+
* @event lyra-load-more - The "load more" control was activated.
|
|
20
|
+
* @csspart base, table, head, header-cell, row, cell, more-button
|
|
21
|
+
*/
|
|
22
|
+
export declare class LyraTable<T = unknown> extends LyraElement {
|
|
23
|
+
static styles: import("lit").CSSResultGroup[];
|
|
24
|
+
columns: TableColumn<T>[];
|
|
25
|
+
rows: T[];
|
|
26
|
+
sortKey: string;
|
|
27
|
+
sortDir: 'asc' | 'desc';
|
|
28
|
+
rowKey?: (row: T) => string | number;
|
|
29
|
+
selectedKey: string | number | null;
|
|
30
|
+
hasMore: boolean;
|
|
31
|
+
moreLabel: string;
|
|
32
|
+
emptyHeading: string;
|
|
33
|
+
emptyDescription: string;
|
|
34
|
+
private keyOf;
|
|
35
|
+
private onHeaderClick;
|
|
36
|
+
render(): TemplateResult;
|
|
37
|
+
}
|
|
38
|
+
declare global {
|
|
39
|
+
interface HTMLElementTagNameMap {
|
|
40
|
+
'lyra-table': LyraTable;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
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 { html, nothing } from 'lit';
|
|
8
|
+
import { property } from 'lit/decorators.js';
|
|
9
|
+
import { repeat } from 'lit/directives/repeat.js';
|
|
10
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
11
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
12
|
+
import { styles } from './table.styles.js';
|
|
13
|
+
import '../empty/empty.js';
|
|
14
|
+
/**
|
|
15
|
+
* `<lyra-table>` — a presentational, sort/select-aware data table. The host
|
|
16
|
+
* owns actual sorting/filtering/pagination of `rows`; this component renders
|
|
17
|
+
* and emits intents (`lyra-sort`, `lyra-row-click`, `lyra-load-more`).
|
|
18
|
+
*
|
|
19
|
+
* @customElement lyra-table
|
|
20
|
+
* @event lyra-sort - A sortable header was activated. `detail: { key }`.
|
|
21
|
+
* @event lyra-row-click - A row was activated. `detail: { row }`.
|
|
22
|
+
* @event lyra-load-more - The "load more" control was activated.
|
|
23
|
+
* @csspart base, table, head, header-cell, row, cell, more-button
|
|
24
|
+
*/
|
|
25
|
+
export class LyraTable extends LyraElement {
|
|
26
|
+
constructor() {
|
|
27
|
+
super(...arguments);
|
|
28
|
+
this.columns = [];
|
|
29
|
+
this.rows = [];
|
|
30
|
+
this.sortKey = '';
|
|
31
|
+
this.sortDir = 'asc';
|
|
32
|
+
this.selectedKey = null;
|
|
33
|
+
this.hasMore = false;
|
|
34
|
+
this.moreLabel = 'Load more';
|
|
35
|
+
this.emptyHeading = 'No data';
|
|
36
|
+
this.emptyDescription = '';
|
|
37
|
+
}
|
|
38
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
39
|
+
keyOf(row, index) {
|
|
40
|
+
return this.rowKey ? this.rowKey(row) : index;
|
|
41
|
+
}
|
|
42
|
+
onHeaderClick(col) {
|
|
43
|
+
if (!col.sortable)
|
|
44
|
+
return;
|
|
45
|
+
this.emit('lyra-sort', { key: col.key });
|
|
46
|
+
}
|
|
47
|
+
render() {
|
|
48
|
+
if (this.rows.length === 0) {
|
|
49
|
+
return html `<lyra-empty
|
|
50
|
+
heading=${this.emptyHeading}
|
|
51
|
+
description=${this.emptyDescription}
|
|
52
|
+
></lyra-empty>`;
|
|
53
|
+
}
|
|
54
|
+
return html `
|
|
55
|
+
<div part="base">
|
|
56
|
+
<table part="table" role="grid">
|
|
57
|
+
<thead part="head">
|
|
58
|
+
<tr role="row">
|
|
59
|
+
${this.columns.map((col) => {
|
|
60
|
+
const ariaSort = this.sortKey === col.key ? (this.sortDir === 'asc' ? 'ascending' : 'descending') : 'none';
|
|
61
|
+
return html `<th
|
|
62
|
+
part="header-cell"
|
|
63
|
+
role="columnheader"
|
|
64
|
+
scope="col"
|
|
65
|
+
data-align=${col.align ?? 'start'}
|
|
66
|
+
?data-sortable=${col.sortable}
|
|
67
|
+
aria-sort=${col.sortable ? ariaSort : nothing}
|
|
68
|
+
tabindex=${col.sortable ? '0' : '-1'}
|
|
69
|
+
@click=${() => this.onHeaderClick(col)}
|
|
70
|
+
@keydown=${(e) => {
|
|
71
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
this.onHeaderClick(col);
|
|
74
|
+
}
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
${col.label}
|
|
78
|
+
</th>`;
|
|
79
|
+
})}
|
|
80
|
+
</tr>
|
|
81
|
+
</thead>
|
|
82
|
+
<tbody>
|
|
83
|
+
${repeat(this.rows, (row, i) => this.keyOf(row, i), (row, i) => {
|
|
84
|
+
const key = this.keyOf(row, i);
|
|
85
|
+
const selected = this.selectedKey !== null && this.selectedKey === key;
|
|
86
|
+
return html `<tr
|
|
87
|
+
part="row"
|
|
88
|
+
role="row"
|
|
89
|
+
aria-selected=${selected ? 'true' : 'false'}
|
|
90
|
+
tabindex="0"
|
|
91
|
+
@click=${() => this.emit('lyra-row-click', { row })}
|
|
92
|
+
@keydown=${(e) => {
|
|
93
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
this.emit('lyra-row-click', { row });
|
|
96
|
+
}
|
|
97
|
+
}}
|
|
98
|
+
>
|
|
99
|
+
${this.columns.map((col) => html `<td part="cell" role="gridcell" data-align=${col.align ?? 'start'}>
|
|
100
|
+
${col.cell(row)}
|
|
101
|
+
</td>`)}
|
|
102
|
+
</tr>`;
|
|
103
|
+
})}
|
|
104
|
+
</tbody>
|
|
105
|
+
</table>
|
|
106
|
+
${this.hasMore
|
|
107
|
+
? html `<button
|
|
108
|
+
part="more-button"
|
|
109
|
+
type="button"
|
|
110
|
+
@click=${() => this.emit('lyra-load-more')}
|
|
111
|
+
>
|
|
112
|
+
${this.moreLabel}
|
|
113
|
+
</button>`
|
|
114
|
+
: nothing}
|
|
115
|
+
</div>
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
__decorate([
|
|
120
|
+
property({ attribute: false })
|
|
121
|
+
], LyraTable.prototype, "columns", void 0);
|
|
122
|
+
__decorate([
|
|
123
|
+
property({ attribute: false })
|
|
124
|
+
], LyraTable.prototype, "rows", void 0);
|
|
125
|
+
__decorate([
|
|
126
|
+
property({ attribute: 'sort-key' })
|
|
127
|
+
], LyraTable.prototype, "sortKey", void 0);
|
|
128
|
+
__decorate([
|
|
129
|
+
property({ attribute: 'sort-dir' })
|
|
130
|
+
], LyraTable.prototype, "sortDir", void 0);
|
|
131
|
+
__decorate([
|
|
132
|
+
property({ attribute: false })
|
|
133
|
+
], LyraTable.prototype, "rowKey", void 0);
|
|
134
|
+
__decorate([
|
|
135
|
+
property({ attribute: false })
|
|
136
|
+
], LyraTable.prototype, "selectedKey", void 0);
|
|
137
|
+
__decorate([
|
|
138
|
+
property({ type: Boolean, attribute: 'has-more', reflect: true })
|
|
139
|
+
], LyraTable.prototype, "hasMore", void 0);
|
|
140
|
+
__decorate([
|
|
141
|
+
property({ attribute: 'more-label' })
|
|
142
|
+
], LyraTable.prototype, "moreLabel", void 0);
|
|
143
|
+
__decorate([
|
|
144
|
+
property({ attribute: 'empty-heading' })
|
|
145
|
+
], LyraTable.prototype, "emptyHeading", void 0);
|
|
146
|
+
__decorate([
|
|
147
|
+
property({ attribute: 'empty-description' })
|
|
148
|
+
], LyraTable.prototype, "emptyDescription", void 0);
|
|
149
|
+
defineElement('table', LyraTable);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
export const styles = css `
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
inline-size: 100%;
|
|
6
|
+
}
|
|
7
|
+
[part='base'] {
|
|
8
|
+
overflow: auto;
|
|
9
|
+
max-block-size: var(--lyra-table-max-height, none);
|
|
10
|
+
border: 1px solid var(--lyra-color-border);
|
|
11
|
+
border-radius: var(--lyra-radius);
|
|
12
|
+
}
|
|
13
|
+
[part='table'] {
|
|
14
|
+
inline-size: 100%;
|
|
15
|
+
border-collapse: collapse;
|
|
16
|
+
font-size: 0.875rem;
|
|
17
|
+
}
|
|
18
|
+
[part='header-cell'] {
|
|
19
|
+
position: sticky;
|
|
20
|
+
inset-block-start: 0;
|
|
21
|
+
background: var(--lyra-color-surface);
|
|
22
|
+
text-align: start;
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
padding: var(--lyra-space-s);
|
|
25
|
+
border-block-end: 1px solid var(--lyra-color-border);
|
|
26
|
+
cursor: default;
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
}
|
|
29
|
+
[part='header-cell'][aria-sort]:not([aria-sort='none']),
|
|
30
|
+
[part='header-cell'][data-sortable] {
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
}
|
|
33
|
+
[part='header-cell'][data-sortable]:hover {
|
|
34
|
+
background: var(--lyra-color-brand-quiet);
|
|
35
|
+
}
|
|
36
|
+
[part='header-cell'][data-align='end'] {
|
|
37
|
+
text-align: end;
|
|
38
|
+
}
|
|
39
|
+
[part='row']:hover {
|
|
40
|
+
background: var(--lyra-color-brand-quiet);
|
|
41
|
+
}
|
|
42
|
+
[part='row'][aria-selected='true'] {
|
|
43
|
+
background: var(--lyra-color-brand-quiet);
|
|
44
|
+
}
|
|
45
|
+
[part='cell'] {
|
|
46
|
+
padding: var(--lyra-space-s);
|
|
47
|
+
border-block-end: 1px solid var(--lyra-color-border);
|
|
48
|
+
}
|
|
49
|
+
[part='cell'][data-align='end'] {
|
|
50
|
+
text-align: end;
|
|
51
|
+
}
|
|
52
|
+
[part='more-button'] {
|
|
53
|
+
display: block;
|
|
54
|
+
inline-size: 100%;
|
|
55
|
+
padding: var(--lyra-space-s);
|
|
56
|
+
border: none;
|
|
57
|
+
background: none;
|
|
58
|
+
color: var(--lyra-color-brand);
|
|
59
|
+
font: inherit;
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
border-block-start: 1px solid var(--lyra-color-border);
|
|
62
|
+
}
|
|
63
|
+
[part='more-button']:hover {
|
|
64
|
+
background: var(--lyra-color-brand-quiet);
|
|
65
|
+
}
|
|
66
|
+
`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aceshooting/lyra-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Free, clean-room Lit web components — a companion to Web Awesome.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"./dist/lyra.js"
|
|
34
34
|
],
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"
|
|
37
|
-
"
|
|
36
|
+
"@floating-ui/dom": "^1.7.6",
|
|
37
|
+
"lit": "^3.3.3"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"@aceshooting/lyra-flags": "^0.1.0"
|
|
@@ -45,13 +45,13 @@
|
|
|
45
45
|
}
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"
|
|
49
|
-
"@
|
|
50
|
-
"@web/test-runner-playwright": "^0.11.0",
|
|
51
|
-
"@web/dev-server-esbuild": "^1.0.2",
|
|
48
|
+
"@aceshooting/lyra-flags": "^0.1.0",
|
|
49
|
+
"@custom-elements-manifest/analyzer": "^0.11.0",
|
|
52
50
|
"@open-wc/testing": "^4.0.0",
|
|
53
|
-
"@
|
|
54
|
-
"@
|
|
51
|
+
"@web/dev-server-esbuild": "^2.0.0",
|
|
52
|
+
"@web/test-runner": "^1.0.0",
|
|
53
|
+
"@web/test-runner-playwright": "^1.0.0",
|
|
54
|
+
"typescript": "^6.0.3"
|
|
55
55
|
},
|
|
56
56
|
"customElements": "custom-elements.json",
|
|
57
57
|
"scripts": {
|