@design.estate/dees-catalog 3.96.0 → 3.96.2

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.
@@ -735,22 +735,24 @@ export class DeesTable<T> extends DeesElement {
735
735
  return html`
736
736
  <td class="actionsCol">
737
737
  <div class="actionsContainer">
738
- ${inRowActions.map(
739
- (actionArg) => html`
740
- <div
741
- class="action"
742
- @click=${() =>
743
- actionArg.actionFunc({
744
- item: itemArg,
745
- table: this,
746
- })}
747
- >
748
- ${actionArg.iconName
749
- ? html` <dees-icon .icon=${actionArg.iconName}></dees-icon> `
750
- : actionArg.name}
751
- </div>
752
- `
753
- )}
738
+ ${inRowActions
739
+ .filter((actionArg) => this.isActionRelevant(actionArg, itemArg))
740
+ .map(
741
+ (actionArg) => html`
742
+ <div
743
+ class="action"
744
+ @click=${() =>
745
+ actionArg.actionFunc({
746
+ item: itemArg,
747
+ table: this,
748
+ })}
749
+ >
750
+ ${actionArg.iconName
751
+ ? html` <dees-icon .icon=${actionArg.iconName}></dees-icon> `
752
+ : actionArg.name}
753
+ </div>
754
+ `
755
+ )}
754
756
  </div>
755
757
  </td>
756
758
  `;
@@ -2119,7 +2121,11 @@ export class DeesTable<T> extends DeesElement {
2119
2121
  this.startEditing(cell.item, cell.col);
2120
2122
  return;
2121
2123
  }
2122
- const dblAction = this.dataActions.find((a) => a.type?.includes('doubleClick'));
2124
+ const dblAction = this.dataActions.find(
2125
+ (action) =>
2126
+ action.type?.includes('doubleClick')
2127
+ && this.isActionRelevant(action, cell.item)
2128
+ );
2123
2129
  if (dblAction) dblAction.actionFunc({ item: cell.item, table: this });
2124
2130
  };
2125
2131
 
@@ -2145,16 +2151,19 @@ export class DeesTable<T> extends DeesElement {
2145
2151
  this.emitSelectionChange();
2146
2152
  this.requestUpdate();
2147
2153
  }
2148
- const userItems: plugins.tsclass.website.IMenuItem[] = this.getActionsForType('contextmenu').map(
2149
- (action) => ({
2150
- name: action.name,
2151
- iconName: action.iconName as any,
2152
- action: async () => {
2153
- await action.actionFunc({ item, table: this });
2154
- return null;
2155
- },
2156
- })
2157
- );
2154
+ const userItems: plugins.tsclass.website.IMenuItem[] = this
2155
+ .getActionsForType('contextmenu')
2156
+ .filter((action) => this.isActionRelevant(action, item))
2157
+ .map(
2158
+ (action) => ({
2159
+ name: action.name,
2160
+ iconName: action.iconName as any,
2161
+ action: async () => {
2162
+ await action.actionFunc({ item, table: this });
2163
+ return null;
2164
+ },
2165
+ })
2166
+ );
2158
2167
  const defaultItems: plugins.tsclass.website.IMenuItem[] = [
2159
2168
  {
2160
2169
  name:
@@ -2333,6 +2342,13 @@ export class DeesTable<T> extends DeesElement {
2333
2342
  );
2334
2343
  }
2335
2344
 
2345
+ private isActionRelevant(actionArg: ITableAction<T>, itemArg: T): boolean {
2346
+ return (
2347
+ !actionArg.actionRelevancyCheckFunc
2348
+ || actionArg.actionRelevancyCheckFunc(itemArg)
2349
+ );
2350
+ }
2351
+
2336
2352
  getActionsForType(typeArg: ITableAction['type'][0]) {
2337
2353
  const actions: ITableAction[] = [];
2338
2354
  for (const action of this.dataActions) {
@@ -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 = [
@@ -213,11 +203,14 @@ export class DeesIcon extends DeesElement {
213
203
  pointer-events: none;
214
204
  }
215
205
 
216
- /* Improve rendering performance */
217
- #iconContainer svg {
218
- display: block;
219
- height: 100%;
220
- width: 100%;
206
+ #iconContainer {
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%;
221
214
  will-change: transform; /* Helps with animations */
222
215
  contain: strict; /* Performance optimization */
223
216
  }
@@ -244,11 +237,11 @@ export class DeesIcon extends DeesElement {
244
237
  }
245
238
 
246
239
  const effectiveIcon = this.getEffectiveIcon();
247
- const container = this.shadowRoot?.querySelector('#iconContainer');
240
+ const container = this.shadowRoot?.querySelector('#iconContainer') as HTMLElement | null;
248
241
  if (!container) return;
249
242
 
250
243
  if (!effectiveIcon) {
251
- container.innerHTML = '';
244
+ this.clearIconContainer(container);
252
245
  this.lastIcon = effectiveIcon;
253
246
  this.lastIconSize = this.iconSize;
254
247
  this.lastColor = this.color;
@@ -276,45 +269,16 @@ export class DeesIcon extends DeesElement {
276
269
  const { type, name } = this.parseIconString(effectiveIcon);
277
270
 
278
271
  if (type === 'lucide') {
279
- // For Lucide, use direct DOM manipulation as shown in the docs
280
- // This approach avoids HTML string issues
281
- container.innerHTML = ''; // Clear container
282
-
283
- try {
284
- // Convert to PascalCase (handles kebab-case Lucide ids like "file-text")
285
- const pascalCaseName = lucideNameToPascalCase(name);
286
-
287
- if ((lucideIcons as any)[pascalCaseName]) {
288
- // Use the documented pattern from Lucide docs
289
- const svgElement = createElement((lucideIcons as any)[pascalCaseName], {
290
- color: this.color,
291
- size: this.iconSize,
292
- strokeWidth: this.strokeWidth
293
- });
294
-
295
- if (svgElement) {
296
- // Directly append the element
297
- container.appendChild(svgElement);
298
- return; // Exit early since we've added the element
299
- }
300
- }
301
-
302
- // If we reach here, something went wrong
303
- throw new Error(`Could not create element for ${pascalCaseName}`);
304
- } catch (error) {
305
- console.error(`Error rendering Lucide icon:`, error);
306
-
307
- // Fall back to the string-based approach
308
- const iconHtml = this.renderLucideIcon(name);
309
- if (iconHtml) {
310
- container.innerHTML = iconHtml;
311
- }
312
- }
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);
313
276
  } else {
314
- container.innerHTML = '';
277
+ this.clearIconContainer(container);
315
278
  console.error(`dees-icon: fa:${name} is no longer supported. Use icon="lucide:${name}" or another Lucide icon.`);
316
279
  }
317
280
  } catch (error) {
281
+ this.clearIconContainer(container);
318
282
  console.error(`Error updating icon ${effectiveIcon}:`, error);
319
283
  }
320
284
  }
@@ -37,6 +37,8 @@ export interface IThemeColors {
37
37
  bgTertiary: string;
38
38
  /** Recessed shell/window base; content surfaces sit elevated above it (both themes). */
39
39
  bgCanvas: string;
40
+ /** Opaque system-label base used when opacity must be composited at element level. */
41
+ labelBase?: string;
40
42
  textPrimary: string;
41
43
  textSecondary: string;
42
44
  textMuted: string;
@@ -226,6 +228,7 @@ export const themeDefaults: ITheme = {
226
228
  bgSecondary: '#f2f2f7', // secondarySystemBackground / grouped
227
229
  bgTertiary: '#e9e9eb', // tertiary recess (headers, hovers)
228
230
  bgCanvas: '#f2f2f7', // recessed shell base (systemGroupedBackground)
231
+ labelBase: '#3c3c43', // opaque system label base
229
232
  textPrimary: '#1d1d1f', // near-black label
230
233
  textSecondary: 'rgba(60, 60, 67, 0.60)', // secondaryLabel
231
234
  textMuted: 'rgba(60, 60, 67, 0.30)', // tertiaryLabel
@@ -258,6 +261,7 @@ export const themeDefaults: ITheme = {
258
261
  bgSecondary: '#1c1c1e', // secondarySystemBackground / systemGray6 — cards, panels
259
262
  bgTertiary: '#2c2c2e', // tertiarySystemBackground / systemGray5 — headers, hovers
260
263
  bgCanvas: '#000000', // recessed shell base (true black, matches systemBackground)
264
+ labelBase: '#ebebf5', // opaque system label base
261
265
  textPrimary: '#f5f5f7', // near-white label
262
266
  textSecondary: 'rgba(235, 235, 245, 0.60)', // secondaryLabel
263
267
  textMuted: 'rgba(235, 235, 245, 0.30)', // tertiaryLabel
@@ -524,6 +528,10 @@ export const themeDefaultStyles: CSSResult = css`
524
528
  /* ========================================
525
529
  * Colors — Text / Label tiers
526
530
  * ======================================== */
531
+ --dees-color-label-base: ${cssManager.bdTheme(l.labelBase || '#3c3c43', d.labelBase || '#ebebf5')};
532
+ --dees-label-opacity-secondary: 0.6;
533
+ --dees-label-opacity-muted: 0.3;
534
+ --dees-label-opacity-quaternary: 0.18;
527
535
  --dees-color-text-primary: ${cssManager.bdTheme(l.textPrimary, d.textPrimary)};
528
536
  --dees-color-text-secondary: ${cssManager.bdTheme(l.textSecondary, d.textSecondary)};
529
537
  --dees-color-text-muted: ${cssManager.bdTheme(l.textMuted, d.textMuted)};