@design.estate/dees-catalog 3.96.1 → 3.96.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -823,14 +823,13 @@ export class DeesStatsGrid extends DeesElement {
823
823
  <div class="grid-title"></div>
824
824
  <div class="grid-actions">
825
825
  ${this.gridActions.map(action => html`
826
- <dees-button
826
+ <dees-button
827
827
  @clicked=${() => this.handleGridAction(action)}
828
828
  type="outline"
829
829
  size="sm"
830
- >
831
- ${action.iconName ? html`<dees-icon .icon=${action.iconName} size="small"></dees-icon>` : ''}
832
- ${action.name}
833
- </dees-button>
830
+ .text=${action.name}
831
+ .icon=${action.iconName || ''}
832
+ ></dees-button>
834
833
  `)}
835
834
  </div>
836
835
  </div>
@@ -1249,4 +1248,4 @@ export class DeesStatsGrid extends DeesElement {
1249
1248
  // open an empty menu.)
1250
1249
  DeesContextmenu.openContextMenuWithOptions(event, tile.actions as any);
1251
1250
  }
1252
- }
1251
+ }
@@ -515,7 +515,6 @@ export const tableStyles: CSSResult[] = [
515
515
  height: var(--dees-control-height-sm);
516
516
  border-radius: var(--dees-radius-sm);
517
517
  color: var(--dees-color-text-muted);
518
- --dees-icon-opacity: var(--dees-label-opacity-muted);
519
518
  cursor: pointer;
520
519
  transition: all var(--dees-transition-fast) var(--dees-ease-standard);
521
520
  }
@@ -523,7 +522,6 @@ export const tableStyles: CSSResult[] = [
523
522
  .action:hover {
524
523
  background: var(--dees-color-badge-default-bg);
525
524
  color: var(--dees-color-text-primary);
526
- --dees-icon-opacity: 1;
527
525
  }
528
526
 
529
527
  .action:active {
@@ -531,15 +529,10 @@ export const tableStyles: CSSResult[] = [
531
529
  }
532
530
 
533
531
  .action dees-icon {
534
- color: var(--dees-color-label-base);
535
532
  width: 16px;
536
533
  height: 16px;
537
534
  }
538
535
 
539
- .action:hover dees-icon {
540
- color: var(--dees-color-text-primary);
541
- }
542
-
543
536
  .footer {
544
537
  display: flex;
545
538
  align-items: center;
@@ -89,8 +89,8 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
89
89
 
90
90
  .option {
91
91
  position: relative;
92
- display: flex;
93
- align-items: center;
92
+ display: grid;
93
+ place-items: center;
94
94
  height: 100%;
95
95
  padding: 0 var(--dees-spacing-lg);
96
96
  border-radius: var(--dees-radius-sm);
@@ -104,6 +104,21 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
104
104
  z-index: 2;
105
105
  }
106
106
 
107
+ .option-label,
108
+ .option::after {
109
+ grid-area: 1 / 1;
110
+ }
111
+
112
+ /* Reserve the selected label's semibold width in every state so moving
113
+ the thumb never changes the segmented control's intrinsic width. */
114
+ .option::after {
115
+ content: attr(data-option);
116
+ visibility: hidden;
117
+ font-weight: 600;
118
+ pointer-events: none;
119
+ user-select: none;
120
+ }
121
+
107
122
  /* Hairline separators between unselected segments; they fade out next to
108
123
  the thumb. */
109
124
  .option::before {
@@ -197,8 +212,9 @@ export class DeesInputMultitoggle extends DeesInputBase<DeesInputMultitoggle> {
197
212
  index === 0 || isSelected || index - 1 === selectedIndex;
198
213
  return html`<div
199
214
  class="option ${isSelected ? 'selected' : ''} ${hideSeparator ? 'no-sep' : ''}"
215
+ data-option=${option}
200
216
  @click=${() => this.handleSelection(option)}
201
- >${option}</div>`;
217
+ ><span class="option-label">${option}</span></div>`;
202
218
  })}
203
219
  </div>
204
220
  </div>
@@ -373,6 +373,7 @@ export class DeesSimpleAppDash extends DeesElement {
373
373
  bottom: 24px;
374
374
  width: calc(100% - 240px);
375
375
  overflow: auto;
376
+ scrollbar-gutter: stable;
376
377
  background: var(--dees-color-bg-canvas);
377
378
  overscroll-behavior: contain;
378
379
  }
@@ -37,16 +37,17 @@ export type IconWithPrefix = `lucide:${string}` | string;
37
37
  // Legacy type kept for consumers that still import it. `iconFA` itself is no longer supported.
38
38
  export type TIconKey = string;
39
39
 
40
- // Use a global static cache for all icons to reduce rendering
41
- const iconCache = new Map<string, string>();
40
+ // Cache opaque mask sources. Color is applied separately through currentColor,
41
+ // so theme and inherited text-color changes never require rebuilding geometry.
42
+ const iconMaskCache = new Map<string, string>();
42
43
 
43
44
  // Clear cache items occasionally to prevent memory leaks
44
45
  const MAX_CACHE_SIZE = 500;
45
46
  function limitCacheSize() {
46
- if (iconCache.size > MAX_CACHE_SIZE) {
47
+ if (iconMaskCache.size > MAX_CACHE_SIZE) {
47
48
  // Remove oldest entries (first 20% of items)
48
- const keysToDelete = Array.from(iconCache.keys()).slice(0, MAX_CACHE_SIZE / 5);
49
- keysToDelete.forEach(key => iconCache.delete(key));
49
+ const keysToDelete = Array.from(iconMaskCache.keys()).slice(0, MAX_CACHE_SIZE / 5);
50
+ keysToDelete.forEach(key => iconMaskCache.delete(key));
50
51
  }
51
52
  }
52
53
 
@@ -151,53 +152,42 @@ export class DeesIcon extends DeesElement {
151
152
  }
152
153
  }
153
154
 
154
- private renderLucideIcon(iconName: string): string {
155
- // Create a cache key based on all visual properties
156
- const cacheKey = `lucide:${iconName}:${this.iconSize}:${this.color}:${this.strokeWidth}`;
157
-
158
- // Check if we already have this icon in the cache
159
- if (iconCache.has(cacheKey)) {
160
- return iconCache.get(cacheKey) || '';
161
- }
155
+ private createLucideMaskImage(iconName: string): string {
156
+ const pascalCaseName = lucideNameToPascalCase(iconName);
157
+ const cacheKey = `lucide:${pascalCaseName}:${this.iconSize}:${this.strokeWidth}`;
158
+ const cachedMask = iconMaskCache.get(cacheKey);
159
+ if (cachedMask) return cachedMask;
162
160
 
163
- try {
164
- // Get the Pascal case icon name (Menu instead of menu, FileText instead of file-text)
165
- const pascalCaseName = lucideNameToPascalCase(iconName);
166
-
167
- // Check if the icon exists in lucideIcons
168
- if (!(lucideIcons as any)[pascalCaseName]) {
169
- console.warn(`Lucide icon '${pascalCaseName}' not found in lucideIcons object`);
170
- return '';
171
- }
161
+ if (!(lucideIcons as any)[pascalCaseName]) {
162
+ throw new Error(`Lucide icon '${pascalCaseName}' was not found`);
163
+ }
172
164
 
173
- // Use the exact pattern from Lucide documentation
174
- const svgElement = createElement((lucideIcons as any)[pascalCaseName], {
175
- color: this.color,
176
- size: this.iconSize,
177
- strokeWidth: this.strokeWidth
178
- });
179
-
180
- if (!svgElement) {
181
- console.warn(`createElement returned empty result for ${pascalCaseName}`);
182
- return '';
183
- }
184
-
185
- // Get the HTML
186
- const result = svgElement.outerHTML;
187
-
188
- // Cache the result for future use
189
- iconCache.set(cacheKey, result);
190
- limitCacheSize();
191
-
192
- return result;
193
- } catch (error) {
194
- console.error(`Error rendering Lucide icon ${iconName}:`, error);
195
-
196
- // Create a fallback SVG with the icon name
197
- 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">
198
- <text x="50%" y="50%" font-size="6" text-anchor="middle" dominant-baseline="middle" fill="${this.color}">${iconName}</text>
199
- </svg>`;
165
+ const svgElement = createElement((lucideIcons as any)[pascalCaseName], {
166
+ color: '#fff',
167
+ size: this.iconSize,
168
+ strokeWidth: this.strokeWidth,
169
+ });
170
+ if (!svgElement) {
171
+ throw new Error(`Lucide did not create an element for '${pascalCaseName}'`);
200
172
  }
173
+
174
+ // The mask must be completely opaque. The consumer's inherited color,
175
+ // including any alpha, is painted exactly once behind this geometry.
176
+ svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
177
+ svgElement.setAttribute('color', '#fff');
178
+ svgElement.setAttribute('stroke', '#fff');
179
+ svgElement.setAttribute('fill', 'none');
180
+
181
+ const maskImage = `url("data:image/svg+xml,${encodeURIComponent(svgElement.outerHTML)}")`;
182
+ iconMaskCache.set(cacheKey, maskImage);
183
+ limitCacheSize();
184
+ return maskImage;
185
+ }
186
+
187
+ private clearIconContainer(container: HTMLElement): void {
188
+ container.style.backgroundColor = 'transparent';
189
+ container.style.setProperty('mask-image', 'none');
190
+ container.style.setProperty('-webkit-mask-image', 'none');
201
191
  }
202
192
 
203
193
  public static styles = [
@@ -214,21 +204,13 @@ export class DeesIcon extends DeesElement {
214
204
  }
215
205
 
216
206
  #iconContainer {
217
- /*
218
- * Apply semantic emphasis to the complete icon, after its Lucide
219
- * strokes have been painted. This avoids darker overlap pixels that
220
- * occur when a translucent currentColor is composited path by path.
221
- *
222
- * Consumers keep the stroke color opaque and set --dees-icon-opacity.
223
- */
224
- opacity: var(--dees-icon-opacity, 1);
225
- }
226
-
227
- /* Improve rendering performance */
228
- #iconContainer svg {
229
- display: block;
230
- height: 100%;
231
- width: 100%;
207
+ background-color: currentColor;
208
+ mask-position: center;
209
+ mask-repeat: no-repeat;
210
+ mask-size: 100% 100%;
211
+ -webkit-mask-position: center;
212
+ -webkit-mask-repeat: no-repeat;
213
+ -webkit-mask-size: 100% 100%;
232
214
  will-change: transform; /* Helps with animations */
233
215
  contain: strict; /* Performance optimization */
234
216
  }
@@ -255,11 +237,11 @@ export class DeesIcon extends DeesElement {
255
237
  }
256
238
 
257
239
  const effectiveIcon = this.getEffectiveIcon();
258
- const container = this.shadowRoot?.querySelector('#iconContainer');
240
+ const container = this.shadowRoot?.querySelector('#iconContainer') as HTMLElement | null;
259
241
  if (!container) return;
260
242
 
261
243
  if (!effectiveIcon) {
262
- container.innerHTML = '';
244
+ this.clearIconContainer(container);
263
245
  this.lastIcon = effectiveIcon;
264
246
  this.lastIconSize = this.iconSize;
265
247
  this.lastColor = this.color;
@@ -287,45 +269,16 @@ export class DeesIcon extends DeesElement {
287
269
  const { type, name } = this.parseIconString(effectiveIcon);
288
270
 
289
271
  if (type === 'lucide') {
290
- // For Lucide, use direct DOM manipulation as shown in the docs
291
- // This approach avoids HTML string issues
292
- container.innerHTML = ''; // Clear container
293
-
294
- try {
295
- // Convert to PascalCase (handles kebab-case Lucide ids like "file-text")
296
- const pascalCaseName = lucideNameToPascalCase(name);
297
-
298
- if ((lucideIcons as any)[pascalCaseName]) {
299
- // Use the documented pattern from Lucide docs
300
- const svgElement = createElement((lucideIcons as any)[pascalCaseName], {
301
- color: this.color,
302
- size: this.iconSize,
303
- strokeWidth: this.strokeWidth
304
- });
305
-
306
- if (svgElement) {
307
- // Directly append the element
308
- container.appendChild(svgElement);
309
- return; // Exit early since we've added the element
310
- }
311
- }
312
-
313
- // If we reach here, something went wrong
314
- throw new Error(`Could not create element for ${pascalCaseName}`);
315
- } catch (error) {
316
- console.error(`Error rendering Lucide icon:`, error);
317
-
318
- // Fall back to the string-based approach
319
- const iconHtml = this.renderLucideIcon(name);
320
- if (iconHtml) {
321
- container.innerHTML = iconHtml;
322
- }
323
- }
272
+ const maskImage = this.createLucideMaskImage(name);
273
+ container.style.backgroundColor = this.color === 'currentColor' ? '' : this.color;
274
+ container.style.setProperty('mask-image', maskImage);
275
+ container.style.setProperty('-webkit-mask-image', maskImage);
324
276
  } else {
325
- container.innerHTML = '';
277
+ this.clearIconContainer(container);
326
278
  console.error(`dees-icon: fa:${name} is no longer supported. Use icon="lucide:${name}" or another Lucide icon.`);
327
279
  }
328
280
  } catch (error) {
281
+ this.clearIconContainer(container);
329
282
  console.error(`Error updating icon ${effectiveIcon}:`, error);
330
283
  }
331
284
  }