🎨 ✨ 🚀 Overhaul UI/UX with comprehensive design system improvements
✨ Features added: - 🔍 Implement functional search in header navigation - ⚙️ Add basic user settings page - 📱 Make dashboard fully responsive 🔧 Enhancements: - 🎭 Standardize CSS with consistent theming across components - 🧹 Remove unused CSS for better performance - 📊 Improve dashboard layout and visual hierarchy - 📦 Redesign last block widget for better usability 💅 This commit introduces a cohesive design system with functional design-token components for a more ✨ polished user experience.
This commit is contained in:
parent
5afeb4d01a
commit
dc9abee715
49 changed files with 4176 additions and 2468 deletions
257
src/components/General/Button.ts
Normal file
257
src/components/General/Button.ts
Normal file
|
@ -0,0 +1,257 @@
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
@customElement('arx-button')
|
||||
export class StyledButton extends LitElement {
|
||||
@property() label = '';
|
||||
@property() type: 'button' | 'submit' | 'reset' | 'menu' = 'button';
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
@property() name = '';
|
||||
@property() value = '';
|
||||
@property() variant: 'default' | 'primary' | 'secondary' | 'accent' = 'default';
|
||||
@property({ type: Boolean }) loading = false;
|
||||
@property({ type: Boolean }) fullWidth = false;
|
||||
@property({ type: Boolean, reflect: true }) pressed = false;
|
||||
@property({ type: String }) href = '';
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
:host([fullWidth]) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button {
|
||||
--bg: var(--color-base-100);
|
||||
--text: var(--color-base-content);
|
||||
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
min-height: 46px;
|
||||
padding: 10px 24px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
border: var(--border) solid var(--color-base-300);
|
||||
border-radius: var(--radius-selector);
|
||||
cursor: pointer;
|
||||
transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
box-shadow: calc(var(--depth) * 3px) calc(var(--depth) * 3px)
|
||||
calc(var(--depth) * 6px)
|
||||
oklch(from var(--color-base-content) l c h / 0.15),
|
||||
calc(var(--depth) * -3px) calc(var(--depth) * -3px)
|
||||
calc(var(--depth) * 6px) oklch(from var(--color-base-100) l c h / 0.7);
|
||||
overflow: hidden;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:host([variant="primary"]) button {
|
||||
--bg: var(--color-primary);
|
||||
--text: var(--color-primary-content);
|
||||
}
|
||||
|
||||
:host([variant="secondary"]) button {
|
||||
--bg: var(--color-secondary);
|
||||
--text: var(--color-secondary-content);
|
||||
}
|
||||
|
||||
:host([variant="accent"]) button {
|
||||
--bg: var(--color-accent);
|
||||
--text: var(--color-accent-content);
|
||||
}
|
||||
|
||||
:host([fullWidth]) button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
button:hover,
|
||||
button:focus-visible {
|
||||
transform: translateY(-2px);
|
||||
background: oklch(from var(--bg) calc(l + 0.02) c h);
|
||||
box-shadow: calc(var(--depth) * 4px) calc(var(--depth) * 4px)
|
||||
calc(var(--depth) * 8px)
|
||||
oklch(from var(--color-base-content) l c h / 0.2),
|
||||
calc(var(--depth) * -4px) calc(var(--depth) * -4px)
|
||||
calc(var(--depth) * 8px) oklch(from var(--color-base-100) l c h / 0.8);
|
||||
}
|
||||
|
||||
button:active,
|
||||
:host([pressed]) button {
|
||||
transform: translateY(1px);
|
||||
border-top: var(--border) solid
|
||||
oklch(from var(--color-base-300) l c h / 0.8);
|
||||
border-left: var(--border) solid
|
||||
oklch(from var(--color-base-300) l c h / 0.8);
|
||||
border-bottom: var(--border) solid
|
||||
oklch(from var(--color-base-200) l c h / 0.8);
|
||||
border-right: var(--border) solid
|
||||
oklch(from var(--color-base-200) l c h / 0.8);
|
||||
box-shadow: inset calc(var(--depth) * 3px) calc(var(--depth) * 3px)
|
||||
calc(var(--depth) * 5px)
|
||||
oklch(from var(--color-base-content) l c h / 0.2),
|
||||
inset calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 3px) oklch(from var(--color-base-100) l c h / 0.5);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
oklch(from var(--bg) calc(l - 0.03) c h),
|
||||
oklch(from var(--bg) l c h)
|
||||
);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: calc(var(--depth) * 1px) calc(var(--depth) * 1px)
|
||||
calc(var(--depth) * 3px)
|
||||
oklch(from var(--color-base-content) l c h / 0.1),
|
||||
calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 3px) oklch(from var(--color-base-100) l c h / 0.4);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: none;
|
||||
position: relative;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
:host([loading]) .spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
:host([loading]) .label {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.spinner::before {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid transparent;
|
||||
border-top-color: currentColor;
|
||||
border-right-color: currentColor;
|
||||
animation: spinner 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spinner {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
::slotted([slot="prefix"]) {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
::slotted([slot="suffix"]) {
|
||||
margin-left: 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<button
|
||||
type=${this.type}
|
||||
?disabled=${this.disabled || this.loading}
|
||||
name=${this.name}
|
||||
value=${this.value}
|
||||
@click=${this._handleClick}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@focus=${this._handleFocus}
|
||||
@blur=${this._handleBlur}
|
||||
>
|
||||
<slot name="prefix"></slot>
|
||||
<div class="spinner"></div>
|
||||
<slot name="label">
|
||||
<span class="label">${this.label}</span>
|
||||
</slot>
|
||||
<slot name="suffix"></slot>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleClick(e: MouseEvent) {
|
||||
if (this.disabled || this.loading) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.href) {
|
||||
let href = this.href;
|
||||
if (href.startsWith('javascript:')) return href;
|
||||
if (href.startsWith('eve://')) href = href.replace('eve://', '#');
|
||||
if (href.startsWith('/')) href = href.replace('/', '#');
|
||||
if (!href.startsWith('#')) href = `#${href}`;
|
||||
window.location.href = href;
|
||||
return;
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('click', {
|
||||
detail: {
|
||||
name: this.name,
|
||||
value: this.value,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleKeyDown(e: KeyboardEvent) {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && !this.disabled && !this.loading) {
|
||||
e.preventDefault();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('click', {
|
||||
detail: {
|
||||
name: this.name,
|
||||
value: this.value,
|
||||
},
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('keydown', {
|
||||
detail: { key: e.key },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleFocus() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('focus', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleBlur() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('blur', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
35
src/components/General/Card.ts
Normal file
35
src/components/General/Card.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement } from 'lit/decorators.js';
|
||||
|
||||
@customElement('arx-card')
|
||||
export class StyledFieldset extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
background: var(--color-base-200);
|
||||
border-radius: var(--radius-box);
|
||||
box-shadow: calc(var(--depth) * 4px) calc(var(--depth) * 4px)
|
||||
calc(var(--depth) * 8px)
|
||||
oklch(from var(--color-base-content) l c h / 0.1),
|
||||
calc(var(--depth) * -2px) calc(var(--depth) * -2px)
|
||||
calc(var(--depth) * 6px) oklch(from var(--color-base-100) l c h / 0.6);
|
||||
padding: 32px;
|
||||
margin-bottom: 32px;
|
||||
border: var(--border) solid var(--color-base-300);
|
||||
transition: all 0.3s ease;
|
||||
&:hover {
|
||||
box-shadow: calc(var(--depth) * 6px) calc(var(--depth) * 6px)
|
||||
calc(var(--depth) * 12px)
|
||||
oklch(from var(--color-base-content) l c h / 0.15),
|
||||
calc(var(--depth) * -3px) calc(var(--depth) * -3px)
|
||||
calc(var(--depth) * 8px)
|
||||
oklch(from var(--color-base-100) l c h / 0.7);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
}
|
122
src/components/General/Fieldset.ts
Normal file
122
src/components/General/Fieldset.ts
Normal file
|
@ -0,0 +1,122 @@
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
|
||||
@customElement('arx-fieldset')
|
||||
export class StyledFieldset extends LitElement {
|
||||
@property() legend = '';
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
@property() variant = 'default'; // default, primary, secondary, accent
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.fieldset-container {
|
||||
background: var(--color-base-100);
|
||||
border: var(--border) solid var(--color-base-300);
|
||||
border-radius: var(--radius-box);
|
||||
padding: 24px;
|
||||
position: relative;
|
||||
box-shadow: calc(var(--depth) * 2px) calc(var(--depth) * 2px)
|
||||
calc(var(--depth) * 4px)
|
||||
oklch(from var(--color-base-content) l c h / 0.1),
|
||||
calc(var(--depth) * -2px) calc(var(--depth) * -2px)
|
||||
calc(var(--depth) * 4px) oklch(from var(--color-base-100) l c h / 0.5);
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
:host([variant="primary"]) .fieldset-container {
|
||||
border-left: calc(var(--border) * 3) solid var(--color-primary);
|
||||
}
|
||||
|
||||
:host([variant="secondary"]) .fieldset-container {
|
||||
border-left: calc(var(--border) * 3) solid var(--color-secondary);
|
||||
}
|
||||
|
||||
:host([variant="accent"]) .fieldset-container {
|
||||
border-left: calc(var(--border) * 3) solid var(--color-accent);
|
||||
}
|
||||
|
||||
.legend {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--color-base-100);
|
||||
border: var(--border) solid var(--color-base-300);
|
||||
padding: 0 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-base-content);
|
||||
border-radius: calc(var(--radius-selector) / 2);
|
||||
transition: all 0.25s ease;
|
||||
}
|
||||
|
||||
:host([variant="primary"]) .legend {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
:host([variant="secondary"]) .legend {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
:host([variant="accent"]) .legend {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.fieldset-content {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
:host([disabled]) .fieldset-container {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
box-shadow: calc(var(--depth) * 1px) calc(var(--depth) * 1px)
|
||||
calc(var(--depth) * 2px)
|
||||
oklch(from var(--color-base-content) l c h / 0.05),
|
||||
calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 2px) oklch(from var(--color-base-100) l c h / 0.3);
|
||||
}
|
||||
|
||||
:host([disabled]) .legend {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
:host(:not([disabled])) .fieldset-container:hover {
|
||||
box-shadow: calc(var(--depth) * 3px) calc(var(--depth) * 3px)
|
||||
calc(var(--depth) * 6px)
|
||||
oklch(from var(--color-base-content) l c h / 0.15),
|
||||
calc(var(--depth) * -3px) calc(var(--depth) * -3px)
|
||||
calc(var(--depth) * 6px) oklch(from var(--color-base-100) l c h / 0.6);
|
||||
}
|
||||
|
||||
:host(:not([disabled])) .fieldset-container:focus-within {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: calc(var(--depth) * 2px) calc(var(--depth) * 2px)
|
||||
calc(var(--depth) * 4px)
|
||||
oklch(from var(--color-base-content) l c h / 0.1),
|
||||
calc(var(--depth) * -2px) calc(var(--depth) * -2px)
|
||||
calc(var(--depth) * 4px) oklch(from var(--color-base-100) l c h / 0.5),
|
||||
0 0 0 2px oklch(from var(--color-accent) l c h / 0.2);
|
||||
}
|
||||
|
||||
:host(:not([disabled])) .fieldset-container:focus-within .legend {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="fieldset-container" ?disabled=${this.disabled}>
|
||||
${when(this.legend, () => html`<div class="legend">${this.legend}</div>`)}
|
||||
<div class="fieldset-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
165
src/components/General/Input.ts
Normal file
165
src/components/General/Input.ts
Normal file
|
@ -0,0 +1,165 @@
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
|
||||
@customElement('arx-input')
|
||||
export class StyledInput extends LitElement {
|
||||
@property() placeholder = '';
|
||||
@property() value = '';
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
@property() type = 'text';
|
||||
@property() name = '';
|
||||
@property({ type: Boolean }) required = false;
|
||||
@property() label = '';
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--color-base-content);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
font-size: 16px;
|
||||
color: var(--color-base-content);
|
||||
background: var(--color-base-100);
|
||||
border: var(--border) solid var(--color-base-300);
|
||||
border-radius: var(--radius-field);
|
||||
box-shadow: inset calc(var(--depth) * 2px) calc(var(--depth) * 2px)
|
||||
calc(var(--depth) * 4px)
|
||||
oklch(from var(--color-base-content) l c h / 0.15),
|
||||
inset calc(var(--depth) * -2px) calc(var(--depth) * -2px)
|
||||
calc(var(--depth) * 4px) oklch(from var(--color-base-100) l c h / 0.7),
|
||||
0 0 0 transparent;
|
||||
transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--color-secondary);
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.1, 0.25, 1.1);
|
||||
}
|
||||
|
||||
input:hover {
|
||||
transform: translateY(-1px);
|
||||
background: oklch(from var(--color-base-100) calc(l + 0.02) c h);
|
||||
}
|
||||
|
||||
input:hover::placeholder {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
transform: translateY(1px);
|
||||
border-top: var(--border) solid
|
||||
oklch(from var(--color-base-300) l c h / 0.8);
|
||||
border-left: var(--border) solid
|
||||
oklch(from var(--color-base-300) l c h / 0.8);
|
||||
border-bottom: var(--border) solid
|
||||
oklch(from var(--color-base-200) l c h / 0.8);
|
||||
border-right: var(--border) solid
|
||||
oklch(from var(--color-base-200) l c h / 0.8);
|
||||
box-shadow: inset calc(var(--depth) * 3px) calc(var(--depth) * 3px)
|
||||
calc(var(--depth) * 5px)
|
||||
oklch(from var(--color-base-content) l c h / 0.2),
|
||||
inset calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 3px) oklch(from var(--color-base-100) l c h / 0.5),
|
||||
0 0 0 transparent;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
oklch(from var(--color-base-100) calc(l - 0.03) c h),
|
||||
oklch(from var(--color-base-100) l c h)
|
||||
);
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
box-shadow: inset calc(var(--depth) * 1px) calc(var(--depth) * 1px)
|
||||
calc(var(--depth) * 2px)
|
||||
oklch(from var(--color-base-content) l c h / 0.05),
|
||||
inset calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 2px) oklch(from var(--color-base-100) l c h / 0.4);
|
||||
transform: none;
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
${when(this.label, () => html`<label for="input-${this.name}">${this.label}</label>`)}
|
||||
<input
|
||||
.value=${this.value}
|
||||
?disabled=${this.disabled}
|
||||
?required=${this.required}
|
||||
placeholder=${this.placeholder}
|
||||
type=${this.type}
|
||||
name=${this.name}
|
||||
@input=${this._handleInput}
|
||||
@focus=${this._handleFocus}
|
||||
@blur=${this._handleBlur}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
/>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleKeyDown(e: KeyboardEvent) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('keydown', {
|
||||
detail: { key: e.key },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleKeyUp(e: KeyboardEvent) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('keyup', {
|
||||
detail: { key: e.key },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleFocus() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('focus', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleBlur() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('blur', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleInput(e: InputEvent) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
this.value = input.value;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('input', {
|
||||
detail: { value: this.value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
206
src/components/General/Prompt.ts
Normal file
206
src/components/General/Prompt.ts
Normal file
|
@ -0,0 +1,206 @@
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
|
||||
import '@components/General/Input';
|
||||
|
||||
@customElement('arx-prompt')
|
||||
export class EvePrompt extends LitElement {
|
||||
@property({ type: String }) promptText = 'Please provide input';
|
||||
@property({ type: String }) cancelText = 'Cancel';
|
||||
@property({ type: String }) saveText = 'Save';
|
||||
@property({ type: Boolean }) open = false;
|
||||
@property({ type: Boolean }) showInput = false;
|
||||
@property({ type: String }) placeholder = 'Enter your response';
|
||||
@property({ type: String }) defaultValue = '';
|
||||
|
||||
@state() private _inputValue = '';
|
||||
private _previousFocus: HTMLElement | null = null;
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: oklch(from var(--color-base-content) l c h / 0.6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.overlay.active {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.prompt-container {
|
||||
background-color: var(--color-base-100);
|
||||
border-radius: var(--radius-box);
|
||||
border: var(--border) solid var(--color-base-300);
|
||||
box-shadow: calc(var(--depth) * 4px) calc(var(--depth) * 4px)
|
||||
calc(var(--depth) * 8px)
|
||||
oklch(from var(--color-base-content) l c h / 0.2),
|
||||
calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 4px) oklch(from var(--color-base-100) l c h / 0.4);
|
||||
width: 90%;
|
||||
max-width: 420px;
|
||||
padding: 28px;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
transition: transform 0.25s cubic-bezier(0.1, 1, 0.2, 1);
|
||||
color: var(--color-base-content);
|
||||
}
|
||||
|
||||
.overlay.active .prompt-container {
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
.prompt-header {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 16px 0;
|
||||
line-height: 1.4;
|
||||
color: var(--color-base-content);
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.prompt-container:focus-within {
|
||||
box-shadow: calc(var(--depth) * 4px) calc(var(--depth) * 4px)
|
||||
calc(var(--depth) * 8px)
|
||||
oklch(from var(--color-base-content) l c h / 0.2),
|
||||
calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 4px) oklch(from var(--color-base-100) l c h / 0.4),
|
||||
0 0 0 2px oklch(from var(--color-accent) l c h / 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._handleKeyDown = this._handleKeyDown.bind(this);
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
document.addEventListener('keydown', this._handleKeyDown);
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
document.removeEventListener('keydown', this._handleKeyDown);
|
||||
}
|
||||
|
||||
override updated(changedProps: Map<string, unknown>) {
|
||||
if (changedProps.has('open') && this.open) {
|
||||
this._inputValue = this.defaultValue;
|
||||
this._previousFocus = document.activeElement as HTMLElement;
|
||||
|
||||
// Focus the input or save button after rendering
|
||||
setTimeout(() => {
|
||||
if (this.showInput) {
|
||||
const input = this.shadowRoot?.querySelector('.input-field') as HTMLElement;
|
||||
if (input) input.focus();
|
||||
} else {
|
||||
const saveBtn = this.shadowRoot?.querySelector('.save-btn') as HTMLElement;
|
||||
if (saveBtn) saveBtn.focus();
|
||||
}
|
||||
}, 50);
|
||||
} else if (changedProps.has('open') && !this.open && this._previousFocus) {
|
||||
this._previousFocus.focus();
|
||||
}
|
||||
}
|
||||
|
||||
private _handleKeyDown(e: KeyboardEvent) {
|
||||
if (!this.open) return;
|
||||
if (e.key === 'Escape') this._handleCancel();
|
||||
if (e.key === 'Enter' && !e.shiftKey) this._handleSave();
|
||||
}
|
||||
|
||||
private _handleInputChange(e: Event) {
|
||||
const target = e.target as HTMLInputElement;
|
||||
this._inputValue = target.value;
|
||||
}
|
||||
|
||||
private _handleCancel() {
|
||||
this.open = false;
|
||||
this.dispatchEvent(new CustomEvent('cancel'));
|
||||
}
|
||||
|
||||
private _handleSave() {
|
||||
this.open = false;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('save', {
|
||||
detail: { value: this._inputValue },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div
|
||||
class="${classMap({ overlay: true, active: this.open })}"
|
||||
@click="${(e: MouseEvent) => e.target === e.currentTarget && this._handleCancel()}"
|
||||
>
|
||||
<div class="prompt-container">
|
||||
<div class="prompt-header">${this.promptText}</div>
|
||||
|
||||
${
|
||||
this.showInput
|
||||
? html`
|
||||
<arx-input
|
||||
type="text"
|
||||
class="input-field"
|
||||
.value=${this._inputValue}
|
||||
@input=${this._handleInputChange}
|
||||
placeholder=${this.placeholder}
|
||||
></arx-input>
|
||||
`
|
||||
: ''
|
||||
}
|
||||
|
||||
<div class="buttons">
|
||||
<arx-button
|
||||
variant="accent"
|
||||
label=${this.cancelText}
|
||||
@click=${this._handleCancel}
|
||||
>
|
||||
</arx-button>
|
||||
<arx-button
|
||||
variant="secondary"
|
||||
label=${this.saveText}
|
||||
@click=${this._handleSave}
|
||||
>
|
||||
</arx-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
show() {
|
||||
this.open = true;
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.open = false;
|
||||
}
|
||||
|
||||
getValue(): string {
|
||||
return this._inputValue;
|
||||
}
|
||||
}
|
207
src/components/General/Select.ts
Normal file
207
src/components/General/Select.ts
Normal file
|
@ -0,0 +1,207 @@
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { map } from 'lit/directives/map.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
|
||||
@customElement('arx-select')
|
||||
export class StyledSelect<T> extends LitElement {
|
||||
@property() placeholder = '';
|
||||
@property() value = '';
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
@property() name = '';
|
||||
@property({ type: Boolean }) required = false;
|
||||
@property() label = '';
|
||||
@property({ type: Array<T> }) options = [];
|
||||
@property() valueMapper?: (option: T) => string;
|
||||
@property() textMapper?: (option: T) => string;
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--color-base-content);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 14px 16px;
|
||||
font-size: 16px;
|
||||
color: var(--color-base-content);
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
background-color: var(--color-base-100);
|
||||
border: var(--border) solid var(--color-base-300);
|
||||
border-radius: var(--radius-field);
|
||||
box-shadow: inset calc(var(--depth) * 2px) calc(var(--depth) * 2px)
|
||||
calc(var(--depth) * 4px)
|
||||
oklch(from var(--color-base-content) l c h / 0.15),
|
||||
inset calc(var(--depth) * -2px) calc(var(--depth) * -2px)
|
||||
calc(var(--depth) * 4px) oklch(from var(--color-base-100) l c h / 0.7),
|
||||
0 0 0 transparent;
|
||||
transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
cursor: pointer;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 16px center;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
select:hover {
|
||||
transform: translateY(-1px);
|
||||
background-color: oklch(from var(--color-base-100) calc(l + 0.02) c h);
|
||||
}
|
||||
|
||||
select:focus {
|
||||
outline: none;
|
||||
transform: translateY(1px);
|
||||
border-top: var(--border) solid
|
||||
oklch(from var(--color-base-300) l c h / 0.8);
|
||||
border-left: var(--border) solid
|
||||
oklch(from var(--color-base-300) l c h / 0.8);
|
||||
border-bottom: var(--border) solid
|
||||
oklch(from var(--color-base-200) l c h / 0.8);
|
||||
border-right: var(--border) solid
|
||||
oklch(from var(--color-base-200) l c h / 0.8);
|
||||
box-shadow: inset calc(var(--depth) * 3px) calc(var(--depth) * 3px)
|
||||
calc(var(--depth) * 5px)
|
||||
oklch(from var(--color-base-content) l c h / 0.2),
|
||||
inset calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 3px) oklch(from var(--color-base-100) l c h / 0.5),
|
||||
0 0 0 transparent;
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 16px center;
|
||||
background-color: linear-gradient(
|
||||
145deg,
|
||||
oklch(from var(--color-base-100) calc(l - 0.03) c h),
|
||||
oklch(from var(--color-base-100) l c h)
|
||||
);
|
||||
}
|
||||
|
||||
select:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
box-shadow: inset calc(var(--depth) * 1px) calc(var(--depth) * 1px)
|
||||
calc(var(--depth) * 2px)
|
||||
oklch(from var(--color-base-content) l c h / 0.05),
|
||||
inset calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 2px) oklch(from var(--color-base-100) l c h / 0.4);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
select option[value=""] {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
`;
|
||||
|
||||
private _getOptionValue(option: T) {
|
||||
if (option === undefined || option === null) return '';
|
||||
if (this.valueMapper) return this.valueMapper(option);
|
||||
if (typeof option === 'object' && 'value' in option) return option.value;
|
||||
return option;
|
||||
}
|
||||
|
||||
private _getOptionText(option: T) {
|
||||
if (option === undefined || option === null) return '';
|
||||
if (this.textMapper) return this.textMapper(option);
|
||||
if (typeof option === 'object' && 'label' in option) return option.label;
|
||||
return option;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
${when(this.label, () => html`<label for="select-${this.name}">${this.label}</label>`)}
|
||||
<select
|
||||
id="select-${this.name}"
|
||||
.value=${this.value}
|
||||
?disabled=${this.disabled}
|
||||
?required=${this.required}
|
||||
name=${this.name}
|
||||
@change=${this._handleChange}
|
||||
@focus=${this._handleFocus}
|
||||
@blur=${this._handleBlur}
|
||||
@keydown=${this._handleKeyDown}
|
||||
@keyup=${this._handleKeyUp}
|
||||
>
|
||||
${when(
|
||||
this.placeholder,
|
||||
() =>
|
||||
html`<option value="" ?selected=${!this.value} disabled>
|
||||
${this.placeholder}
|
||||
</option>`,
|
||||
)}
|
||||
${map(this.options, (option) => {
|
||||
const optionValue = this._getOptionValue(option);
|
||||
const optionText = this._getOptionText(option);
|
||||
return html`
|
||||
<option
|
||||
value=${optionValue}
|
||||
?selected=${optionValue === this.value}
|
||||
>
|
||||
${optionText}
|
||||
</option>
|
||||
`;
|
||||
})}
|
||||
</select>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleKeyDown(e: KeyboardEvent) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('keydown', {
|
||||
detail: { key: e.key },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleKeyUp(e: KeyboardEvent) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('keyup', {
|
||||
detail: { key: e.key },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleFocus() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('focus', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleBlur() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('blur', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleChange(e: Event) {
|
||||
const select = e.target as HTMLSelectElement;
|
||||
this.value = select.value;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('change', {
|
||||
detail: { value: this.value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
212
src/components/General/Textarea.ts
Normal file
212
src/components/General/Textarea.ts
Normal file
|
@ -0,0 +1,212 @@
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
|
||||
@customElement('arx-textarea')
|
||||
export class StyledTextarea extends LitElement {
|
||||
@property() placeholder = '';
|
||||
@property() value = '';
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
@property() name = '';
|
||||
@property({ type: Boolean }) required = false;
|
||||
@property() label = '';
|
||||
@property() rows = 4;
|
||||
@property() maxlength = '';
|
||||
@property({ type: Boolean }) resizable = true;
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: var(--color-base-content);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 80px;
|
||||
padding: 14px 16px;
|
||||
font-size: 16px;
|
||||
color: var(--color-base-content);
|
||||
font-family: inherit;
|
||||
line-height: 1.5;
|
||||
background: var(--color-base-100);
|
||||
border: var(--border) solid var(--color-base-300);
|
||||
border-radius: var(--radius-field);
|
||||
box-shadow: inset calc(var(--depth) * 2px) calc(var(--depth) * 2px)
|
||||
calc(var(--depth) * 4px)
|
||||
oklch(from var(--color-base-content) l c h / 0.15),
|
||||
inset calc(var(--depth) * -2px) calc(var(--depth) * -2px)
|
||||
calc(var(--depth) * 4px) oklch(from var(--color-base-100) l c h / 0.7),
|
||||
0 0 0 transparent;
|
||||
transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
:host(:not([resizable])) textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
textarea::placeholder {
|
||||
color: var(--color-secondary);
|
||||
transition: all 0.3s cubic-bezier(0.25, 0.1, 0.25, 1.1);
|
||||
}
|
||||
|
||||
textarea:hover {
|
||||
transform: translateY(-1px);
|
||||
background: oklch(from var(--color-base-100) calc(l + 0.02) c h);
|
||||
}
|
||||
|
||||
textarea:hover::placeholder {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
transform: translateY(1px);
|
||||
border-top: var(--border) solid
|
||||
oklch(from var(--color-base-300) l c h / 0.8);
|
||||
border-left: var(--border) solid
|
||||
oklch(from var(--color-base-300) l c h / 0.8);
|
||||
border-bottom: var(--border) solid
|
||||
oklch(from var(--color-base-200) l c h / 0.8);
|
||||
border-right: var(--border) solid
|
||||
oklch(from var(--color-base-200) l c h / 0.8);
|
||||
box-shadow: inset calc(var(--depth) * 3px) calc(var(--depth) * 3px)
|
||||
calc(var(--depth) * 5px)
|
||||
oklch(from var(--color-base-content) l c h / 0.2),
|
||||
inset calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 3px) oklch(from var(--color-base-100) l c h / 0.5),
|
||||
0 0 0 transparent;
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
oklch(from var(--color-base-100) calc(l - 0.03) c h),
|
||||
oklch(from var(--color-base-100) l c h)
|
||||
);
|
||||
}
|
||||
|
||||
textarea:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
box-shadow: inset calc(var(--depth) * 1px) calc(var(--depth) * 1px)
|
||||
calc(var(--depth) * 2px)
|
||||
oklch(from var(--color-base-content) l c h / 0.05),
|
||||
inset calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 2px) oklch(from var(--color-base-100) l c h / 0.4);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.character-count {
|
||||
display: none;
|
||||
text-align: right;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.has-maxlength .character-count {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.character-count.near-limit {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.character-count.at-limit {
|
||||
color: var(--color-error);
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
const charCount = this.value?.length || 0;
|
||||
const maxLen = Number.parseInt(this.maxlength) || 0;
|
||||
const nearLimit = maxLen && charCount >= maxLen * 0.8;
|
||||
const atLimit = maxLen && charCount >= maxLen * 0.95;
|
||||
const hasMaxlength = !!this.maxlength;
|
||||
|
||||
return html`
|
||||
${when(this.label, () => html`<label for="textarea-${this.name}">${this.label}</label>`)}
|
||||
<div class="${hasMaxlength ? 'has-maxlength' : ''}">
|
||||
<textarea
|
||||
id="textarea-${this.name}"
|
||||
.value=${this.value}
|
||||
?disabled=${this.disabled}
|
||||
?required=${this.required}
|
||||
placeholder=${this.placeholder}
|
||||
rows=${this.rows}
|
||||
maxlength=${this.maxlength}
|
||||
name=${this.name}
|
||||
@input=${this._handleInput}
|
||||
@focus=${this._handleFocus}
|
||||
@blur=${this._handleBlur}
|
||||
@keydown=${this._handleKeyDown}
|
||||
></textarea>
|
||||
<div
|
||||
class="character-count ${nearLimit ? 'near-limit' : ''} ${atLimit ? 'at-limit' : ''}"
|
||||
>
|
||||
${charCount}/${this.maxlength}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === 'Tab' && !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
const textarea = e.target as HTMLTextAreaElement;
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
|
||||
this.value = `${this.value.substring(0, start)}\t${this.value.substring(end)}`;
|
||||
|
||||
setTimeout(() => {
|
||||
textarea.selectionStart = textarea.selectionEnd = start + 1;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('keydown', {
|
||||
detail: { key: e.key },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleFocus() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('focus', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleBlur() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('blur', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleInput(e: InputEvent) {
|
||||
const textarea = e.target as HTMLTextAreaElement;
|
||||
this.value = textarea.value;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('input', {
|
||||
detail: { value: this.value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
216
src/components/General/Toggle.ts
Normal file
216
src/components/General/Toggle.ts
Normal file
|
@ -0,0 +1,216 @@
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
|
||||
@customElement('arx-toggle')
|
||||
export class StyledToggle extends LitElement {
|
||||
@property() label = '';
|
||||
@property({ type: Boolean, reflect: true }) checked = false;
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
@property() name = '';
|
||||
@property({ type: Boolean }) required = false;
|
||||
@property() value = 'on';
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.toggle-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
color: var(--color-base-content);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toggle-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.toggle-input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.toggle-track {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: var(--color-base-100);
|
||||
border-radius: 24px;
|
||||
transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
border: var(--border) solid var(--color-base-300);
|
||||
box-shadow: inset calc(var(--depth) * 2px) calc(var(--depth) * 2px)
|
||||
calc(var(--depth) * 4px)
|
||||
oklch(from var(--color-base-content) l c h / 0.15),
|
||||
inset calc(var(--depth) * -2px) calc(var(--depth) * -2px)
|
||||
calc(var(--depth) * 4px) oklch(from var(--color-base-100) l c h / 0.7);
|
||||
}
|
||||
|
||||
.toggle-track:hover {
|
||||
background-color: oklch(from var(--color-base-100) calc(l + 0.02) c h);
|
||||
}
|
||||
|
||||
.toggle-knob {
|
||||
position: absolute;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
left: 3px;
|
||||
bottom: 2px;
|
||||
background-color: var(--color-base-content);
|
||||
border-radius: 50%;
|
||||
transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
box-shadow: calc(var(--depth) * 2px) calc(var(--depth) * 2px)
|
||||
calc(var(--depth) * 4px)
|
||||
oklch(from var(--color-base-content) l c h / 0.2),
|
||||
calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 3px) oklch(from var(--color-base-100) l c h / 0.3);
|
||||
}
|
||||
|
||||
.toggle-input:checked + .toggle-track {
|
||||
background-color: var(--color-base-100);
|
||||
border-top: var(--border) solid
|
||||
oklch(from var(--color-base-300) l c h / 0.8);
|
||||
border-left: var(--border) solid
|
||||
oklch(from var(--color-base-300) l c h / 0.8);
|
||||
border-bottom: var(--border) solid
|
||||
oklch(from var(--color-base-200) l c h / 0.8);
|
||||
border-right: var(--border) solid
|
||||
oklch(from var(--color-base-200) l c h / 0.8);
|
||||
box-shadow: inset calc(var(--depth) * 3px) calc(var(--depth) * 3px)
|
||||
calc(var(--depth) * 5px)
|
||||
oklch(from var(--color-base-content) l c h / 0.2),
|
||||
inset calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 3px) oklch(from var(--color-base-100) l c h / 0.5);
|
||||
}
|
||||
|
||||
.toggle-input:checked + .toggle-track .toggle-knob {
|
||||
transform: translateX(24px);
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
|
||||
.toggle-input:focus + .toggle-track {
|
||||
outline: none;
|
||||
box-shadow: inset calc(var(--depth) * 3px) calc(var(--depth) * 3px)
|
||||
calc(var(--depth) * 5px)
|
||||
oklch(from var(--color-base-content) l c h / 0.2),
|
||||
inset calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 3px) oklch(from var(--color-base-100) l c h / 0.5),
|
||||
0 0 0 2px oklch(from var(--color-accent) l c h / 0.3);
|
||||
}
|
||||
|
||||
.toggle-input:disabled + .toggle-track {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
box-shadow: inset calc(var(--depth) * 1px) calc(var(--depth) * 1px)
|
||||
calc(var(--depth) * 2px)
|
||||
oklch(from var(--color-base-content) l c h / 0.05),
|
||||
inset calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 2px) oklch(from var(--color-base-100) l c h / 0.4);
|
||||
}
|
||||
|
||||
.toggle-input:disabled + .toggle-track .toggle-knob {
|
||||
background-color: var(--color-secondary);
|
||||
box-shadow: calc(var(--depth) * 1px) calc(var(--depth) * 1px)
|
||||
calc(var(--depth) * 2px)
|
||||
oklch(from var(--color-base-content) l c h / 0.1),
|
||||
calc(var(--depth) * -1px) calc(var(--depth) * -1px)
|
||||
calc(var(--depth) * 2px) oklch(from var(--color-base-100) l c h / 0.2);
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="toggle-container">
|
||||
${when(this.label, () => html` <label for="toggle-${this.name}">${this.label}</label> `)}
|
||||
<div class="toggle-wrapper">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="toggle-${this.name}"
|
||||
class="toggle-input"
|
||||
.checked=${this.checked}
|
||||
?disabled=${this.disabled}
|
||||
?required=${this.required}
|
||||
name=${this.name}
|
||||
value=${this.value}
|
||||
@change=${this._handleChange}
|
||||
@focus=${this._handleFocus}
|
||||
@blur=${this._handleBlur}
|
||||
@keydown=${this._handleKeyDown}
|
||||
/>
|
||||
<label for="toggle-${this.name}" class="toggle-track">
|
||||
<span class="toggle-knob"></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleChange(e: Event) {
|
||||
const checkbox = e.target as HTMLInputElement;
|
||||
this.checked = checkbox.checked;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('change', {
|
||||
detail: { checked: this.checked, value: this.value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleFocus() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('focus', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleBlur() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('blur', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private _handleKeyDown(e: KeyboardEvent) {
|
||||
if (e.key === ' ' || e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
this.checked = !this.checked;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('change', {
|
||||
detail: { checked: this.checked, value: this.value },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('keydown', {
|
||||
detail: { key: e.key },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
241
src/components/General/Tooltip.ts
Normal file
241
src/components/General/Tooltip.ts
Normal file
|
@ -0,0 +1,241 @@
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('arx-tooltip')
|
||||
export class Tooltip extends LitElement {
|
||||
@property() content = '';
|
||||
@property() position = 'top';
|
||||
@property() delay = 100; // Delay before showing tooltip (ms)
|
||||
@property() offset = 8; // Distance from target element
|
||||
@property() variant = 'default'; // default, primary, secondary, accent, error, warning, success
|
||||
@property({ type: Boolean }) arrow = true;
|
||||
@property({ type: Boolean }) allowHtml = false;
|
||||
@property({ type: Boolean }) interactive = false;
|
||||
@property({ type: Boolean }) disabled = false;
|
||||
|
||||
@state() private isVisible = false;
|
||||
@state() private timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tooltip-container {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
max-width: 300px;
|
||||
width: 100%;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--radius-field);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.4;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.2s, visibility 0.2s, transform 0.2s;
|
||||
box-shadow: calc(var(--depth) * 2px) calc(var(--depth) * 2px)
|
||||
calc(var(--depth) * 4px)
|
||||
oklch(from var(--color-base-content) l c h / 0.2);
|
||||
transform: scale(0.95);
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.tooltip.interactive {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.tooltip.visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* Default variant */
|
||||
.tooltip.default {
|
||||
background-color: var(--color-base-content);
|
||||
color: var(--color-base-100);
|
||||
}
|
||||
|
||||
/* Primary variant */
|
||||
.tooltip.primary {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-primary-content);
|
||||
}
|
||||
|
||||
/* Secondary variant */
|
||||
.tooltip.secondary {
|
||||
background-color: var(--color-secondary);
|
||||
color: var(--color-secondary-content);
|
||||
}
|
||||
|
||||
/* Accent variant */
|
||||
.tooltip.accent {
|
||||
background-color: var(--color-accent);
|
||||
color: var(--color-accent-content);
|
||||
}
|
||||
|
||||
/* Error variant */
|
||||
.tooltip.error {
|
||||
background-color: var(--color-error);
|
||||
color: var(--color-error-content);
|
||||
}
|
||||
|
||||
/* Warning variant */
|
||||
.tooltip.warning {
|
||||
background-color: var(--color-warning);
|
||||
color: var(--color-warning-content);
|
||||
}
|
||||
|
||||
/* Success variant */
|
||||
.tooltip.success {
|
||||
background-color: var(--color-success);
|
||||
color: var(--color-success-content);
|
||||
}
|
||||
|
||||
/* Positions */
|
||||
.tooltip.top {
|
||||
bottom: calc(100% + var(--offset, 8px));
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scale(0.95);
|
||||
}
|
||||
|
||||
.tooltip.top.visible {
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
|
||||
.tooltip.right {
|
||||
left: calc(100% + var(--offset, 8px));
|
||||
top: 50%;
|
||||
transform: translateY(-50%) scale(0.95);
|
||||
}
|
||||
|
||||
.tooltip.right.visible {
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
|
||||
.tooltip.bottom {
|
||||
top: calc(100% + var(--offset, 8px));
|
||||
left: 50%;
|
||||
transform: translateX(-50%) scale(0.95);
|
||||
}
|
||||
|
||||
.tooltip.bottom.visible {
|
||||
transform: translateX(-50%) scale(1);
|
||||
}
|
||||
|
||||
.tooltip.left {
|
||||
right: calc(100% + var(--offset, 8px));
|
||||
top: 50%;
|
||||
transform: translateY(-50%) scale(0.95);
|
||||
}
|
||||
|
||||
.tooltip.left.visible {
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
|
||||
/* Arrow */
|
||||
.tooltip::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: inherit;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.tooltip.top::after {
|
||||
bottom: -4px;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.tooltip.right::after {
|
||||
left: -4px;
|
||||
top: 50%;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.tooltip.bottom::after {
|
||||
top: -4px;
|
||||
left: 50%;
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.tooltip.left::after {
|
||||
right: -4px;
|
||||
top: 50%;
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.tooltip.no-arrow::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.tooltip {
|
||||
max-width: 200px;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.4rem 0.6rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div
|
||||
class="tooltip-container"
|
||||
@mouseenter=${!this.disabled ? this._onMouseEnter : null}
|
||||
@mouseleave=${!this.disabled ? this._onMouseLeave : null}
|
||||
@focus=${!this.disabled ? this._onMouseEnter : null}
|
||||
@blur=${!this.disabled ? this._onMouseLeave : null}
|
||||
>
|
||||
<slot></slot>
|
||||
<div
|
||||
class="tooltip ${this.position} ${this.variant} ${
|
||||
this.isVisible ? 'visible' : ''
|
||||
} ${!this.arrow ? 'no-arrow' : ''} ${this.interactive ? 'interactive' : ''}"
|
||||
style="--offset: ${this.offset}px;"
|
||||
role="tooltip"
|
||||
aria-hidden=${!this.isVisible}
|
||||
>
|
||||
${this.allowHtml ? html`<div .innerHTML=${this.content}></div>` : this.content}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _onMouseEnter() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
}
|
||||
|
||||
this.timer = setTimeout(() => {
|
||||
this.isVisible = true;
|
||||
}, this.delay);
|
||||
}
|
||||
|
||||
private _onMouseLeave() {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
|
||||
this.isVisible = false;
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = null;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue