Fixed some small bugs
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
import { LitElement, html, css, svg } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
export interface CircleSegment {
|
||||
label: string;
|
||||
value: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
@customElement('circle-chart')
|
||||
export class CircleChart extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
background: var(--color-bg-nav);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
transition: border-color 0.25s ease;
|
||||
}
|
||||
|
||||
.chart-wrapper:hover {
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-accent) 40%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chart-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.svg-container {
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.center-label {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.center-value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.center-text {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
color: color-mix(in srgb, var(--color-text) 45%, transparent);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
svg {
|
||||
transform: rotate(-90deg);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
circle {
|
||||
transition:
|
||||
stroke-dasharray 0.6s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
opacity 0.25s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
circle:hover {
|
||||
opacity: 0.8;
|
||||
filter: brightness(1.15);
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.legend-dot {
|
||||
width: 0.55rem;
|
||||
height: 0.55rem;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 0.82rem;
|
||||
color: color-mix(in srgb, var(--color-text) 65%, transparent);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.legend-value {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
`;
|
||||
|
||||
@property() heading = '';
|
||||
@property() centerText = 'Total';
|
||||
@property({ type: Array }) segments: CircleSegment[] = [];
|
||||
|
||||
private defaultColors = [
|
||||
'var(--color-accent)',
|
||||
'#30a46c',
|
||||
'#e79d13',
|
||||
'#e5484d',
|
||||
'#6e56cf',
|
||||
'#0091ff',
|
||||
];
|
||||
|
||||
render() {
|
||||
const total = this.segments.reduce((s, d) => s + d.value, 0);
|
||||
const radius = 62;
|
||||
const circumference = 2 * Math.PI * radius;
|
||||
const gapAngle = 0.02;
|
||||
const totalGap = gapAngle * this.segments.length;
|
||||
const usable = Math.max(0, circumference * (1 - totalGap));
|
||||
const gapSize = circumference * gapAngle;
|
||||
let offset = 0;
|
||||
|
||||
const arcs = this.segments.map((seg, i) => {
|
||||
const pct = total > 0 ? seg.value / total : 0;
|
||||
const dash = pct * usable;
|
||||
const gap = circumference - dash;
|
||||
const color =
|
||||
seg.color || this.defaultColors[i % this.defaultColors.length];
|
||||
const currentOffset = offset;
|
||||
offset += dash + gapSize;
|
||||
|
||||
return svg`
|
||||
<circle
|
||||
cx="80" cy="80" r="${radius}"
|
||||
fill="none"
|
||||
stroke="${color}"
|
||||
stroke-width="16"
|
||||
stroke-dasharray="${dash} ${gap}"
|
||||
stroke-dashoffset="${-currentOffset}"
|
||||
/>
|
||||
`;
|
||||
});
|
||||
|
||||
return html`
|
||||
<div class="chart-wrapper">
|
||||
${this.heading
|
||||
? html`<h3 class="title">${this.heading}</h3>`
|
||||
: null}
|
||||
<div class="chart-body">
|
||||
<div class="svg-container">
|
||||
<svg viewBox="0 0 160 160">${arcs}</svg>
|
||||
<div class="center-label">
|
||||
<span class="center-value">${total}</span>
|
||||
<span class="center-text">${this.centerText}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend">
|
||||
${this.segments.map(
|
||||
(seg, i) => html`
|
||||
<div class="legend-item">
|
||||
<span
|
||||
class="legend-dot"
|
||||
style="background: ${seg.color ||
|
||||
this.defaultColors[
|
||||
i % this.defaultColors.length
|
||||
]}"
|
||||
></span>
|
||||
<span class="legend-label">${seg.label}</span>
|
||||
<span class="legend-value">${seg.value}</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
import { LitElement, html, css, svg } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
export interface LineSeries {
|
||||
label: string;
|
||||
color?: string;
|
||||
points: { x: number; y: number }[];
|
||||
}
|
||||
|
||||
@customElement('line-chart')
|
||||
export class LineChart extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.chart-wrapper {
|
||||
background: var(--color-bg-nav);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.25rem;
|
||||
transition: border-color 0.25s ease;
|
||||
}
|
||||
|
||||
.chart-wrapper:hover {
|
||||
border-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-accent) 40%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1rem;
|
||||
font-weight: 650;
|
||||
color: var(--color-text);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.78rem;
|
||||
color: color-mix(in srgb, var(--color-text) 45%, transparent);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.svg-container {
|
||||
width: 100%;
|
||||
overflow: visible;
|
||||
aspect-ratio: 500 / 300;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
display: block;
|
||||
}
|
||||
.grid-line {
|
||||
stroke: var(--color-border);
|
||||
stroke-width: 0.5;
|
||||
}
|
||||
|
||||
.axis-label {
|
||||
font-size: 9px;
|
||||
fill: color-mix(in srgb, var(--color-text) 40%, transparent);
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.line-path {
|
||||
fill: none;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.line-path:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.area-path {
|
||||
opacity: 0.08;
|
||||
transition: opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.legend-line {
|
||||
width: 1rem;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.legend-label {
|
||||
font-size: 0.78rem;
|
||||
color: color-mix(in srgb, var(--color-text) 60%, transparent);
|
||||
font-weight: 500;
|
||||
}
|
||||
`;
|
||||
|
||||
@property() heading = '';
|
||||
@property() subtitle = '';
|
||||
@property() xLabel = 'X';
|
||||
@property() yLabel = 'Y';
|
||||
@property({ type: Array }) series: LineSeries[] = [];
|
||||
@property({ type: Boolean }) showArea = false;
|
||||
|
||||
private defaultColors = [
|
||||
'var(--color-accent)',
|
||||
'#30a46c',
|
||||
'#e79d13',
|
||||
'#e5484d',
|
||||
'#6e56cf',
|
||||
'#0091ff',
|
||||
];
|
||||
|
||||
private padding = { top: 20, right: 25, bottom: 40, left: 45 };
|
||||
private width = 500;
|
||||
private height = 300;
|
||||
|
||||
private niceScale(min: number, max: number, ticks: number) {
|
||||
const range = max - min || 1;
|
||||
const rough = range / ticks;
|
||||
const mag = Math.pow(10, Math.floor(Math.log10(rough)));
|
||||
const norm = rough / mag;
|
||||
let step: number;
|
||||
if (norm <= 1.5) step = 1 * mag;
|
||||
else if (norm <= 3) step = 2 * mag;
|
||||
else if (norm <= 7) step = 5 * mag;
|
||||
else step = 10 * mag;
|
||||
const nMin = Math.floor(min / step) * step;
|
||||
const nMax = Math.ceil(max / step) * step;
|
||||
return { min: nMin, max: nMax, step };
|
||||
}
|
||||
|
||||
render() {
|
||||
const p = this.padding;
|
||||
const w = this.width - p.left - p.right;
|
||||
const h = this.height - p.top - p.bottom;
|
||||
|
||||
const allPts = this.series.flatMap((s) => s.points);
|
||||
const xs = allPts.map((d) => d.x);
|
||||
const ys = allPts.map((d) => d.y);
|
||||
const rawXMin = Math.min(...xs, 0);
|
||||
const rawXMax = Math.max(...xs, 1);
|
||||
const rawYMin = Math.min(...ys, 0);
|
||||
const rawYMax = Math.max(...ys, 1);
|
||||
|
||||
const xScale = this.niceScale(rawXMin, rawXMax, 5);
|
||||
const yScale = this.niceScale(rawYMin, rawYMax, 5);
|
||||
|
||||
const sx = (v: number) =>
|
||||
p.left + ((v - xScale.min) / (xScale.max - xScale.min)) * w;
|
||||
const sy = (v: number) =>
|
||||
p.top +
|
||||
h -
|
||||
((v - yScale.min) / (yScale.max - yScale.min)) * h;
|
||||
|
||||
const gridLines = [];
|
||||
for (
|
||||
let val = yScale.min;
|
||||
val <= yScale.max + yScale.step * 0.01;
|
||||
val += yScale.step
|
||||
) {
|
||||
const y = sy(val);
|
||||
gridLines.push(svg`
|
||||
<line class="grid-line" x1="${p.left}" y1="${y}" x2="${this.width - p.right}" y2="${y}" />
|
||||
<text class="axis-label" x="${p.left - 8}" y="${y + 3}" text-anchor="end">${Math.round(val)}</text>
|
||||
`);
|
||||
}
|
||||
for (
|
||||
let val = xScale.min;
|
||||
val <= xScale.max + xScale.step * 0.01;
|
||||
val += xScale.step
|
||||
) {
|
||||
const x = sx(val);
|
||||
gridLines.push(svg`
|
||||
<text class="axis-label" x="${x}" y="${this.height - p.bottom + 18}" text-anchor="middle">${Math.round(val)}</text>
|
||||
`);
|
||||
}
|
||||
|
||||
const baseLine = sy(yScale.min);
|
||||
|
||||
const lines = this.series.map((s, i) => {
|
||||
const color =
|
||||
s.color || this.defaultColors[i % this.defaultColors.length];
|
||||
const sorted = [...s.points].sort((a, b) => a.x - b.x);
|
||||
if (!sorted.length) return svg``;
|
||||
|
||||
const d = sorted
|
||||
.map((pt, j) => `${j === 0 ? 'M' : 'L'}${sx(pt.x)},${sy(pt.y)}`)
|
||||
.join(' ');
|
||||
|
||||
const areaD =
|
||||
`M${sx(sorted[0].x)},${baseLine} ` +
|
||||
sorted.map((pt) => `L${sx(pt.x)},${sy(pt.y)}`).join(' ') +
|
||||
` L${sx(sorted[sorted.length - 1].x)},${baseLine} Z`;
|
||||
|
||||
return svg`
|
||||
${this.showArea ? svg`<path class="area-path" d="${areaD}" fill="${color}" />` : null}
|
||||
<path class="line-path" d="${d}" stroke="${color}" />
|
||||
`;
|
||||
});
|
||||
|
||||
return html`
|
||||
<div class="chart-wrapper">
|
||||
${this.heading || this.subtitle
|
||||
? html`
|
||||
<div class="header">
|
||||
${this.heading
|
||||
? html`<h3 class="title">${this.heading}</h3>`
|
||||
: null}
|
||||
${this.subtitle
|
||||
? html`<p class="subtitle">${this.subtitle}</p>`
|
||||
: null}
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
<div class="svg-container">
|
||||
<svg viewBox="0 0 ${this.width} ${this.height}">
|
||||
${gridLines} ${lines}
|
||||
<text class="axis-label" x="${this.width / 2}" y="${this.height - 4}" text-anchor="middle">
|
||||
${this.xLabel}
|
||||
</text>
|
||||
<text class="axis-label" x="12" y="${this.height / 2}" text-anchor="middle"
|
||||
transform="rotate(-90, 12, ${this.height / 2})">
|
||||
${this.yLabel}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
${this.series.length > 1
|
||||
? html`
|
||||
<div class="legend">
|
||||
${this.series.map(
|
||||
(s, i) => html`
|
||||
<div class="legend-item">
|
||||
<span class="legend-line" style="background: ${s.color ||
|
||||
this.defaultColors[
|
||||
i % this.defaultColors.length
|
||||
]}"></span>
|
||||
<span class="legend-label">${s.label}</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
// components/loading-bar.ts
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
@customElement('loading-bar')
|
||||
export class LoadingBar extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: color-mix(in srgb, var(--color-text) 70%, transparent);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.track {
|
||||
width: 100%;
|
||||
height: 0.5rem;
|
||||
background: color-mix(in srgb, var(--color-text) 8%, transparent);
|
||||
border-radius: 1rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([size='sm']) .track {
|
||||
height: 0.3rem;
|
||||
}
|
||||
|
||||
:host([size='lg']) .track {
|
||||
height: 0.75rem;
|
||||
}
|
||||
|
||||
.fill {
|
||||
height: 100%;
|
||||
border-radius: 1rem;
|
||||
background: var(--color-accent);
|
||||
transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fill.success {
|
||||
background: #30a46c;
|
||||
}
|
||||
|
||||
.fill.warning {
|
||||
background: #e79d13;
|
||||
}
|
||||
|
||||
.fill.error {
|
||||
background: #e5484d;
|
||||
}
|
||||
|
||||
:host([indeterminate]) .fill {
|
||||
width: 40% !important;
|
||||
animation: indeterminate 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes indeterminate {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(350%);
|
||||
}
|
||||
}
|
||||
|
||||
.fill::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.15) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
border-radius: inherit;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ type: Number }) value = 0;
|
||||
@property() label = '';
|
||||
@property() variant: 'accent' | 'success' | 'warning' | 'error' =
|
||||
'accent';
|
||||
@property({ type: Boolean, reflect: true }) indeterminate = false;
|
||||
@property({ type: Boolean }) hideValue = false;
|
||||
@property({ reflect: true }) size: 'sm' | 'md' | 'lg' = 'md';
|
||||
|
||||
render() {
|
||||
const clamped = Math.max(0, Math.min(100, this.value));
|
||||
|
||||
return html`
|
||||
<div class="wrapper">
|
||||
${this.label || !this.hideValue
|
||||
? html`
|
||||
<div class="meta">
|
||||
${this.label
|
||||
? html`<span class="label">${this.label}</span>`
|
||||
: html`<span></span>`}
|
||||
${!this.hideValue && !this.indeterminate
|
||||
? html`<span class="value">${clamped}%</span>`
|
||||
: null}
|
||||
</div>
|
||||
`
|
||||
: null}
|
||||
<div class="track">
|
||||
<div
|
||||
class="fill ${this.variant}"
|
||||
style="width: ${this.indeterminate ? 40 : clamped}%"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// components/ui-badge.ts
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
export type BadgeVariant = 'accent' | 'success' | 'warning' | 'error' | 'muted';
|
||||
|
||||
@@ -15,7 +15,7 @@ export class UiBadge extends LitElement {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.3rem 0.75rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.03em;
|
||||
@@ -23,17 +23,24 @@ export class UiBadge extends LitElement {
|
||||
border-radius: 2rem;
|
||||
border: 1px solid transparent;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon ::slotted(svg) {
|
||||
width: 0.85rem;
|
||||
height: 0.85rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-top: 0.05em;
|
||||
}
|
||||
|
||||
.badge.accent {
|
||||
@@ -68,13 +75,25 @@ export class UiBadge extends LitElement {
|
||||
`;
|
||||
|
||||
@property() variant: BadgeVariant = 'accent';
|
||||
@state() private hasIcon = false;
|
||||
|
||||
private handleSlotChange(e: Event) {
|
||||
const slot = e.target as HTMLSlotElement;
|
||||
this.hasIcon = slot.assignedNodes().length > 0;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<span class="badge ${this.variant}">
|
||||
<span class="icon"><slot name="icon"></slot></span>
|
||||
${this.hasIcon
|
||||
? html`<span class="icon">
|
||||
<slot name="icon" @slotchange=${this.handleSlotChange}></slot>
|
||||
</span>`
|
||||
: html`<slot name="icon" @slotchange=${this.handleSlotChange} style="display:none"></slot>`}
|
||||
<span class="label">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
// components/ui-button-secondary.ts
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('ui-button-secondary')
|
||||
export class UiButtonSecondary extends LitElement {
|
||||
@@ -57,6 +57,10 @@ export class UiButtonSecondary extends LitElement {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon:not(:has(*)) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon ::slotted(svg) {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
@@ -67,10 +71,19 @@ export class UiButtonSecondary extends LitElement {
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
@property({ type: Boolean, reflect: true }) full = false;
|
||||
|
||||
@state() private hasIcon = false;
|
||||
|
||||
private handleSlotChange(e: Event) {
|
||||
const slot = e.target as HTMLSlotElement;
|
||||
this.hasIcon = slot.assignedNodes().length > 0;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<button ?disabled=${this.disabled}>
|
||||
<span class="icon"><slot name="icon"></slot></span>
|
||||
${this.hasIcon ? html`<span class="icon">
|
||||
<slot name="icon" @slotchange=${this.handleSlotChange}></slot>
|
||||
</span>` : html`<slot name="icon" @slotchange=${this.handleSlotChange} style="display:none"></slot>`}
|
||||
<slot></slot>
|
||||
</button>
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// components/ui-button.ts
|
||||
import { LitElement, html, css } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('ui-button')
|
||||
export class UiButton extends LitElement {
|
||||
@@ -52,12 +52,15 @@ export class UiButton extends LitElement {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.icon:not(:has(*)) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.icon ::slotted(svg) {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
@@ -68,10 +71,18 @@ export class UiButton extends LitElement {
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
@property({ type: Boolean, reflect: true }) full = false;
|
||||
|
||||
@state() private hasIcon = false;
|
||||
|
||||
private handleSlotChange(e: Event) {
|
||||
const slot = e.target as HTMLSlotElement;
|
||||
this.hasIcon = slot.assignedNodes().length > 0;
|
||||
}
|
||||
render() {
|
||||
return html`
|
||||
<button ?disabled=${this.disabled}>
|
||||
<span class="icon"><slot name="icon"></slot></span>
|
||||
${this.hasIcon ? html`<span class="icon">
|
||||
<slot name="icon" @slotchange=${this.handleSlotChange}></slot>
|
||||
</span>` : html`<slot name="icon" @slotchange=${this.handleSlotChange} style="display:none"></slot>`}
|
||||
<slot></slot>
|
||||
</button>
|
||||
`;
|
||||
|
||||
@@ -13,6 +13,9 @@ import '../components/form-input';
|
||||
import '../components/horizontal-divider';
|
||||
import '../components/notify-bar';
|
||||
import '../components/ui-link';
|
||||
import '../components/circle-chart';
|
||||
import '../components/loading-bar';
|
||||
import '../components/line-chart';
|
||||
|
||||
@customElement('dev-page')
|
||||
export class DevPage extends LitElement {
|
||||
@@ -571,6 +574,102 @@ export class DevPage extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<h2>Loading Bars</h2>
|
||||
<p>Progress indicators with variants and states</p>
|
||||
</div>
|
||||
<div class="component-group">
|
||||
<div class="component-label">Variants</div>
|
||||
<div class="component-preview col">
|
||||
<loading-bar label="Accent" value="72"></loading-bar>
|
||||
<loading-bar label="Success" value="100" variant="success"></loading-bar>
|
||||
<loading-bar label="Warning" value="45" variant="warning"></loading-bar>
|
||||
<loading-bar label="Error" value="18" variant="error"></loading-bar>
|
||||
</div>
|
||||
<div class="component-label">Sizes</div>
|
||||
<div class="component-preview col">
|
||||
<loading-bar label="Small" value="60" size="sm"></loading-bar>
|
||||
<loading-bar label="Medium" value="60" size="md"></loading-bar>
|
||||
<loading-bar label="Large" value="60" size="lg"></loading-bar>
|
||||
</div>
|
||||
<div class="component-label">Indeterminate</div>
|
||||
<div class="component-preview col">
|
||||
<loading-bar label="Loading…" indeterminate hideValue></loading-bar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<h2>Line Chart</h2>
|
||||
<p>Trend lines with optional area fill</p>
|
||||
</div>
|
||||
<div class="component-group">
|
||||
<div class="component-label">Multi-Series</div>
|
||||
<div class="component-preview col">
|
||||
<line-chart
|
||||
heading="Score Progression"
|
||||
subtitle="Last 6 Rounds"
|
||||
xLabel="Round"
|
||||
yLabel="Score"
|
||||
showArea
|
||||
.series=${[
|
||||
{
|
||||
label: 'Pilot A',
|
||||
color: 'var(--color-accent)',
|
||||
points: [
|
||||
{ x: 0, y: 420 },
|
||||
{ x: 1, y: 420 },
|
||||
{ x: 2, y: 580 },
|
||||
{ x: 3, y: 540 },
|
||||
{ x: 4, y: 710 },
|
||||
{ x: 5, y: 690 },
|
||||
{ x: 6, y: 820 },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Pilot B',
|
||||
color: '#30a46c',
|
||||
points: [
|
||||
{ x: 1, y: 380 },
|
||||
{ x: 2, y: 450 },
|
||||
{ x: 3, y: 620 },
|
||||
{ x: 4, y: 590 },
|
||||
{ x: 5, y: 750 },
|
||||
{ x: 6, y: 780 },
|
||||
],
|
||||
},
|
||||
]}
|
||||
></line-chart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="section">
|
||||
<div class="section-head">
|
||||
<h2>Circle Chart</h2>
|
||||
<p>Donut chart for proportional data</p>
|
||||
</div>
|
||||
<div class="component-group">
|
||||
<div class="component-label">Example</div>
|
||||
<div class="component-preview col">
|
||||
<circle-chart
|
||||
heading="Pilots by Category"
|
||||
centerText="Pilots"
|
||||
.segments=${[
|
||||
{ label: 'Sport', value: 124 },
|
||||
{ label: 'Serial', value: 89 },
|
||||
{ label: 'Open', value: 47 },
|
||||
{ label: 'Tandem', value: 18 },
|
||||
]}
|
||||
></circle-chart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user