Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,43 @@
}
}
},
"dev-icon-adopted-stylesheets": {
"projectType": "application",
"root": "packages/components-dev/icon-adopted-stylesheets",
"sourceRoot": "packages/components-dev/icon-adopted-stylesheets",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"outputPath": {
"base": "dist/components-dev/icon-adopted-stylesheets"
},
"tsConfig": "packages/components-dev/icon-adopted-stylesheets/tsconfig.app.json",
"index": "packages/components-dev/index.html",
"styles": ["packages/components-dev/icon-adopted-stylesheets/global.scss"],
"assets": [
{
"glob": "**/*",
"input": "node_modules/@koobiq/icons/fonts",
"output": "assets/kbq-icons"
}
],
"polyfills": ["zone.js"],
"extractLicenses": false,
"sourceMap": true,
"optimization": false,
"namedChunks": true,
"browser": "packages/components-dev/icon-adopted-stylesheets/main.ts"
}
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"buildTarget": "dev-icon-adopted-stylesheets:build"
}
}
}
},
"dev-button-toggle": {
"projectType": "application",
"root": "packages/components-dev/button-toggle",
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@
"dev:highlight": "ng serve dev-highlight --port 3003",
"dev:hint": "ng serve dev-hint --port 3003",
"dev:icon": "ng serve dev-icon --port 3003",
"dev:icon-adopted-stylesheets": "ng serve dev-icon-adopted-stylesheets --port 3003",
"dev:icon-item": "ng serve dev-icon-item --port 3003",
"dev:inline-edit": "ng serve dev-inline-edit --port 3003",
"dev:input": "ng serve dev-input --port 3003",
Expand Down
38 changes: 38 additions & 0 deletions packages/components-dev/icon-adopted-stylesheets/global.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copy of packages/components-dev/main.scss WITHOUT the `@use '@koobiq/icons/fonts/kbq-icons'` line.
// The base icon CSS is intentionally NOT loaded as an author <style> — it is delivered at runtime
// via adoptedStyleSheets/<style> (see module.ts) to reproduce the bug and verify the fix.

@use '@koobiq/design-tokens/web/new/css-tokens.css';
@use '@koobiq/design-tokens/web/new/css-tokens-light.css';
@use '@koobiq/design-tokens/web/new/css-tokens-dark.css';

@use '../../components/core/styles/theming/prebuilt/theme';
@use '../../components/core/styles/visual/layout';
@use '../../components/core/styles/common/list';
@use '../../components/core/styles/common/tokens';

// Inter
@import '@fontsource/inter/400.css';
@import '@fontsource/inter/500.css';
@import '@fontsource/inter/600.css';
@import '@fontsource/inter/700.css';
@import '@fontsource/inter/400-italic.css';
@import '@fontsource/inter/500-italic.css';

// JetBrains Mono
@import '@fontsource/jetbrains-mono/400.css';
@import '@fontsource/jetbrains-mono/700.css';

@include layout.layouts-for-breakpoint();

body {
@include tokens.kbq-typography-level-to-styles-css-variables(typography, text-normal);

background: var(--kbq-background-bg);
color: var(--kbq-foreground-contrast);
margin: 0;
}

hr {
margin: var(--kbq-size-xxl) 0;
}
9 changes: 9 additions & 0 deletions packages/components-dev/icon-adopted-stylesheets/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { DevApp } from './module';

bootstrapApplication(DevApp, {
providers: [
provideAnimations()
]
}).catch((error) => console.error(error));
119 changes: 119 additions & 0 deletions packages/components-dev/icon-adopted-stylesheets/module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { DOCUMENT } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
inject,
OnDestroy,
OnInit,
signal,
ViewEncapsulation
} from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqBadgeColors, KbqBadgeModule } from '@koobiq/components/badge';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqComponentColors } from '@koobiq/components/core';
import { KbqFormFieldModule } from '@koobiq/components/form-field';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqInputModule } from '@koobiq/components/input';
import { KbqLinkModule } from '@koobiq/components/link';
import { KbqTagsModule } from '@koobiq/components/tags';
import { DevThemeToggle } from '../theme-toggle';

/** How the base icon CSS is delivered into the this.document. */
type IconMode = 'style' | 'adopted' | 'adopted-layer';

@Component({
selector: 'dev-app',
imports: [
FormsModule,
KbqIconModule,
KbqButtonModule,
KbqBadgeModule,
KbqLinkModule,
KbqTagsModule,
KbqFormFieldModule,
KbqInputModule,
DevThemeToggle
],
templateUrl: './template.html',
styleUrls: ['./styles.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class DevApp implements OnInit, OnDestroy {
private readonly document = inject(DOCUMENT);

readonly colors = KbqComponentColors;
readonly badgeColors = KbqBadgeColors;

/** Non-empty value so that kbq-password-toggle is visible (it is hidden when the control is empty). */
password = 'Secret123!';

/** Current icon CSS delivery mode. Defaults to `adopted` (no layer) so the bug is visible right away. */
readonly mode = signal<IconMode>('adopted');

/** kbq-icons.css text with its url() rewritten to the served asset path. */
private css = '';
private readonly sheet = new CSSStyleSheet();
private styleEl?: HTMLStyleElement;

ngOnInit(): void {
void this.loadCss();
}

private async loadCss(): Promise<void> {
const raw = await fetch('assets/kbq-icons/kbq-icons.css').then((response) => response.text());

// Both <style> and adopted sheets resolve url() against the document base URL,
// so rewrite the relative './kbq-icons.(ttf|woff)' to the served asset path.
this.css = raw.replaceAll('url("./', 'url("assets/kbq-icons/');

this.apply(this.mode());
}

ngOnDestroy(): void {
this.detach();
}

apply(mode: IconMode): void {
this.mode.set(mode);
this.detach();

if (!this.css) {
return;
}

switch (mode) {
// Baseline "as today": an author <style> at the start of <head> comes earlier in the cascade
// than component styles, so same-specificity overrides (rotate helpers, etc.) win.
case 'style': {
const element = this.document.createElement('style');

element.textContent = this.css;
this.document.head.prepend(element);
this.styleEl = element;
break;
}
// Bug: adopted sheets sort AFTER all author styles → the base `.kbq` (0-1-0) beats the
// `.kbq-icon-rotate_*`/`.kbq-icon-flip-*` helpers (also 0-1-0) → rotations/flips are lost.
case 'adopted': {
this.sheet.replaceSync(this.css);
this.document.adoptedStyleSheets = [...this.document.adoptedStyleSheets, this.sheet];
break;
}
// Fix: a layered rule loses to any unlayered author rule of the same origin BEFORE
// specificity and order are compared → component overrides win again.
case 'adopted-layer': {
this.sheet.replaceSync(`@layer kbq-icons {\n${this.css}\n}`);
this.document.adoptedStyleSheets = [...this.document.adoptedStyleSheets, this.sheet];
break;
}
}
}

private detach(): void {
this.document.adoptedStyleSheets = this.document.adoptedStyleSheets.filter((sheet) => sheet !== this.sheet);
this.styleEl?.remove();
this.styleEl = undefined;
}
}
86 changes: 86 additions & 0 deletions packages/components-dev/icon-adopted-stylesheets/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
.dev-toolbar {
position: sticky;
top: 0;
z-index: 10;

display: flex;
align-items: center;
justify-content: space-between;
gap: var(--kbq-size-l);

padding: var(--kbq-size-m);
margin-bottom: var(--kbq-size-l);

background: var(--kbq-background-bg);
border-bottom: 1px solid var(--kbq-line-contrast-less);
}

.dev-toolbar__modes {
display: flex;
gap: var(--kbq-size-s);
flex-wrap: wrap;
}

.dev-hint {
max-width: 920px;
margin: 0 var(--kbq-size-m) var(--kbq-size-xl);
padding: var(--kbq-size-m);

background: var(--kbq-background-theme-fade);
border-radius: var(--kbq-size-s);

code {
padding: 0 var(--kbq-size-3xs);
background: var(--kbq-background-contrast-fade);
border-radius: var(--kbq-size-3xs);
}
}

h3 {
margin: var(--kbq-size-xl) var(--kbq-size-m) var(--kbq-size-xs);
}

.dev-section-note {
margin: 0 var(--kbq-size-m) var(--kbq-size-m);
color: var(--kbq-foreground-contrast-secondary);
}

.dev-grid {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--kbq-size-l);

padding: var(--kbq-size-m);
margin: 0 var(--kbq-size-m);
}

.dev-grid_column {
flex-direction: column;
align-items: flex-start;
}

.dev-grid_baseline {
align-items: baseline;
}

.dev-cell {
display: inline-flex;
flex-direction: column;
align-items: center;
gap: var(--kbq-size-xxs);

min-width: 64px;
padding: var(--kbq-size-s);

border: 1px dashed var(--kbq-line-contrast-less);
border-radius: var(--kbq-size-xs);

small {
color: var(--kbq-foreground-contrast-secondary);
}
}

kbq-tag-list {
margin: 0 var(--kbq-size-m);
}
Loading
Loading