feat(components): add large set of new UI components and demos, reorganize groups, and bump a few dependencies
This commit is contained in:
335
ts_web/elements/00group-utility/dees-icon/dees-icon.demo.ts
Normal file
335
ts_web/elements/00group-utility/dees-icon/dees-icon.demo.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
import { icons, type IconWithPrefix } from '../dees-icon/dees-icon.js';
|
||||
import * as lucideIcons from 'lucide';
|
||||
|
||||
export const demoFunc = () => {
|
||||
// Group FontAwesome icons by type
|
||||
const faIcons = Object.keys(icons.fa);
|
||||
|
||||
// Extract Lucide icons from the lucideIcons object directly
|
||||
// Log the first few keys to understand the structure
|
||||
console.log('First few Lucide keys:', Object.keys(lucideIcons).slice(0, 5));
|
||||
|
||||
// Get all icon functions from lucideIcons (they have PascalCase names)
|
||||
const lucideIconsList = Object.keys(lucideIcons)
|
||||
.filter(key => {
|
||||
// Skip utility functions and focus on icon components (first letter is uppercase)
|
||||
const isUppercaseFirst = key[0] === key[0].toUpperCase() && key[0] !== key[0].toLowerCase();
|
||||
const isFunction = typeof lucideIcons[key] === 'function';
|
||||
const notUtility = !['createElement', 'createIcons', 'default'].includes(key);
|
||||
return isFunction && isUppercaseFirst && notUtility;
|
||||
})
|
||||
.map(pascalName => {
|
||||
// Convert PascalCase to camelCase
|
||||
return pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
|
||||
});
|
||||
|
||||
// Log how many icons we found
|
||||
console.log(`Found ${lucideIconsList.length} Lucide icons`);
|
||||
|
||||
// If we didn't find any, try an alternative approach
|
||||
if (lucideIconsList.length === 0) {
|
||||
console.log('Trying alternative approach to find Lucide icons');
|
||||
|
||||
// Try to get icon names from a known property if available
|
||||
if (lucideIcons.icons) {
|
||||
const iconSource = lucideIcons.icons || {};
|
||||
lucideIconsList.push(...Object.keys(iconSource));
|
||||
console.log(`Found ${lucideIconsList.length} icons via alternative method`);
|
||||
}
|
||||
}
|
||||
|
||||
// Define the functions in TS scope instead of script tags
|
||||
const copyAllIconNames = () => {
|
||||
// Generate complete list of all icon names with prefixes
|
||||
const faIconsList = faIcons.map(name => `fa:${name}`);
|
||||
const lucideIconsListPrefixed = lucideIconsList.map(name => `lucide:${name}`);
|
||||
const allIcons = [...faIconsList, ...lucideIconsListPrefixed];
|
||||
const textToCopy = allIcons.join('\n');
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
// Show feedback
|
||||
const currentEvent = window.event as MouseEvent;
|
||||
const button = currentEvent.currentTarget as HTMLElement;
|
||||
const originalText = button.textContent;
|
||||
button.textContent = `✓ Copied ${allIcons.length} icon names!`;
|
||||
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
}, 2000);
|
||||
});
|
||||
};
|
||||
|
||||
const searchIcons = (event: InputEvent) => {
|
||||
const searchTerm = (event.target as HTMLInputElement).value.toLowerCase().trim();
|
||||
// Get the demo container first, then search within it
|
||||
const demoContainer = (event.target as HTMLElement).closest('.demoContainer');
|
||||
const containers = demoContainer.querySelectorAll('.iconContainer');
|
||||
|
||||
containers.forEach(container => {
|
||||
const iconName = container.getAttribute('data-name');
|
||||
|
||||
if (searchTerm === '') {
|
||||
container.classList.remove('hidden');
|
||||
} else if (iconName && iconName.includes(searchTerm)) {
|
||||
container.classList.remove('hidden');
|
||||
} else {
|
||||
container.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
// Update counts - search within demoContainer
|
||||
demoContainer.querySelectorAll('.section-container').forEach(section => {
|
||||
const visibleIcons = section.querySelectorAll('.iconContainer:not(.hidden)').length;
|
||||
const countElement = section.querySelector('.icon-count');
|
||||
if (countElement) {
|
||||
const totalIconsCount = section.classList.contains('fa-section')
|
||||
? faIcons.length
|
||||
: lucideIconsList.length;
|
||||
|
||||
countElement.textContent = visibleIcons === totalIconsCount
|
||||
? `${totalIconsCount} icons`
|
||||
: `${visibleIcons} of ${totalIconsCount} icons`;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const copyIconName = (iconNameToCopy: string, type: 'fa' | 'lucide') => {
|
||||
// Use the new prefix format
|
||||
const textToCopy = `${type}:${iconNameToCopy}`;
|
||||
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
// Find the event target
|
||||
const currentEvent = window.event as MouseEvent;
|
||||
const currentTarget = currentEvent.currentTarget as HTMLElement;
|
||||
// Show feedback
|
||||
const tooltip = currentTarget.querySelector('.copy-tooltip');
|
||||
if (tooltip) {
|
||||
tooltip.textContent = 'Copied!';
|
||||
|
||||
setTimeout(() => {
|
||||
tooltip.textContent = 'Click to copy';
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return html`
|
||||
<style>
|
||||
.demoContainer {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
background: #111111;
|
||||
padding: 20px;
|
||||
font-size: 30px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
#iconSearch {
|
||||
flex: 1;
|
||||
padding: 12px 16px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
#iconSearch:focus {
|
||||
outline: none;
|
||||
border-color: #e4002b;
|
||||
}
|
||||
|
||||
.copy-all-button {
|
||||
padding: 12px 20px;
|
||||
font-size: 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #e4002b;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.copy-all-button:hover {
|
||||
background: #c4001b;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.copy-all-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
dees-icon {
|
||||
transition: all 0.2s ease;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.iconContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 16px 0px 16px;
|
||||
border: 1px solid #333333;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.iconContainer:hover {
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.iconName {
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
color: #ccc;
|
||||
background: #333333;
|
||||
padding: 6px 10px;
|
||||
margin-left: -16px;
|
||||
margin-right: -16px;
|
||||
margin-top: 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 120px;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
width: 100%;
|
||||
color: #ffffff;
|
||||
font-size: 24px;
|
||||
margin: 20px 0;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #333333;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.api-note {
|
||||
font-size: 14px;
|
||||
color: #e4002b;
|
||||
margin-bottom: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #e4002b;
|
||||
border-radius: 4px;
|
||||
background: rgba(228, 0, 43, 0.1);
|
||||
}
|
||||
|
||||
.icon-count {
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
font-weight: normal;
|
||||
background: #222;
|
||||
padding: 5px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.icons-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-container {
|
||||
width: 100%;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.copy-tooltip {
|
||||
position: absolute;
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 5px 10px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
top: -30px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.iconContainer:hover .copy-tooltip {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.iconContainer:hover dees-icon {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="demoContainer">
|
||||
<div class="search-container">
|
||||
<input type="text" id="iconSearch" placeholder="Search icons..." @input=${searchIcons}>
|
||||
<button class="copy-all-button" @click=${copyAllIconNames}>📋 Copy All Icon Names</button>
|
||||
</div>
|
||||
|
||||
<div class="api-note">
|
||||
New API: Use <code>icon="fa:iconName"</code> or <code>icon="lucide:iconName"</code> instead of <code>iconFA</code>.
|
||||
Click any icon to copy its new format to clipboard.
|
||||
</div>
|
||||
|
||||
<div class="section-container fa-section">
|
||||
<div class="section-title">
|
||||
FontAwesome Icons
|
||||
<span class="icon-count">${faIcons.length} icons</span>
|
||||
</div>
|
||||
<div class="icons-grid">
|
||||
${faIcons.map(
|
||||
(iconName) => {
|
||||
const prefixedName = `fa:${iconName}`;
|
||||
return html`
|
||||
<div class="iconContainer fa-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'fa')}>
|
||||
<dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon>
|
||||
<div class="iconName">fa:${iconName}</div>
|
||||
<span class="copy-tooltip">Click to copy</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-container lucide-section">
|
||||
<div class="section-title">
|
||||
Lucide Icons
|
||||
<span class="icon-count">${lucideIconsList.length} icons</span>
|
||||
</div>
|
||||
<div class="icons-grid">
|
||||
${lucideIconsList.map(
|
||||
(iconName) => {
|
||||
const prefixedName = `lucide:${iconName}`;
|
||||
return html`
|
||||
<div class="iconContainer lucide-icon" data-name=${iconName.toLowerCase()} @click=${() => copyIconName(iconName, 'lucide')}>
|
||||
<dees-icon .icon=${prefixedName as IconWithPrefix} iconSize="24"></dees-icon>
|
||||
<div class="iconName">lucide:${iconName}</div>
|
||||
<span class="copy-tooltip">Click to copy</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
458
ts_web/elements/00group-utility/dees-icon/dees-icon.ts
Normal file
458
ts_web/elements/00group-utility/dees-icon/dees-icon.ts
Normal file
@@ -0,0 +1,458 @@
|
||||
import {
|
||||
DeesElement,
|
||||
html,
|
||||
property,
|
||||
customElement,
|
||||
cssManager,
|
||||
css,
|
||||
type CSSResult,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
import { icon, type IconDefinition } from '@fortawesome/fontawesome-svg-core';
|
||||
import {
|
||||
faFacebook,
|
||||
faGoogle,
|
||||
faLinkedin,
|
||||
faMedium,
|
||||
faSlackHash,
|
||||
faTwitter,
|
||||
faInstagram,
|
||||
faTiktok,
|
||||
} from '@fortawesome/free-brands-svg-icons';
|
||||
|
||||
import {
|
||||
faCopy as faCopyRegular,
|
||||
faCircleCheck as faCircleCheckRegular,
|
||||
faCircleXmark as faCircleXmarkRegular,
|
||||
faMessage as faMessageRegular,
|
||||
faPaste as faPasteRegular,
|
||||
faSun as faSunRegular,
|
||||
faTrashCan as faTrashCanRegular,
|
||||
} from '@fortawesome/free-regular-svg-icons';
|
||||
import {
|
||||
faArrowRight as faArrowRightSolid,
|
||||
faArrowUpRightFromSquare as faArrowUpRightFromSquareSolid,
|
||||
faBell as faBellSolid,
|
||||
faBug as faBugSolid,
|
||||
faBuilding as faBuildingSolid,
|
||||
faCaretLeft as faCaretLeftSolid,
|
||||
faCaretRight as faCaretRightSolid,
|
||||
faCheck as faCheckSolid,
|
||||
faCircleInfo as faCircleInfoSolid,
|
||||
faCircleCheck as faCircleCheckSolid,
|
||||
faCircleXmark as faCircleXmarkSolid,
|
||||
faClockRotateLeft as faClockRotateLeftSolid,
|
||||
faCopy as faCopySolid,
|
||||
faDesktop as faDesktopSolid,
|
||||
faEye as faEyeSolid,
|
||||
faEyeSlash as faEyeSlashSolid,
|
||||
faFileInvoice as faFileInvoiceSolid,
|
||||
faFileInvoiceDollar as faFileInvoiceDollarSolid,
|
||||
faGear as faGearSolid,
|
||||
faGrip as faGripSolid,
|
||||
faMagnifyingGlass as faMagnifyingGlassSolid,
|
||||
faMessage as faMessageSolid,
|
||||
faMoneyCheckDollar as faMoneyCheckDollarSolid,
|
||||
faMugHot as faMugHotSolid,
|
||||
faMinus as faMinusSolid,
|
||||
faNetworkWired as faNetworkWiredSolid,
|
||||
faPaperclip as faPaperclipSolid,
|
||||
faPaste as faPasteSolid,
|
||||
faPenToSquare as faPenToSquareSolid,
|
||||
faPlus as faPlusSolid,
|
||||
faReceipt as faReceiptSolid,
|
||||
faRss as faRssSolid,
|
||||
faUsers as faUsersSolid,
|
||||
faShare as faShareSolid,
|
||||
faSun as faSunSolid,
|
||||
faTerminal as faTerminalSolid,
|
||||
faTrash as faTrashSolid,
|
||||
faTrashCan as faTrashCanSolid,
|
||||
faWallet as faWalletSolid,
|
||||
faXmark as faXmarkSolid,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { demoFunc } from './dees-icon.demo.js';
|
||||
|
||||
// Import Lucide icons and the createElement function
|
||||
import * as lucideIcons from 'lucide';
|
||||
import { createElement } from 'lucide';
|
||||
|
||||
// Collect FontAwesome icons
|
||||
const faIcons = {
|
||||
// normal
|
||||
arrowRight: faArrowRightSolid,
|
||||
arrowUpRightFromSquare: faArrowUpRightFromSquareSolid,
|
||||
bell: faBellSolid,
|
||||
bug: faBugSolid,
|
||||
building: faBuildingSolid,
|
||||
caretLeft: faCaretLeftSolid,
|
||||
caretRight: faCaretRightSolid,
|
||||
check: faCheckSolid,
|
||||
circleInfo: faCircleInfoSolid,
|
||||
circleCheck: faCircleCheckRegular,
|
||||
circleCheckSolid: faCircleCheckSolid,
|
||||
circleXmark: faCircleXmarkRegular,
|
||||
circleXmarkSolid: faCircleXmarkSolid,
|
||||
clockRotateLeft: faClockRotateLeftSolid,
|
||||
copy: faCopyRegular,
|
||||
copySolid: faCopySolid,
|
||||
desktop: faDesktopSolid,
|
||||
eye: faEyeSolid,
|
||||
eyeSlash: faEyeSlashSolid,
|
||||
fileInvoice: faFileInvoiceSolid,
|
||||
fileInvoiceDoller: faFileInvoiceDollarSolid,
|
||||
gear: faGearSolid,
|
||||
grip: faGripSolid,
|
||||
magnifyingGlass: faMagnifyingGlassSolid,
|
||||
message: faMessageRegular,
|
||||
messageSolid: faMessageSolid,
|
||||
moneyCheckDollar: faMoneyCheckDollarSolid,
|
||||
mugHot: faMugHotSolid,
|
||||
minus: faMinusSolid,
|
||||
networkWired: faNetworkWiredSolid,
|
||||
paperclip: faPaperclipSolid,
|
||||
paste: faPasteRegular,
|
||||
pasteSolid: faPasteSolid,
|
||||
penToSquare: faPenToSquareSolid,
|
||||
plus: faPlusSolid,
|
||||
receipt: faReceiptSolid,
|
||||
rss: faRssSolid,
|
||||
share: faShareSolid,
|
||||
sun: faSunRegular,
|
||||
sunSolid: faSunSolid,
|
||||
terminal: faTerminalSolid,
|
||||
trash: faTrashSolid,
|
||||
trashSolid: faTrashSolid,
|
||||
trashCan: faTrashCanRegular,
|
||||
trashCanSolid: faTrashCanSolid,
|
||||
users: faUsersSolid,
|
||||
wallet: faWalletSolid,
|
||||
xmark: faXmarkSolid,
|
||||
// brands
|
||||
facebook: faFacebook,
|
||||
google: faGoogle,
|
||||
instagram: faInstagram,
|
||||
linkedin: faLinkedin,
|
||||
medium: faMedium,
|
||||
slack: faSlackHash,
|
||||
tiktok: faTiktok,
|
||||
twitter: faTwitter,
|
||||
};
|
||||
|
||||
// Create a string literal type for all FA icons
|
||||
type FAIconKey = keyof typeof faIcons;
|
||||
|
||||
// Create union types for the icons with prefixes
|
||||
export type IconWithPrefix = `fa:${FAIconKey}` | `lucide:${string}`;
|
||||
|
||||
// Export only FontAwesome icons directly
|
||||
export const icons = {
|
||||
fa: faIcons
|
||||
};
|
||||
|
||||
// Legacy type for backward compatibility
|
||||
export type TIconKey = FAIconKey | `lucide:${string}`;
|
||||
|
||||
// Use a global static cache for all icons to reduce rendering
|
||||
const iconCache = new Map<string, string>();
|
||||
|
||||
// Clear cache items occasionally to prevent memory leaks
|
||||
const MAX_CACHE_SIZE = 500;
|
||||
function limitCacheSize() {
|
||||
if (iconCache.size > MAX_CACHE_SIZE) {
|
||||
// Remove oldest entries (first 20% of items)
|
||||
const keysToDelete = Array.from(iconCache.keys()).slice(0, MAX_CACHE_SIZE / 5);
|
||||
keysToDelete.forEach(key => iconCache.delete(key));
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-icon': DeesIcon;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-icon')
|
||||
export class DeesIcon extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Utility'];
|
||||
|
||||
/**
|
||||
* @deprecated Use the `icon` property instead with format "fa:iconName" or "lucide:iconName"
|
||||
*/
|
||||
@property({
|
||||
type: String,
|
||||
converter: {
|
||||
// Convert attribute string to property (for reflected attributes)
|
||||
fromAttribute: (value: string): TIconKey => value as TIconKey,
|
||||
// Convert property to attribute (for reflection)
|
||||
toAttribute: (value: TIconKey): string => value
|
||||
}
|
||||
})
|
||||
accessor iconFA: TIconKey | undefined = undefined;
|
||||
|
||||
/**
|
||||
* The preferred icon property. Use format "fa:iconName" or "lucide:iconName"
|
||||
* Examples: "fa:check", "lucide:menu"
|
||||
*/
|
||||
@property({
|
||||
type: String,
|
||||
converter: {
|
||||
fromAttribute: (value: string): IconWithPrefix => value as IconWithPrefix,
|
||||
toAttribute: (value: IconWithPrefix): string => value
|
||||
}
|
||||
})
|
||||
accessor icon: IconWithPrefix | undefined = undefined;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor iconSize: number;
|
||||
|
||||
@property({ type: String })
|
||||
accessor color: string = 'currentColor';
|
||||
|
||||
@property({ type: Number })
|
||||
accessor strokeWidth: number = 2;
|
||||
|
||||
// For tracking when we need to re-render
|
||||
private lastIcon: IconWithPrefix | TIconKey | null = null;
|
||||
private lastIconSize: number | null = null;
|
||||
private lastColor: string | null = null;
|
||||
private lastStrokeWidth: number | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the effective icon value, supporting both the new `icon` property
|
||||
* and the legacy `iconFA` property for backward compatibility.
|
||||
* Prefers `icon` if both are set.
|
||||
*/
|
||||
private getEffectiveIcon(): IconWithPrefix | TIconKey | null {
|
||||
// Prefer the new API
|
||||
if (this.icon) {
|
||||
return this.icon;
|
||||
}
|
||||
|
||||
// Fall back to the old API
|
||||
if (this.iconFA) {
|
||||
// If iconFA is already in the proper format (lucide:name), use it directly
|
||||
if (this.iconFA.startsWith('lucide:')) {
|
||||
return this.iconFA;
|
||||
}
|
||||
|
||||
// For FontAwesome icons with no prefix, add the prefix
|
||||
return `fa:${this.iconFA}` as IconWithPrefix;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an icon string into its type and name parts
|
||||
* @param iconStr The icon string in format "type:name"
|
||||
* @returns Object with type and name properties
|
||||
*/
|
||||
private parseIconString(iconStr: string): { type: 'fa' | 'lucide', name: string } {
|
||||
const lowerStr = iconStr.toLowerCase();
|
||||
if (lowerStr.startsWith('fa:')) {
|
||||
return {
|
||||
type: 'fa',
|
||||
name: iconStr.substring(3) // Remove 'fa:' prefix
|
||||
};
|
||||
} else if (lowerStr.startsWith('lucide:')) {
|
||||
return {
|
||||
type: 'lucide',
|
||||
name: iconStr.substring(7) // Remove 'lucide:' prefix
|
||||
};
|
||||
} else {
|
||||
// Default to Lucide when no prefix is provided
|
||||
return {
|
||||
type: 'lucide',
|
||||
name: iconStr
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private renderLucideIcon(iconName: string): string {
|
||||
// Create a cache key based on all visual properties
|
||||
const cacheKey = `lucide:${iconName}:${this.iconSize}:${this.color}:${this.strokeWidth}`;
|
||||
|
||||
// Check if we already have this icon in the cache
|
||||
if (iconCache.has(cacheKey)) {
|
||||
return iconCache.get(cacheKey) || '';
|
||||
}
|
||||
|
||||
try {
|
||||
// Get the Pascal case icon name (Menu instead of menu)
|
||||
const pascalCaseName = iconName.charAt(0).toUpperCase() + iconName.slice(1);
|
||||
|
||||
// Check if the icon exists in lucideIcons
|
||||
if (!lucideIcons[pascalCaseName]) {
|
||||
console.warn(`Lucide icon '${pascalCaseName}' not found in lucideIcons object`);
|
||||
return '';
|
||||
}
|
||||
|
||||
// Use the exact pattern from Lucide documentation
|
||||
const svgElement = createElement(lucideIcons[pascalCaseName], {
|
||||
color: this.color,
|
||||
size: this.iconSize,
|
||||
strokeWidth: this.strokeWidth
|
||||
});
|
||||
|
||||
if (!svgElement) {
|
||||
console.warn(`createElement returned empty result for ${pascalCaseName}`);
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get the HTML
|
||||
const result = svgElement.outerHTML;
|
||||
|
||||
// Cache the result for future use
|
||||
iconCache.set(cacheKey, result);
|
||||
limitCacheSize();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`Error rendering Lucide icon ${iconName}:`, error);
|
||||
|
||||
// Create a fallback SVG with the icon name
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${this.iconSize}" height="${this.iconSize}" viewBox="0 0 24 24" fill="none" stroke="${this.color}" stroke-width="${this.strokeWidth}" stroke-linecap="round" stroke-linejoin="round">
|
||||
<text x="50%" y="50%" font-size="6" text-anchor="middle" dominant-baseline="middle" fill="${this.color}">${iconName}</text>
|
||||
</svg>`;
|
||||
}
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Improve rendering performance */
|
||||
#iconContainer svg {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
will-change: transform; /* Helps with animations */
|
||||
contain: strict; /* Performance optimization */
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render() {
|
||||
return html`
|
||||
${domtools.elementBasic.styles}
|
||||
<style>
|
||||
#iconContainer {
|
||||
width: ${this.iconSize}px;
|
||||
height: ${this.iconSize}px;
|
||||
}
|
||||
</style>
|
||||
<div id="iconContainer"></div>
|
||||
`;
|
||||
}
|
||||
|
||||
public updated() {
|
||||
// If size is not specified, use font size as a base
|
||||
if (!this.iconSize) {
|
||||
this.iconSize = parseInt(globalThis.getComputedStyle(this).fontSize.replace(/\D/g,''));
|
||||
}
|
||||
|
||||
// Get the effective icon (either from icon or iconFA property)
|
||||
const effectiveIcon = this.getEffectiveIcon();
|
||||
|
||||
// Check if we actually need to update the icon
|
||||
// This prevents unnecessary DOM operations when properties haven't changed
|
||||
if (this.lastIcon === effectiveIcon &&
|
||||
this.lastIconSize === this.iconSize &&
|
||||
this.lastColor === this.color &&
|
||||
this.lastStrokeWidth === this.strokeWidth) {
|
||||
return; // No visual changes - skip update
|
||||
}
|
||||
|
||||
// Update our "last properties" for future change detection
|
||||
this.lastIcon = effectiveIcon;
|
||||
this.lastIconSize = this.iconSize;
|
||||
this.lastColor = this.color;
|
||||
this.lastStrokeWidth = this.strokeWidth;
|
||||
|
||||
const container = this.shadowRoot?.querySelector('#iconContainer');
|
||||
if (!container || !effectiveIcon) return;
|
||||
|
||||
try {
|
||||
// Parse the icon string to get type and name
|
||||
const { type, name } = this.parseIconString(effectiveIcon);
|
||||
|
||||
if (type === 'lucide') {
|
||||
// For Lucide, use direct DOM manipulation as shown in the docs
|
||||
// This approach avoids HTML string issues
|
||||
container.innerHTML = ''; // Clear container
|
||||
|
||||
try {
|
||||
// Convert to PascalCase
|
||||
const pascalCaseName = name.charAt(0).toUpperCase() + name.slice(1);
|
||||
|
||||
if (lucideIcons[pascalCaseName]) {
|
||||
// Use the documented pattern from Lucide docs
|
||||
const svgElement = createElement(lucideIcons[pascalCaseName], {
|
||||
color: this.color,
|
||||
size: this.iconSize,
|
||||
strokeWidth: this.strokeWidth
|
||||
});
|
||||
|
||||
if (svgElement) {
|
||||
// Directly append the element
|
||||
container.appendChild(svgElement);
|
||||
return; // Exit early since we've added the element
|
||||
}
|
||||
}
|
||||
|
||||
// If we reach here, something went wrong
|
||||
throw new Error(`Could not create element for ${pascalCaseName}`);
|
||||
} catch (error) {
|
||||
console.error(`Error rendering Lucide icon:`, error);
|
||||
|
||||
// Fall back to the string-based approach
|
||||
const iconHtml = this.renderLucideIcon(name);
|
||||
if (iconHtml) {
|
||||
container.innerHTML = iconHtml;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Use FontAwesome rendering via HTML string
|
||||
const faIcon = icons.fa[name as FAIconKey];
|
||||
if (faIcon) {
|
||||
const iconHtml = icon(faIcon).html[0];
|
||||
container.innerHTML = iconHtml;
|
||||
} else {
|
||||
console.warn(`FontAwesome icon not found: ${name}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error updating icon ${effectiveIcon}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up resources when element is removed
|
||||
async disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
|
||||
// Clear our references
|
||||
this.lastIcon = null;
|
||||
this.lastIconSize = null;
|
||||
this.lastColor = null;
|
||||
this.lastStrokeWidth = null;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-utility/dees-icon/index.ts
Normal file
1
ts_web/elements/00group-utility/dees-icon/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-icon.js';
|
||||
@@ -0,0 +1,46 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => {
|
||||
const onChanged = (e: CustomEvent) => {
|
||||
// find the demo wrapper and update the 'changed' log inside it
|
||||
const wrapper = (e.target as HTMLElement).closest('.demoWrapper');
|
||||
const el = wrapper?.querySelector('#changed');
|
||||
if (el) el.textContent = `search-changed: ${e.detail.value}`;
|
||||
};
|
||||
const onSubmit = (e: CustomEvent) => {
|
||||
// find the demo wrapper and update the 'submitted' log inside it
|
||||
const wrapper = (e.target as HTMLElement).closest('.demoWrapper');
|
||||
const el = wrapper?.querySelector('#submitted');
|
||||
if (el) el.textContent = `search-submit: ${e.detail.value}`;
|
||||
};
|
||||
return html`
|
||||
<style>
|
||||
.demoWrapper {
|
||||
display: block;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
background: #888888;
|
||||
}
|
||||
.logs {
|
||||
padding: 16px;
|
||||
width: 600px;
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
}
|
||||
.logs div {
|
||||
margin: 4px 0;
|
||||
}
|
||||
</style>
|
||||
<div class="demoWrapper">
|
||||
<dees-searchbar
|
||||
@search-changed=${onChanged}
|
||||
@search-submit=${onSubmit}
|
||||
></dees-searchbar>
|
||||
<div class="logs">
|
||||
<div id="changed">search-changed:</div>
|
||||
<div id="submitted">search-submit:</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
162
ts_web/elements/00group-utility/dees-searchbar/dees-searchbar.ts
Normal file
162
ts_web/elements/00group-utility/dees-searchbar/dees-searchbar.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
property,
|
||||
html,
|
||||
cssManager,
|
||||
unsafeCSS,
|
||||
css,
|
||||
type TemplateResult,
|
||||
domtools,
|
||||
query,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as colors from '../../00colors.js';
|
||||
import { demoFunc } from './dees-searchbar.demo.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-searchbar': DeesSearchbar;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-searchbar')
|
||||
export class DeesSearchbar extends DeesElement {
|
||||
// DEMO
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Utility'];
|
||||
|
||||
// STATIC
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
padding: 40px;
|
||||
font-family: Dees Sans;
|
||||
display: block;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#000000')};
|
||||
}
|
||||
|
||||
.searchboxContainer {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
max-width: 800px;
|
||||
background: ${cssManager.bdTheme('#00000015', '#ffffff15')};
|
||||
--boxHeight: 60px;
|
||||
height: var(--boxHeight);
|
||||
border-radius: var(--boxHeight);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 140px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#00000015', '#ffffff20')};
|
||||
}
|
||||
|
||||
input {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: none;
|
||||
color: ${cssManager.bdTheme('#000000', '#eeeeeb')};
|
||||
padding-left: 25px;
|
||||
margin-right: -8px;
|
||||
outline: none;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.searchButton {
|
||||
--buttonPadding: 8px;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#000000')};
|
||||
color: ${cssManager.bdTheme('#000000', '#eeeeeb')};
|
||||
line-height: calc(var(--boxHeight) - (var(--buttonPadding) * 2));
|
||||
border-radius: var(--boxHeight);
|
||||
transform: scale(1) ;
|
||||
transform-origin: 50% 50%;
|
||||
text-align: center;
|
||||
|
||||
transition: transform 0.1s, background 0.1s;
|
||||
margin-right: var(--buttonPadding);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.searchButton:hover {
|
||||
color: #fff;
|
||||
background: ${cssManager.bdTheme(colors.bright.blue, colors.dark.blue)};
|
||||
}
|
||||
|
||||
.searchButton:active {
|
||||
color: #fff;
|
||||
background: ${cssManager.bdTheme(colors.bright.blueActive, colors.dark.blueActive)};
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin: auto;
|
||||
max-width: 800px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// INSTANCE
|
||||
|
||||
@property()
|
||||
accessor filters = [];
|
||||
|
||||
|
||||
public searchInput!: HTMLInputElement;
|
||||
public searchButton!: HTMLElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<div class="searchboxContainer">
|
||||
<input type="text" placeholder="Your Skills (e.g. TypeScript, Rust, Projectmanagement)" />
|
||||
<div class="searchButton">Search -></div>
|
||||
</div>
|
||||
${this.filters.length > 0 ? html`
|
||||
<div class="filters">
|
||||
<dees-heading level="hr-small">Filters</dees-heading>
|
||||
<dees-input-dropdown .label=${'location'}></dees-input-dropdown>
|
||||
</div>
|
||||
` : html``}
|
||||
`;
|
||||
}
|
||||
/**
|
||||
* Lifecycle: after first render, wire up events for input and submit actions
|
||||
*/
|
||||
public firstUpdated(): void {
|
||||
// dispatch change on each input
|
||||
this.searchInput.addEventListener('input', () => {
|
||||
this.dispatchEvent(new CustomEvent('search-changed', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { value: this.searchInput.value }
|
||||
}));
|
||||
});
|
||||
// submit on Enter key
|
||||
this.searchInput.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
this._dispatchSubmit();
|
||||
}
|
||||
});
|
||||
// submit on button click
|
||||
this.searchButton.addEventListener('click', () => this._dispatchSubmit());
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a submit event with the current search value
|
||||
*/
|
||||
private _dispatchSubmit(): void {
|
||||
this.dispatchEvent(new CustomEvent('search-submit', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: { value: this.searchInput.value }
|
||||
}));
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-utility/dees-searchbar/index.ts
Normal file
1
ts_web/elements/00group-utility/dees-searchbar/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-searchbar.js';
|
||||
275
ts_web/elements/00group-utility/dees-theme/dees-theme.demo.ts
Normal file
275
ts_web/elements/00group-utility/dees-theme/dees-theme.demo.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
padding: 48px;
|
||||
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')};
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.section-description {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.token-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.token-item {
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.token-name {
|
||||
font-family: 'Intel One Mono', monospace;
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.token-value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
}
|
||||
|
||||
.spacing-demo {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.spacing-box {
|
||||
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.radius-demo {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.radius-box {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.shadow-demo {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.shadow-box {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#27272a')};
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
|
||||
}
|
||||
|
||||
.height-demo {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.height-box {
|
||||
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
width: 120px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-container">
|
||||
<dees-theme>
|
||||
<div class="section">
|
||||
<div class="section-title">Spacing Scale</div>
|
||||
<div class="section-description">
|
||||
CSS variables: --dees-spacing-xs through --dees-spacing-3xl
|
||||
</div>
|
||||
<div class="spacing-demo">
|
||||
<div>
|
||||
<div class="spacing-box" style="width: var(--dees-spacing-xs); height: var(--dees-spacing-xs);"></div>
|
||||
<div class="token-name">xs (4px)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="spacing-box" style="width: var(--dees-spacing-sm); height: var(--dees-spacing-sm);"></div>
|
||||
<div class="token-name">sm (8px)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="spacing-box" style="width: var(--dees-spacing-md); height: var(--dees-spacing-md);"></div>
|
||||
<div class="token-name">md (12px)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="spacing-box" style="width: var(--dees-spacing-lg); height: var(--dees-spacing-lg);"></div>
|
||||
<div class="token-name">lg (16px)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="spacing-box" style="width: var(--dees-spacing-xl); height: var(--dees-spacing-xl);"></div>
|
||||
<div class="token-name">xl (24px)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="spacing-box" style="width: var(--dees-spacing-2xl); height: var(--dees-spacing-2xl);"></div>
|
||||
<div class="token-name">2xl (32px)</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="spacing-box" style="width: var(--dees-spacing-3xl); height: var(--dees-spacing-3xl);"></div>
|
||||
<div class="token-name">3xl (48px)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Border Radius Scale</div>
|
||||
<div class="section-description">
|
||||
CSS variables: --dees-radius-xs through --dees-radius-full
|
||||
</div>
|
||||
<div class="radius-demo">
|
||||
<div>
|
||||
<div class="radius-box" style="border-radius: var(--dees-radius-xs);">xs</div>
|
||||
<div class="token-name">2px</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="radius-box" style="border-radius: var(--dees-radius-sm);">sm</div>
|
||||
<div class="token-name">4px</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="radius-box" style="border-radius: var(--dees-radius-md);">md</div>
|
||||
<div class="token-name">6px</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="radius-box" style="border-radius: var(--dees-radius-lg);">lg</div>
|
||||
<div class="token-name">8px</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="radius-box" style="border-radius: var(--dees-radius-xl);">xl</div>
|
||||
<div class="token-name">12px</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="radius-box" style="border-radius: var(--dees-radius-full);">full</div>
|
||||
<div class="token-name">999px</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Shadow Elevation Scale</div>
|
||||
<div class="section-description">
|
||||
CSS variables: --dees-shadow-xs through --dees-shadow-lg
|
||||
</div>
|
||||
<div class="shadow-demo">
|
||||
<div>
|
||||
<div class="shadow-box" style="box-shadow: var(--dees-shadow-xs);">xs</div>
|
||||
<div class="token-name">minimal</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="shadow-box" style="box-shadow: var(--dees-shadow-sm);">sm</div>
|
||||
<div class="token-name">subtle</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="shadow-box" style="box-shadow: var(--dees-shadow-md);">md</div>
|
||||
<div class="token-name">medium</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="shadow-box" style="box-shadow: var(--dees-shadow-lg);">lg</div>
|
||||
<div class="token-name">prominent</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Control Height Scale</div>
|
||||
<div class="section-description">
|
||||
CSS variables: --dees-control-height-sm through --dees-control-height-xl
|
||||
</div>
|
||||
<div class="height-demo">
|
||||
<div>
|
||||
<div class="height-box" style="height: var(--dees-control-height-sm);">sm</div>
|
||||
<div class="token-name">32px</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="height-box" style="height: var(--dees-control-height-md);">md</div>
|
||||
<div class="token-name">36px</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="height-box" style="height: var(--dees-control-height-lg);">lg</div>
|
||||
<div class="token-name">40px</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="height-box" style="height: var(--dees-control-height-xl);">xl</div>
|
||||
<div class="token-name">48px</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="section-title">Transition Durations</div>
|
||||
<div class="section-description">
|
||||
CSS variables: --dees-transition-fast through --dees-transition-slower
|
||||
</div>
|
||||
<div class="token-grid">
|
||||
<div class="token-item">
|
||||
<div class="token-name">--dees-transition-fast</div>
|
||||
<div class="token-value">0.1s</div>
|
||||
</div>
|
||||
<div class="token-item">
|
||||
<div class="token-name">--dees-transition-default</div>
|
||||
<div class="token-value">0.15s</div>
|
||||
</div>
|
||||
<div class="token-item">
|
||||
<div class="token-name">--dees-transition-slow</div>
|
||||
<div class="token-value">0.2s</div>
|
||||
</div>
|
||||
<div class="token-item">
|
||||
<div class="token-name">--dees-transition-slower</div>
|
||||
<div class="token-value">0.3s</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dees-theme>
|
||||
</div>
|
||||
`;
|
||||
225
ts_web/elements/00group-utility/dees-theme/dees-theme.ts
Normal file
225
ts_web/elements/00group-utility/dees-theme/dees-theme.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import {
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
property,
|
||||
customElement,
|
||||
html,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import {
|
||||
type ITheme,
|
||||
type IThemeColors,
|
||||
type IThemeSpacing,
|
||||
type IThemeRadius,
|
||||
type IThemeShadows,
|
||||
type IThemeTransitions,
|
||||
type IThemeControlHeights,
|
||||
themeDefaults,
|
||||
themeDefaultStyles,
|
||||
} from '../../00theme.js';
|
||||
|
||||
import { demoFunc } from './dees-theme.demo.js';
|
||||
|
||||
/**
|
||||
* A theme provider component that wraps children and provides CSS custom properties.
|
||||
* Can be used at the app root or around specific sections to customize theming.
|
||||
*
|
||||
* Usage:
|
||||
* ```html
|
||||
* <dees-theme>
|
||||
* <my-app></my-app>
|
||||
* </dees-theme>
|
||||
* ```
|
||||
*
|
||||
* With custom overrides:
|
||||
* ```html
|
||||
* <dees-theme .customSpacing=${{ lg: '20px' }}>
|
||||
* <my-section></my-section>
|
||||
* </dees-theme>
|
||||
* ```
|
||||
*/
|
||||
@customElement('dees-theme')
|
||||
export class DeesTheme extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Utility'];
|
||||
|
||||
// ============================================
|
||||
// Properties for theme overrides
|
||||
// ============================================
|
||||
|
||||
@property({ type: Object })
|
||||
accessor customSpacing: Partial<IThemeSpacing> | null = null;
|
||||
|
||||
@property({ type: Object })
|
||||
accessor customRadius: Partial<IThemeRadius> | null = null;
|
||||
|
||||
@property({ type: Object })
|
||||
accessor customShadows: Partial<IThemeShadows> | null = null;
|
||||
|
||||
@property({ type: Object })
|
||||
accessor customTransitions: Partial<IThemeTransitions> | null = null;
|
||||
|
||||
@property({ type: Object })
|
||||
accessor customControlHeights: Partial<IThemeControlHeights> | null = null;
|
||||
|
||||
// ============================================
|
||||
// Styles
|
||||
// ============================================
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// Render
|
||||
// ============================================
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
${this.generateCustomStyles()}
|
||||
</style>
|
||||
<slot></slot>
|
||||
`;
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Private Methods
|
||||
// ============================================
|
||||
|
||||
private generateCustomStyles(): string {
|
||||
const styles: string[] = [':host {'];
|
||||
|
||||
// Custom spacing
|
||||
if (this.customSpacing) {
|
||||
for (const [key, value] of Object.entries(this.customSpacing)) {
|
||||
if (value) {
|
||||
styles.push(` --dees-spacing-${key}: ${value};`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom radius
|
||||
if (this.customRadius) {
|
||||
for (const [key, value] of Object.entries(this.customRadius)) {
|
||||
if (value) {
|
||||
styles.push(` --dees-radius-${key}: ${value};`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom shadows
|
||||
if (this.customShadows) {
|
||||
for (const [key, value] of Object.entries(this.customShadows)) {
|
||||
if (value) {
|
||||
styles.push(` --dees-shadow-${key}: ${value};`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom transitions
|
||||
if (this.customTransitions) {
|
||||
for (const [key, value] of Object.entries(this.customTransitions)) {
|
||||
if (value) {
|
||||
const cssKey = key === 'default' ? 'default' : key;
|
||||
styles.push(` --dees-transition-${cssKey}: ${value};`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom control heights
|
||||
if (this.customControlHeights) {
|
||||
for (const [key, value] of Object.entries(this.customControlHeights)) {
|
||||
if (value) {
|
||||
styles.push(` --dees-control-height-${key}: ${value};`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
styles.push('}');
|
||||
return styles.join('\n');
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// Public API Methods
|
||||
// ============================================
|
||||
|
||||
/**
|
||||
* Set a spacing value dynamically
|
||||
*/
|
||||
public setSpacing(key: keyof IThemeSpacing, value: string): void {
|
||||
this.customSpacing = { ...this.customSpacing, [key]: value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a radius value dynamically
|
||||
*/
|
||||
public setRadius(key: keyof IThemeRadius, value: string): void {
|
||||
this.customRadius = { ...this.customRadius, [key]: value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a shadow value dynamically
|
||||
*/
|
||||
public setShadow(key: keyof IThemeShadows, value: string): void {
|
||||
this.customShadows = { ...this.customShadows, [key]: value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a transition value dynamically
|
||||
*/
|
||||
public setTransition(key: keyof IThemeTransitions, value: string): void {
|
||||
this.customTransitions = { ...this.customTransitions, [key]: value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a control height value dynamically
|
||||
*/
|
||||
public setControlHeight(key: keyof IThemeControlHeights, value: string): void {
|
||||
this.customControlHeights = { ...this.customControlHeights, [key]: value };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current theme configuration (defaults + overrides)
|
||||
*/
|
||||
public getTheme(): ITheme {
|
||||
return {
|
||||
colors: themeDefaults.colors,
|
||||
spacing: { ...themeDefaults.spacing, ...this.customSpacing },
|
||||
radius: { ...themeDefaults.radius, ...this.customRadius },
|
||||
shadows: { ...themeDefaults.shadows, ...this.customShadows },
|
||||
transitions: { ...themeDefaults.transitions, ...this.customTransitions },
|
||||
controlHeights: { ...themeDefaults.controlHeights, ...this.customControlHeights },
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all custom overrides to defaults
|
||||
*/
|
||||
public resetToDefaults(): void {
|
||||
this.customSpacing = null;
|
||||
this.customRadius = null;
|
||||
this.customShadows = null;
|
||||
this.customTransitions = null;
|
||||
this.customControlHeights = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a complete theme object
|
||||
*/
|
||||
public applyTheme(theme: Partial<ITheme>): void {
|
||||
if (theme.spacing) this.customSpacing = theme.spacing;
|
||||
if (theme.radius) this.customRadius = theme.radius;
|
||||
if (theme.shadows) this.customShadows = theme.shadows;
|
||||
if (theme.transitions) this.customTransitions = theme.transitions;
|
||||
if (theme.controlHeights) this.customControlHeights = theme.controlHeights;
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-utility/dees-theme/index.ts
Normal file
1
ts_web/elements/00group-utility/dees-theme/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-theme.js';
|
||||
@@ -0,0 +1,10 @@
|
||||
import { html } from '@design.estate/dees-element';
|
||||
|
||||
import { DeesUpdater } from '../dees-updater/dees-updater.js';
|
||||
|
||||
export const demoFunc = async () => {
|
||||
const updater = await DeesUpdater.createAndShow();
|
||||
setTimeout(async () => {
|
||||
await updater.destroy();
|
||||
}, 10000);
|
||||
}
|
||||
114
ts_web/elements/00group-utility/dees-updater/dees-updater.ts
Normal file
114
ts_web/elements/00group-utility/dees-updater/dees-updater.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
type TemplateResult,
|
||||
html,
|
||||
property,
|
||||
type CSSResult,
|
||||
domtools,
|
||||
} from '@design.estate/dees-element';
|
||||
import { demoFunc } from './dees-updater.demo.js';
|
||||
|
||||
import '../../00group-overlay/dees-windowlayer/dees-windowlayer.js';
|
||||
import { css, cssManager } from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-updater': DeesUpdater;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-updater')
|
||||
export class DeesUpdater extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Utility'];
|
||||
|
||||
public static async createAndShow() {
|
||||
const updater = new DeesUpdater();
|
||||
document.body.appendChild(updater);
|
||||
return updater;
|
||||
}
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
accessor currentVersion: string;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
accessor updatedVersion: string;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
domtools.elementBasic.setup();
|
||||
}
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
.modalContainer {
|
||||
will-change: transform;
|
||||
position: relative;
|
||||
background: ${cssManager.bdTheme('#eeeeeb', '#222')};
|
||||
max-width: 800px;
|
||||
border-radius: 8px;
|
||||
border-top: 1px solid ${cssManager.bdTheme('#eeeeeb', '#333')};
|
||||
}
|
||||
|
||||
.headingContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px 40px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: none;
|
||||
font-size: 20px;
|
||||
color: ${cssManager.bdTheme('#333', '#fff')};
|
||||
margin-left: 20px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.buttonContainer {
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
<dees-windowlayer
|
||||
@clicked="${this.windowLayerClicked}"
|
||||
.options=${{
|
||||
blur: true,
|
||||
}}
|
||||
>
|
||||
<div class="modalContainer">
|
||||
<div class="headingContainer">
|
||||
<dees-spinner .size=${60}></dees-spinner>
|
||||
<h1>Updating the application...</h1>
|
||||
</div>
|
||||
<div class="progress">
|
||||
<dees-progressbar .progress=${0.5}></dees-progressbar>
|
||||
</div>
|
||||
<div class="buttonContainer">
|
||||
<dees-button>More info</dees-button>
|
||||
<dees-button>Changelog</dees-button>
|
||||
</div>
|
||||
</div> </dees-windowlayer
|
||||
>>
|
||||
`;
|
||||
}
|
||||
|
||||
public async destroy() {
|
||||
this.parentElement.removeChild(this);
|
||||
}
|
||||
|
||||
private windowLayerClicked() {}
|
||||
}
|
||||
1
ts_web/elements/00group-utility/dees-updater/index.ts
Normal file
1
ts_web/elements/00group-utility/dees-updater/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-updater.js';
|
||||
@@ -0,0 +1,103 @@
|
||||
import {
|
||||
customElement,
|
||||
DeesElement,
|
||||
domtools,
|
||||
type TemplateResult,
|
||||
html,
|
||||
property,
|
||||
type CSSResult,
|
||||
state,
|
||||
css,
|
||||
cssManager,
|
||||
} from '@design.estate/dees-element';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-windowcontrols': DeesWindowControls;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement('dees-windowcontrols')
|
||||
export class DeesWindowControls extends DeesElement {
|
||||
// STATIC
|
||||
public static demo = () => html`<dees-windowcontrols></dees-windowcontrols>`;
|
||||
public static demoGroups = ['Utility'];
|
||||
|
||||
// Instance
|
||||
@property({
|
||||
reflect: true,
|
||||
})
|
||||
accessor type: 'mac' | 'linux' | 'windows' = 'mac';
|
||||
|
||||
@property({
|
||||
reflect: true,
|
||||
})
|
||||
accessor position: 'left' | 'right' = 'left';
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
position: relative;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.windowControls {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.windowControls div {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
background: #222222;
|
||||
}
|
||||
|
||||
.windowControls div.close {
|
||||
background: #ff5f57;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.windowControls div.toDock {
|
||||
background: #ffbd2e;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.windowControls div.minMax {
|
||||
background: #27c93f;
|
||||
}
|
||||
|
||||
.windowControls div:hover {
|
||||
background: #333333;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
public render(): TemplateResult {
|
||||
return html`
|
||||
${(this.type === 'mac' && this.position === 'left') ||
|
||||
((this.type === 'linux' || this.type === 'windows') && this.position === 'right')
|
||||
? html`
|
||||
<div class="windowControls">
|
||||
<div class="close"></div>
|
||||
<div class="toDock"></div>
|
||||
<div class="minMax"></div>
|
||||
</div>
|
||||
`
|
||||
: html``}
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './dees-windowcontrols.js';
|
||||
6
ts_web/elements/00group-utility/index.ts
Normal file
6
ts_web/elements/00group-utility/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// Utility Components
|
||||
export * from './dees-icon/index.js';
|
||||
export * from './dees-searchbar/index.js';
|
||||
export * from './dees-theme/index.js';
|
||||
export * from './dees-updater/index.js';
|
||||
export * from './dees-windowcontrols/index.js';
|
||||
Reference in New Issue
Block a user