From e6198508e7f2ac829017653f500da9b17b08bc80 Mon Sep 17 00:00:00 2001 From: CodingPhoenixx Date: Fri, 13 Feb 2026 17:36:02 +0100 Subject: [PATCH] Fixed some small bugs --- flightscore/src/components/circle-chart.ts | 214 ++++++++++++++ flightscore/src/components/line-chart.ts | 272 ++++++++++++++++++ flightscore/src/components/loading-bar.ts | 139 +++++++++ flightscore/src/components/ui-badge.ts | 71 +++-- .../src/components/ui-button-secondary.ts | 31 +- flightscore/src/components/ui-button.ts | 29 +- flightscore/src/pages/dev-page.ts | 99 +++++++ 7 files changed, 811 insertions(+), 44 deletions(-) create mode 100644 flightscore/src/components/circle-chart.ts create mode 100644 flightscore/src/components/line-chart.ts create mode 100644 flightscore/src/components/loading-bar.ts diff --git a/flightscore/src/components/circle-chart.ts b/flightscore/src/components/circle-chart.ts new file mode 100644 index 0000000..527daa9 --- /dev/null +++ b/flightscore/src/components/circle-chart.ts @@ -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` + + `; + }); + + return html` +
+ ${this.heading + ? html`

${this.heading}

` + : null} +
+
+ ${arcs} +
+ ${total} + ${this.centerText} +
+
+
+ ${this.segments.map( + (seg, i) => html` +
+ + ${seg.label} + ${seg.value} +
+ ` + )} +
+
+
+ `; + } +} \ No newline at end of file diff --git a/flightscore/src/components/line-chart.ts b/flightscore/src/components/line-chart.ts new file mode 100644 index 0000000..b7286cd --- /dev/null +++ b/flightscore/src/components/line-chart.ts @@ -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` + + ${Math.round(val)} + `); + } + for ( + let val = xScale.min; + val <= xScale.max + xScale.step * 0.01; + val += xScale.step + ) { + const x = sx(val); + gridLines.push(svg` + ${Math.round(val)} + `); + } + + 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`` : null} + + `; + }); + + return html` +
+ ${this.heading || this.subtitle + ? html` +
+ ${this.heading + ? html`

${this.heading}

` + : null} + ${this.subtitle + ? html`

${this.subtitle}

` + : null} +
+ ` + : null} +
+ + ${gridLines} ${lines} + + ${this.xLabel} + + + ${this.yLabel} + + +
+ ${this.series.length > 1 + ? html` +
+ ${this.series.map( + (s, i) => html` +
+ + ${s.label} +
+ ` + )} +
+ ` + : null} +
+ `; + } +} \ No newline at end of file diff --git a/flightscore/src/components/loading-bar.ts b/flightscore/src/components/loading-bar.ts new file mode 100644 index 0000000..8c3dfda --- /dev/null +++ b/flightscore/src/components/loading-bar.ts @@ -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` +
+ ${this.label || !this.hideValue + ? html` +
+ ${this.label + ? html`${this.label}` + : html``} + ${!this.hideValue && !this.indeterminate + ? html`${clamped}%` + : null} +
+ ` + : null} +
+
+
+
+ `; + } +} \ No newline at end of file diff --git a/flightscore/src/components/ui-badge.ts b/flightscore/src/components/ui-badge.ts index a6ab879..f2e9d38 100644 --- a/flightscore/src/components/ui-badge.ts +++ b/flightscore/src/components/ui-badge.ts @@ -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'; @@ -11,30 +11,37 @@ export class UiBadge extends LitElement { display: inline-flex; } - .badge { - display: inline-flex; - align-items: center; - gap: 0.4rem; - padding: 0.3rem 0.75rem; - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0.03em; - text-transform: uppercase; - border-radius: 2rem; - border: 1px solid transparent; - white-space: nowrap; - } +.badge { + display: inline-flex; + align-items: center; + gap: 0.4rem; + padding: 0.35rem 0.75rem; + font-size: 0.75rem; + font-weight: 600; + letter-spacing: 0.03em; + text-transform: uppercase; + border-radius: 2rem; + border: 1px solid transparent; + white-space: nowrap; + line-height: 1; +} - .icon { - display: inline-flex; - align-items: center; - } +.icon { + display: inline-flex; + align-items: center; + flex-shrink: 0; +} - .icon ::slotted(svg) { - width: 0.85rem; - height: 0.85rem; - flex-shrink: 0; - } +.icon ::slotted(svg) { + width: 0.85rem; + height: 0.85rem; +} + +.label { + display: inline-flex; + align-items: center; + padding-top: 0.05em; +} .badge.accent { color: var(--color-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` - - + + ${this.hasIcon + ? html` + + ` + : html``} + - `; + + `; } } \ No newline at end of file diff --git a/flightscore/src/components/ui-button-secondary.ts b/flightscore/src/components/ui-button-secondary.ts index 894a442..c543156 100644 --- a/flightscore/src/components/ui-button-secondary.ts +++ b/flightscore/src/components/ui-button-secondary.ts @@ -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 { @@ -51,11 +51,15 @@ export class UiButtonSecondary extends LitElement { opacity: 0.55; cursor: not-allowed; } + +.icon { + display: inline-flex; + align-items: center; +} - .icon { - display: inline-flex; - align-items: center; - } +.icon:not(:has(*)) { + display: none; +} .icon ::slotted(svg) { width: 1rem; @@ -67,12 +71,21 @@ 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` - + `; } } \ No newline at end of file diff --git a/flightscore/src/components/ui-button.ts b/flightscore/src/components/ui-button.ts index bb6a255..919b08f 100644 --- a/flightscore/src/components/ui-button.ts +++ b/flightscore/src/components/ui-button.ts @@ -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,11 +52,14 @@ export class UiButton extends LitElement { opacity: 0.55; cursor: not-allowed; } +.icon { + display: inline-flex; + align-items: center; +} - .icon { - display: inline-flex; - align-items: center; - } +.icon:not(:has(*)) { + display: none; +} .icon ::slotted(svg) { width: 1rem; @@ -68,12 +71,20 @@ 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` - + `; } } \ No newline at end of file diff --git a/flightscore/src/pages/dev-page.ts b/flightscore/src/pages/dev-page.ts index 7fff3cf..30927c8 100644 --- a/flightscore/src/pages/dev-page.ts +++ b/flightscore/src/pages/dev-page.ts @@ -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 { + +
+
+

Loading Bars

+

Progress indicators with variants and states

+
+
+
Variants
+
+ + + + +
+
Sizes
+
+ + + +
+
Indeterminate
+
+ +
+
+
+ + +
+
+

Line Chart

+

Trend lines with optional area fill

+
+
+
Multi-Series
+
+ +
+
+
+ + +
+
+

Circle Chart

+

Donut chart for proportional data

+
+
+
Example
+
+ +
+
+
`; }