Add comprehensive library expansion with new components and demos

- Add new libraries: ui-accessibility, ui-animations, ui-backgrounds, ui-code-display, ui-data-utils, ui-font-manager, hcl-studio
- Add extensive layout components: gallery-grid, infinite-scroll-container, kanban-board, masonry, split-view, sticky-layout
- Add comprehensive demo components for all new features
- Update project configuration and dependencies
- Expand component exports and routing structure
- Add UI landing pages planning document

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
skyai_dev
2025-09-05 05:37:37 +10:00
parent 876eb301a0
commit 5346d6d0c9
5476 changed files with 350855 additions and 10 deletions

View File

@@ -0,0 +1,226 @@
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
// Code block component styles with design token integration
.code-block {
position: relative;
width: 100%;
font-family: 'Roboto Mono', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace;
font-size: $base-typography-font-size-sm;
background: var(--code-bg, #{$semantic-color-surface-primary});
// Variant styles
&--default {
border: $semantic-border-card-width $semantic-border-card-style var(--code-border, #{$semantic-color-border-secondary});
border-radius: $semantic-border-card-radius;
overflow: hidden;
}
&--minimal {
border: none;
border-radius: 0;
}
&--bordered {
border-left: 4px solid var(--code-keyword, #{$semantic-color-brand-primary});
padding-left: $semantic-spacing-component-md;
}
// Wrapping variant
&--wrap {
.code-block__code {
white-space: pre-wrap;
word-break: break-word;
}
}
// Copyable variant
&--copyable {
&:hover .code-block__copy-btn {
opacity: 1;
}
}
}
.code-block__copy-btn {
position: absolute;
top: $semantic-spacing-component-sm;
right: $semantic-spacing-component-sm;
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
border: none;
background: var(--code-surface, #{$semantic-color-surface-secondary});
color: var(--code-text, #{$semantic-color-text-secondary});
border-radius: $semantic-border-button-radius;
cursor: pointer;
transition: all $semantic-duration-fast $semantic-easing-standard;
opacity: 0;
z-index: 1;
&:hover {
background: var(--code-border, #{$semantic-color-surface-elevated});
color: var(--code-text, #{$semantic-color-text-primary});
}
&:active {
transform: scale(0.95);
}
&:focus-visible {
opacity: 1;
outline: $semantic-border-focus-width solid var(--code-keyword, #{$semantic-color-focus});
outline-offset: 2px;
}
}
.code-block__pre {
margin: 0;
padding: $semantic-spacing-component-md;
background: transparent;
overflow-x: auto;
font-family: inherit;
font-size: inherit;
line-height: $base-typography-line-height-relaxed;
display: flex;
&--with-line-numbers {
.code-block__code {
padding-left: $semantic-spacing-component-md;
}
}
.code-block--minimal & {
padding: 0;
}
.code-block--bordered & {
padding-left: 0;
}
}
// Line numbers
.code-block__line-numbers {
display: flex;
flex-direction: column;
padding-right: $semantic-spacing-component-md;
border-right: $semantic-border-separator-width $semantic-border-separator-style var(--code-border, #{$semantic-color-border-subtle});
user-select: none;
min-width: 3ch;
text-align: right;
flex-shrink: 0;
}
.code-block__line-number {
color: var(--code-line-number, #{$semantic-color-text-tertiary});
font-size: inherit;
line-height: inherit;
padding: 0 $semantic-spacing-component-xs;
&--highlighted {
background: var(--code-line-highlight, #{$semantic-color-container-primary});
color: var(--code-text, #{$semantic-color-text-primary});
}
}
// Code content
.code-block__code {
display: block;
padding: 0;
margin: 0;
background: transparent;
color: var(--code-text, #{$semantic-color-text-primary});
font-family: inherit;
font-size: inherit;
line-height: inherit;
white-space: pre;
overflow: visible;
flex: 1;
min-width: 0;
// Syntax highlighting styles
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: var(--code-comment, #{$semantic-color-text-tertiary});
font-style: italic;
}
.token.keyword,
.token.control,
.token.directive,
.token.unit {
color: var(--code-keyword, #{$semantic-color-brand-primary});
font-weight: $base-typography-font-weight-medium;
}
.token.string,
.token.attr-value {
color: var(--code-string, #{$semantic-color-success});
}
.token.number {
color: var(--code-number, #{$semantic-color-warning});
}
.token.function,
.token.class-name {
color: var(--code-function, #{$semantic-color-interactive-primary});
}
.token.operator,
.token.entity,
.token.url {
color: var(--code-operator, #{$semantic-color-text-primary});
}
.token.punctuation {
color: var(--code-punctuation, #{$semantic-color-text-secondary});
}
.token.variable {
color: var(--code-variable, #{$semantic-color-text-primary});
}
.token.boolean,
.token.constant {
color: var(--code-keyword, #{$semantic-color-brand-primary});
}
.token.property,
.token.attr-name {
color: var(--code-function, #{$semantic-color-interactive-primary});
}
.token.selector,
.token.tag {
color: var(--code-keyword, #{$semantic-color-brand-primary});
}
.token.important {
font-weight: $base-typography-font-weight-bold;
}
.token.bold {
font-weight: $base-typography-font-weight-bold;
}
.token.italic {
font-style: italic;
}
}
// Responsive adjustments
@media (max-width: 768px) {
.code-block__pre {
padding: $semantic-spacing-component-sm;
}
.code-block__copy-btn {
top: $semantic-spacing-component-xs;
right: $semantic-spacing-component-xs;
}
}

View File

@@ -0,0 +1,168 @@
import { Component, Input, computed, signal, ChangeDetectionStrategy, ViewEncapsulation, OnInit, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SyntaxHighlighterService } from '../../services/syntax-highlighter.service';
import { CodeThemeService, CodeTheme } from '../../services/theme.service';
import { CopyButtonDirective } from '../../directives/copy-button.directive';
export type CodeBlockVariant = 'default' | 'minimal' | 'bordered';
@Component({
selector: 'ui-code-block',
standalone: true,
imports: [CommonModule, CopyButtonDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
template: `
<div [class]="containerClasses()">
@if (copyable) {
<button
type="button"
class="code-block__copy-btn"
[uiCopyButton]="code"
[attr.aria-label]="'Copy code to clipboard'"
title="Copy code">
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
</button>
}
<pre [class]="preClasses()">
@if (showLineNumbers && lineCount() > 1) {
<div class="code-block__line-numbers" [attr.aria-hidden]="true">
@for (lineNum of lineNumbers(); track lineNum) {
<span
class="code-block__line-number"
[class.code-block__line-number--highlighted]="isLineHighlighted(lineNum)">
{{ lineNum }}
</span>
}
</div>
}
<code
[class]="codeClasses()"
[innerHTML]="highlightedCode()">
</code>
</pre>
</div>
`,
styleUrl: './code-block.component.scss'
})
export class CodeBlockComponent implements OnInit, OnChanges {
@Input() code = '';
@Input() language = '';
@Input() theme: CodeTheme | null = null;
@Input() variant: CodeBlockVariant = 'default';
@Input() showLineNumbers = true;
@Input() copyable = true;
@Input() highlightLines: (number | string)[] = [];
@Input() startLineNumber = 1;
@Input() wrap = false;
constructor(
private syntaxHighlighter: SyntaxHighlighterService,
private themeService: CodeThemeService
) {}
readonly finalLanguage = computed(() => {
if (this.language) {
return this.language;
}
return this.syntaxHighlighter.detectLanguage(this.code);
});
readonly highlightedCode = signal('');
private async updateHighlightedCode() {
if (!this.code) {
this.highlightedCode.set('');
return;
}
const lang = this.finalLanguage();
try {
const highlighted = await this.syntaxHighlighter.highlight(this.code, {
language: lang,
lineNumbers: this.showLineNumbers,
highlightLines: this.highlightLines
});
this.highlightedCode.set(highlighted);
} catch (error) {
console.warn('Failed to highlight code:', error);
this.highlightedCode.set(this.escapeHtml(this.code));
}
}
readonly lineCount = computed(() => {
return this.code ? this.code.split('\n').length : 0;
});
readonly lineNumbers = computed(() => {
const count = this.lineCount();
return Array.from({ length: count }, (_, i) => i + this.startLineNumber);
});
readonly currentTheme = computed(() => {
return this.theme || this.themeService.theme();
});
readonly containerClasses = computed(() => {
const classes = ['code-block'];
classes.push(`code-block--${this.variant}`);
classes.push(`code-theme-${this.currentTheme()}`);
if (this.wrap) classes.push('code-block--wrap');
if (this.copyable) classes.push('code-block--copyable');
return classes.join(' ');
});
readonly preClasses = computed(() => {
const classes = ['code-block__pre'];
if (this.showLineNumbers && this.lineCount() > 1) {
classes.push('code-block__pre--with-line-numbers');
}
return classes.join(' ');
});
readonly codeClasses = computed(() => {
const classes = ['code-block__code'];
classes.push(`language-${this.finalLanguage()}`);
return classes.join(' ');
});
isLineHighlighted(lineNumber: number): boolean {
return this.highlightLines.some(highlight => {
if (typeof highlight === 'number') {
return highlight === lineNumber;
}
// Handle range format like "5-10"
const range = highlight.toString().split('-');
if (range.length === 2) {
const start = parseInt(range[0], 10);
const end = parseInt(range[1], 10);
return lineNumber >= start && lineNumber <= end;
}
return false;
});
}
ngOnInit(): void {
this.updateHighlightedCode();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['code'] || changes['language']) {
this.updateHighlightedCode();
}
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}

View File

@@ -0,0 +1,378 @@
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
// Code snippet component styles with design token integration
.code-snippet {
position: relative;
width: 100%;
border-radius: $semantic-border-card-radius;
border: $semantic-border-card-width $semantic-border-card-style var(--code-border, #{$semantic-color-border-secondary});
background: var(--code-bg, #{$semantic-color-surface-primary});
overflow: hidden;
font-family: 'Roboto Mono', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace;
// Size variants
&--sm {
font-size: $base-typography-font-size-xs;
.code-snippet__header {
padding: $semantic-spacing-component-padding-xs $semantic-spacing-component-padding-sm;
gap: $semantic-spacing-component-xs;
}
.code-snippet__content {
padding: $semantic-spacing-component-padding-xs $semantic-spacing-component-padding-sm;
}
}
&--md {
font-size: $base-typography-font-size-sm;
.code-snippet__header {
padding: $semantic-spacing-component-padding-sm $semantic-spacing-component-padding-md;
gap: $semantic-spacing-component-sm;
}
.code-snippet__content {
padding: $semantic-spacing-component-padding-sm $semantic-spacing-component-padding-md;
}
}
&--lg {
font-size: $base-typography-font-size-md;
.code-snippet__header {
padding: $semantic-spacing-component-padding-md $semantic-spacing-component-padding-lg;
gap: $semantic-spacing-component-md;
}
.code-snippet__content {
padding: $semantic-spacing-component-padding-md $semantic-spacing-component-padding-lg;
}
}
// Wrapping variant
&--wrap {
.code-snippet__code {
white-space: pre-wrap;
word-break: break-word;
}
}
// Constrained height variant
&--constrained {
.code-snippet__content {
max-height: var(--max-height);
overflow: hidden;
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: $semantic-spacing-component-xl;
background: linear-gradient(to bottom, transparent, var(--code-bg, #{$semantic-color-surface-primary}));
pointer-events: none;
}
}
}
}
// Header section
.code-snippet__header {
display: flex;
align-items: center;
justify-content: space-between;
background: var(--code-surface, #{$semantic-color-surface-secondary});
border-bottom: $semantic-border-divider-width $semantic-border-divider-style var(--code-border, #{$semantic-color-border-secondary});
color: var(--code-text, #{$semantic-color-text-primary});
}
.code-snippet__title {
font-weight: $base-typography-font-weight-medium;
color: var(--code-text, #{$semantic-color-text-primary});
}
.code-snippet__actions {
display: flex;
align-items: center;
gap: $semantic-spacing-component-sm;
}
.code-snippet__language-badge {
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
background: var(--code-keyword, #{$semantic-color-interactive-primary});
color: white;
border-radius: $semantic-border-input-radius;
font-size: $base-typography-font-size-xs;
font-weight: $base-typography-font-weight-medium;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.code-snippet__copy-btn {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
border: none;
background: transparent;
color: var(--code-text, #{$semantic-color-text-secondary});
border-radius: $semantic-border-button-radius;
cursor: pointer;
transition: all $semantic-duration-fast $semantic-easing-standard;
&:hover {
background: var(--code-surface, #{$semantic-color-surface-elevated});
color: var(--code-text, #{$semantic-color-text-primary});
}
&:active {
transform: scale(0.95);
}
&:focus-visible {
outline: $semantic-border-focus-width solid var(--code-keyword, #{$semantic-color-focus});
outline-offset: 2px;
}
}
// Content area
.code-snippet__content {
position: relative;
overflow-x: auto;
background: var(--code-bg, #{$semantic-color-surface-primary});
}
.code-snippet__pre {
margin: 0;
padding: 0;
background: transparent;
overflow: visible;
font-family: inherit;
font-size: inherit;
line-height: $base-typography-line-height-relaxed;
display: flex;
&--with-line-numbers {
.code-snippet__code {
padding-left: $semantic-spacing-component-md;
}
}
}
// Line numbers
.code-snippet__line-numbers {
display: flex;
flex-direction: column;
padding-right: $semantic-spacing-component-md;
border-right: $semantic-border-separator-width $semantic-border-separator-style var(--code-border, #{$semantic-color-border-subtle});
user-select: none;
min-width: 3ch;
text-align: right;
}
.code-snippet__line-number {
color: var(--code-line-number, #{$semantic-color-text-tertiary});
font-size: inherit;
line-height: inherit;
padding: 0 $semantic-spacing-component-xs;
&--highlighted {
background: var(--code-line-highlight, #{$semantic-color-container-primary});
color: var(--code-text, #{$semantic-color-text-primary});
}
}
// Code content
.code-snippet__code {
display: block;
padding: 0;
margin: 0;
background: transparent;
color: var(--code-text, #{$semantic-color-text-primary});
font-family: inherit;
font-size: inherit;
line-height: inherit;
white-space: pre;
overflow: visible;
flex: 1;
// Syntax highlighting styles
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: var(--code-comment, #{$semantic-color-text-tertiary});
font-style: italic;
}
.token.keyword,
.token.control,
.token.directive,
.token.unit {
color: var(--code-keyword, #{$semantic-color-brand-primary});
font-weight: $base-typography-font-weight-medium;
}
.token.string,
.token.attr-value {
color: var(--code-string, #{$semantic-color-success});
}
.token.number {
color: var(--code-number, #{$semantic-color-warning});
}
.token.function,
.token.class-name {
color: var(--code-function, #{$semantic-color-interactive-primary});
}
.token.operator,
.token.entity,
.token.url {
color: var(--code-operator, #{$semantic-color-text-primary});
}
.token.punctuation {
color: var(--code-punctuation, #{$semantic-color-text-secondary});
}
.token.variable {
color: var(--code-variable, #{$semantic-color-text-primary});
}
.token.boolean,
.token.constant {
color: var(--code-keyword, #{$semantic-color-brand-primary});
}
.token.property,
.token.attr-name {
color: var(--code-function, #{$semantic-color-interactive-primary});
}
.token.selector,
.token.tag {
color: var(--code-keyword, #{$semantic-color-brand-primary});
}
}
// Expand section
.code-snippet__expand {
display: flex;
justify-content: center;
padding: $semantic-spacing-component-sm;
background: var(--code-surface, #{$semantic-color-surface-secondary});
border-top: $semantic-border-separator-width $semantic-border-separator-style var(--code-border, #{$semantic-color-border-secondary});
}
.code-snippet__expand-btn {
padding: $semantic-spacing-component-xs $semantic-spacing-component-md;
border: $semantic-border-button-width $semantic-border-button-style var(--code-border, #{$semantic-color-border-primary});
border-radius: $semantic-border-button-radius;
background: var(--code-bg, #{$semantic-color-surface-primary});
color: var(--code-text, #{$semantic-color-text-primary});
font-size: $base-typography-font-size-sm;
cursor: pointer;
transition: all $semantic-duration-fast $semantic-easing-standard;
&:hover {
background: var(--code-surface, #{$semantic-color-surface-elevated});
}
&:focus-visible {
outline: $semantic-border-focus-width solid var(--code-keyword, #{$semantic-color-focus});
outline-offset: 2px;
}
}
// Theme-specific overrides
.code-theme-github-light {
--code-bg: #{$semantic-color-surface-primary};
--code-surface: #{$semantic-color-surface-secondary};
--code-text: #{$semantic-color-text-primary};
--code-comment: #{$semantic-color-text-tertiary};
--code-keyword: #{$semantic-color-brand-primary};
--code-string: #{$semantic-color-success};
--code-number: #{$semantic-color-warning};
--code-function: #{$semantic-color-interactive-primary};
--code-operator: #{$semantic-color-text-primary};
--code-punctuation: #{$semantic-color-text-secondary};
--code-variable: #{$semantic-color-text-primary};
--code-line-number: #{$semantic-color-text-tertiary};
--code-line-highlight: #{$semantic-color-container-primary};
--code-border: #{$semantic-color-border-secondary};
}
.code-theme-github-dark {
--code-bg: #0d1117;
--code-surface: #161b22;
--code-text: #f0f6fc;
--code-comment: #8b949e;
--code-keyword: #ff7b72;
--code-string: #a5d6ff;
--code-number: #79c0ff;
--code-function: #d2a8ff;
--code-operator: #f0f6fc;
--code-punctuation: #c9d1d9;
--code-variable: #f0f6fc;
--code-line-number: #8b949e;
--code-line-highlight: rgba(255, 211, 61, 0.08);
--code-border: #30363d;
}
.code-theme-one-dark {
--code-bg: #282c34;
--code-surface: #3e4451;
--code-text: #abb2bf;
--code-comment: #5c6370;
--code-keyword: #c678dd;
--code-string: #98c379;
--code-number: #d19a66;
--code-function: #61afef;
--code-operator: #abb2bf;
--code-punctuation: #abb2bf;
--code-variable: #e06c75;
--code-line-number: #5c6370;
--code-line-highlight: #3e4451;
--code-border: #3e4451;
}
.code-theme-material {
--code-bg: #263238;
--code-surface: #37474f;
--code-text: #eeffff;
--code-comment: #546e7a;
--code-keyword: #c792ea;
--code-string: #c3e88d;
--code-number: #f78c6c;
--code-function: #82aaff;
--code-operator: #89ddff;
--code-punctuation: #89ddff;
--code-variable: #eeffff;
--code-line-number: #546e7a;
--code-line-highlight: #37474f;
--code-border: #37474f;
}
.code-theme-dracula {
--code-bg: #282a36;
--code-surface: #44475a;
--code-text: #f8f8f2;
--code-comment: #6272a4;
--code-keyword: #ff79c6;
--code-string: #f1fa8c;
--code-number: #bd93f9;
--code-function: #8be9fd;
--code-operator: #ff79c6;
--code-punctuation: #f8f8f2;
--code-variable: #f8f8f2;
--code-line-number: #6272a4;
--code-line-highlight: #44475a;
--code-border: #44475a;
}

View File

@@ -0,0 +1,214 @@
import { Component, Input, Output, EventEmitter, computed, signal, ChangeDetectionStrategy, ViewEncapsulation, OnInit, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SyntaxHighlighterService } from '../../services/syntax-highlighter.service';
import { CodeThemeService, CodeTheme } from '../../services/theme.service';
import { CopyButtonDirective } from '../../directives/copy-button.directive';
export type CodeSnippetSize = 'sm' | 'md' | 'lg';
@Component({
selector: 'ui-code-snippet',
standalone: true,
imports: [CommonModule, CopyButtonDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
template: `
<div [class]="containerClasses()">
@if (title || showLanguage || copyable) {
<div class="code-snippet__header">
@if (title) {
<span class="code-snippet__title">{{ title }}</span>
}
<div class="code-snippet__actions">
@if (showLanguage && finalLanguage()) {
<span class="code-snippet__language-badge">{{ finalLanguage() }}</span>
}
@if (copyable) {
<button
type="button"
class="code-snippet__copy-btn"
[uiCopyButton]="code"
[attr.aria-label]="'Copy code to clipboard'"
title="Copy code">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
</button>
}
</div>
</div>
}
<div class="code-snippet__content">
<pre [class]="preClasses()">
@if (showLineNumbers && lineCount() > 1) {
<div class="code-snippet__line-numbers" [attr.aria-hidden]="true">
@for (lineNum of lineNumbers(); track lineNum) {
<span
class="code-snippet__line-number"
[class.code-snippet__line-number--highlighted]="isLineHighlighted(lineNum)">
{{ lineNum }}
</span>
}
</div>
}
<code
[class]="codeClasses()"
[innerHTML]="highlightedCode()">
</code>
</pre>
</div>
@if (maxHeight && !expanded() && isOverflowing()) {
<div class="code-snippet__expand">
<button
type="button"
class="code-snippet__expand-btn"
(click)="toggleExpanded()"
[attr.aria-expanded]="expanded()">
Show {{ isOverflowing() ? 'more' : 'less' }}
</button>
</div>
}
</div>
`,
styleUrl: './code-snippet.component.scss'
})
export class CodeSnippetComponent implements OnInit, OnChanges {
@Input() code = '';
@Input() language = '';
@Input() title = '';
@Input() theme: CodeTheme | null = null;
@Input() size: CodeSnippetSize = 'md';
@Input() showLineNumbers = false;
@Input() copyable = true;
@Input() showLanguage = true;
@Input() highlightLines: (number | string)[] = [];
@Input() maxHeight: string | null = null;
@Input() wrap = false;
@Input() startLineNumber = 1;
@Output() codeChange = new EventEmitter<string>();
@Output() languageChange = new EventEmitter<string>();
readonly expanded = signal(false);
constructor(
private syntaxHighlighter: SyntaxHighlighterService,
private themeService: CodeThemeService
) {}
readonly finalLanguage = computed(() => {
if (this.language) {
return this.language;
}
return this.syntaxHighlighter.detectLanguage(this.code);
});
readonly highlightedCode = signal('');
private async updateHighlightedCode() {
if (!this.code) {
this.highlightedCode.set('');
return;
}
const lang = this.finalLanguage();
try {
const highlighted = await this.syntaxHighlighter.highlight(this.code, {
language: lang,
lineNumbers: this.showLineNumbers,
highlightLines: this.highlightLines
});
this.highlightedCode.set(highlighted);
} catch (error) {
console.warn('Failed to highlight code:', error);
this.highlightedCode.set(this.escapeHtml(this.code));
}
}
readonly lineCount = computed(() => {
return this.code ? this.code.split('\n').length : 0;
});
readonly lineNumbers = computed(() => {
const count = this.lineCount();
return Array.from({ length: count }, (_, i) => i + this.startLineNumber);
});
readonly currentTheme = computed(() => {
return this.theme || this.themeService.theme();
});
readonly containerClasses = computed(() => {
const classes = ['code-snippet'];
classes.push(`code-snippet--${this.size}`);
classes.push(`code-theme-${this.currentTheme()}`);
if (this.wrap) classes.push('code-snippet--wrap');
if (this.maxHeight && !this.expanded()) classes.push('code-snippet--constrained');
return classes.join(' ');
});
readonly preClasses = computed(() => {
const classes = ['code-snippet__pre'];
if (this.showLineNumbers && this.lineCount() > 1) {
classes.push('code-snippet__pre--with-line-numbers');
}
return classes.join(' ');
});
readonly codeClasses = computed(() => {
const classes = ['code-snippet__code'];
classes.push(`language-${this.finalLanguage()}`);
return classes.join(' ');
});
isLineHighlighted(lineNumber: number): boolean {
return this.highlightLines.some(highlight => {
if (typeof highlight === 'number') {
return highlight === lineNumber;
}
// Handle range format like "5-10"
const range = highlight.toString().split('-');
if (range.length === 2) {
const start = parseInt(range[0], 10);
const end = parseInt(range[1], 10);
return lineNumber >= start && lineNumber <= end;
}
return false;
});
}
isOverflowing(): boolean {
// This would need to be implemented with ViewChild and element measurements
// For now, return true if content is long
return this.lineCount() > 20;
}
toggleExpanded(): void {
this.expanded.update(val => !val);
}
ngOnInit(): void {
this.updateHighlightedCode();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['code'] || changes['language']) {
this.updateHighlightedCode();
}
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}

View File

@@ -0,0 +1,142 @@
@use '../../../../../ui-design-system/src/styles/semantic/index' as *;
// Inline code component styles with design token integration
.inline-code {
position: relative;
display: inline-flex;
align-items: center;
gap: $semantic-spacing-component-xs;
background: var(--code-surface, #{$semantic-color-surface-secondary});
border: $semantic-border-input-width $semantic-border-input-style var(--code-border, #{$semantic-color-border-secondary});
border-radius: $semantic-border-input-radius;
font-family: 'Roboto Mono', 'Monaco', 'Consolas', 'Liberation Mono', 'Courier New', monospace;
// Size variants
&--xs {
padding: $semantic-spacing-component-xs;
font-size: $base-typography-font-size-xs;
}
&--sm {
padding: $semantic-spacing-component-xs $semantic-spacing-component-sm;
font-size: $base-typography-font-size-sm;
}
&--md {
padding: $semantic-spacing-component-sm;
font-size: $base-typography-font-size-sm;
}
&--lg {
padding: $semantic-spacing-component-sm $semantic-spacing-component-md;
font-size: $base-typography-font-size-md;
}
// Copyable variant
&--copyable {
padding-right: calc(#{$semantic-spacing-component-sm} + 1.5rem + #{$semantic-spacing-component-xs});
}
}
.inline-code__code {
display: inline;
padding: 0;
margin: 0;
background: transparent;
border: none;
color: var(--code-text, #{$semantic-color-text-primary});
font-family: inherit;
font-size: inherit;
line-height: 1.2;
white-space: nowrap;
// Syntax highlighting styles (simplified for inline use)
.token.comment {
color: var(--code-comment, #{$semantic-color-text-tertiary});
font-style: italic;
}
.token.keyword {
color: var(--code-keyword, #{$semantic-color-brand-primary});
font-weight: $base-typography-font-weight-medium;
}
.token.string {
color: var(--code-string, #{$semantic-color-success});
}
.token.number {
color: var(--code-number, #{$semantic-color-warning});
}
.token.function {
color: var(--code-function, #{$semantic-color-interactive-primary});
}
.token.operator {
color: var(--code-operator, #{$semantic-color-text-primary});
}
.token.punctuation {
color: var(--code-punctuation, #{$semantic-color-text-secondary});
}
.token.variable {
color: var(--code-variable, #{$semantic-color-text-primary});
}
.token.boolean,
.token.constant {
color: var(--code-keyword, #{$semantic-color-brand-primary});
}
}
.inline-code__copy-btn {
position: absolute;
right: $semantic-spacing-component-xs;
top: 50%;
transform: translateY(-50%);
display: flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
padding: 0;
border: none;
background: transparent;
color: var(--code-comment, #{$semantic-color-text-tertiary});
border-radius: calc(#{$semantic-border-button-radius} * 0.5);
cursor: pointer;
transition: all $semantic-duration-fast $semantic-easing-standard;
opacity: 0;
.inline-code:hover & {
opacity: 1;
}
&:hover {
background: var(--code-bg, #{$semantic-color-surface-primary});
color: var(--code-text, #{$semantic-color-text-primary});
}
&:active {
transform: translateY(-50%) scale(0.9);
}
&:focus-visible {
opacity: 1;
outline: 1px solid var(--code-keyword, #{$semantic-color-focus});
outline-offset: 1px;
}
svg {
width: 10px;
height: 10px;
}
}
// Theme inheritance from parent
.inline-code {
// Themes are handled by parent code-theme- classes
// or can be applied directly to inline-code elements
}

View File

@@ -0,0 +1,118 @@
import { Component, Input, computed, signal, ChangeDetectionStrategy, ViewEncapsulation, OnInit, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SyntaxHighlighterService } from '../../services/syntax-highlighter.service';
import { CodeThemeService, CodeTheme } from '../../services/theme.service';
import { CopyButtonDirective } from '../../directives/copy-button.directive';
export type InlineCodeSize = 'xs' | 'sm' | 'md' | 'lg';
@Component({
selector: 'ui-inline-code',
standalone: true,
imports: [CommonModule, CopyButtonDirective],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
template: `
<span [class]="containerClasses()">
<code
[class]="codeClasses()"
[innerHTML]="highlightedCode()">
</code>
@if (copyable) {
<button
type="button"
class="inline-code__copy-btn"
[uiCopyButton]="code"
[attr.aria-label]="'Copy code to clipboard'"
title="Copy code">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
</button>
}
</span>
`,
styleUrl: './inline-code.component.scss'
})
export class InlineCodeComponent implements OnInit, OnChanges {
@Input() code = '';
@Input() language = '';
@Input() theme: CodeTheme | null = null;
@Input() size: InlineCodeSize = 'md';
@Input() copyable = false;
constructor(
private syntaxHighlighter: SyntaxHighlighterService,
private themeService: CodeThemeService
) {}
readonly finalLanguage = computed(() => {
if (this.language) {
return this.language;
}
// For inline code, we're more conservative with auto-detection
return 'text';
});
readonly highlightedCode = signal('');
private async updateHighlightedCode() {
if (!this.code) {
this.highlightedCode.set('');
return;
}
const lang = this.finalLanguage();
if (lang === 'text') {
this.highlightedCode.set(this.escapeHtml(this.code));
return;
}
try {
const highlighted = await this.syntaxHighlighter.highlight(this.code, {
language: lang
});
this.highlightedCode.set(highlighted);
} catch (error) {
console.warn('Failed to highlight code:', error);
this.highlightedCode.set(this.escapeHtml(this.code));
}
}
readonly currentTheme = computed(() => {
return this.theme || this.themeService.theme();
});
readonly containerClasses = computed(() => {
const classes = ['inline-code'];
classes.push(`inline-code--${this.size}`);
classes.push(`code-theme-${this.currentTheme()}`);
if (this.copyable) classes.push('inline-code--copyable');
return classes.join(' ');
});
readonly codeClasses = computed(() => {
const classes = ['inline-code__code'];
classes.push(`language-${this.finalLanguage()}`);
return classes.join(' ');
});
ngOnInit(): void {
this.updateHighlightedCode();
}
ngOnChanges(changes: SimpleChanges): void {
if (changes['code'] || changes['language']) {
this.updateHighlightedCode();
}
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}

View File

@@ -0,0 +1,81 @@
import { Directive, Input, HostListener, inject, signal } from '@angular/core';
@Directive({
selector: '[uiCopyButton]',
standalone: true
})
export class CopyButtonDirective {
@Input() uiCopyButton!: string;
@Input() copySuccessMessage = 'Copied!';
@Input() copyErrorMessage = 'Failed to copy';
private copied = signal(false);
private timeoutId: number | null = null;
@HostListener('click', ['$event'])
async onClick(event: Event): Promise<void> {
event.preventDefault();
event.stopPropagation();
if (!this.uiCopyButton) {
console.warn('No content provided to copy');
return;
}
try {
await this.copyToClipboard(this.uiCopyButton);
this.showFeedback(true);
} catch (error) {
console.error('Failed to copy to clipboard:', error);
this.showFeedback(false);
}
}
private async copyToClipboard(text: string): Promise<void> {
if (navigator.clipboard && window.isSecureContext) {
// Use modern clipboard API
await navigator.clipboard.writeText(text);
} else {
// Fallback for older browsers or non-secure contexts
this.fallbackCopyToClipboard(text);
}
}
private fallbackCopyToClipboard(text: string): void {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
} catch (error) {
throw new Error('Fallback copy failed');
} finally {
document.body.removeChild(textArea);
}
}
private showFeedback(success: boolean): void {
this.copied.set(success);
// Clear existing timeout
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
// Reset after 2 seconds
this.timeoutId = window.setTimeout(() => {
this.copied.set(false);
this.timeoutId = null;
}, 2000);
}
get isCopied(): boolean {
return this.copied();
}
}

View File

@@ -0,0 +1,180 @@
import { Injectable } from '@angular/core';
export interface HighlightOptions {
language: string;
lineNumbers?: boolean;
highlightLines?: (number | string)[];
}
@Injectable({
providedIn: 'root'
})
export class SyntaxHighlighterService {
private supportedLanguages = new Set([
'javascript', 'typescript', 'css', 'scss', 'html', 'json',
'bash', 'python', 'java', 'csharp', 'markup'
]);
private prismLoaded = false;
private loadPromise: Promise<any> | null = null;
constructor() {}
async highlight(code: string, options: HighlightOptions): Promise<string> {
const { language } = options;
if (!this.isLanguageSupported(language)) {
return this.escapeHtml(code);
}
try {
const Prism = await this.loadPrism();
await this.loadLanguage(language);
const grammar = Prism.languages[language];
if (!grammar) {
return this.escapeHtml(code);
}
return Prism.highlight(code, grammar, language);
} catch (error) {
console.warn('Failed to highlight code:', error);
return this.escapeHtml(code);
}
}
private async loadPrism(): Promise<any> {
if (this.prismLoaded && (window as any).Prism) {
return (window as any).Prism;
}
if (this.loadPromise) {
return this.loadPromise;
}
this.loadPromise = new Promise(async (resolve, reject) => {
try {
if (typeof window === 'undefined') {
// Server-side rendering fallback
resolve({ highlight: () => '', languages: {} });
return;
}
// Load Prism core
const prismModule = await import(/* @vite-ignore */ 'prismjs');
const Prism = prismModule.default || prismModule;
// Ensure global availability
(window as any).Prism = Prism;
this.prismLoaded = true;
resolve(Prism);
} catch (error) {
console.warn('Failed to load Prism:', error);
reject(error);
}
});
return this.loadPromise;
}
private async loadLanguage(language: string): Promise<void> {
if (typeof window === 'undefined') return;
const Prism = (window as any).Prism;
if (!Prism) return;
// Check if language is already loaded
if (Prism.languages[language]) {
return;
}
try {
// Map of language identifiers to their import paths
const languageMap: Record<string, string> = {
'typescript': 'prismjs/components/prism-typescript',
'javascript': 'prismjs/components/prism-javascript',
'css': 'prismjs/components/prism-css',
'scss': 'prismjs/components/prism-scss',
'json': 'prismjs/components/prism-json',
'markup': 'prismjs/components/prism-markup',
'html': 'prismjs/components/prism-markup',
'bash': 'prismjs/components/prism-bash',
'python': 'prismjs/components/prism-python',
'java': 'prismjs/components/prism-java',
'csharp': 'prismjs/components/prism-csharp'
};
const importPath = languageMap[language];
if (importPath) {
await import(/* @vite-ignore */ importPath);
}
} catch (error) {
console.warn(`Failed to load language ${language}:`, error);
}
}
isLanguageSupported(language: string): boolean {
return this.supportedLanguages.has(language.toLowerCase());
}
detectLanguage(code: string): string {
// Simple heuristics for language detection
const trimmedCode = code.trim();
if (trimmedCode.startsWith('<?') || trimmedCode.includes('<html>') ||
/<\/?[a-z][\s\S]*>/i.test(trimmedCode)) {
return 'markup';
}
if (trimmedCode.startsWith('{') || trimmedCode.startsWith('[')) {
try {
JSON.parse(trimmedCode);
return 'json';
} catch {}
}
if (trimmedCode.includes('function ') || trimmedCode.includes('const ') ||
trimmedCode.includes('let ') || trimmedCode.includes('var ')) {
if (trimmedCode.includes(': ') && (trimmedCode.includes('interface ') ||
trimmedCode.includes('type '))) {
return 'typescript';
}
return 'javascript';
}
if (trimmedCode.includes('#') && trimmedCode.includes('def ')) {
return 'python';
}
if (trimmedCode.includes('public class ') || trimmedCode.includes('import java.')) {
return 'java';
}
if (trimmedCode.includes('using System') || trimmedCode.includes('namespace ')) {
return 'csharp';
}
if (trimmedCode.includes('#!/bin/') || trimmedCode.includes('echo ')) {
return 'bash';
}
if (trimmedCode.includes('{') && (trimmedCode.includes('color:') ||
trimmedCode.includes('margin:') || trimmedCode.includes('@'))) {
return trimmedCode.includes('$') ? 'scss' : 'css';
}
return 'text';
}
getSupportedLanguages(): string[] {
return Array.from(this.supportedLanguages).sort();
}
private escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}

View File

@@ -0,0 +1,154 @@
import { Injectable, signal, computed } from '@angular/core';
export type CodeTheme = 'github-light' | 'github-dark' | 'one-dark' | 'material' | 'dracula';
export interface ThemeColors {
background: string;
surface: string;
text: string;
comment: string;
keyword: string;
string: string;
number: string;
function: string;
operator: string;
punctuation: string;
variable: string;
lineNumber: string;
lineHighlight: string;
border: string;
}
@Injectable({
providedIn: 'root'
})
export class CodeThemeService {
private currentTheme = signal<CodeTheme>('github-light');
readonly theme = this.currentTheme.asReadonly();
readonly themeColors = computed(() => this.getThemeColors(this.currentTheme()));
private themes: Record<CodeTheme, ThemeColors> = {
'github-light': {
background: 'var(--semantic-color-surface-primary, #ffffff)',
surface: 'var(--semantic-color-surface-secondary, #f6f8fa)',
text: 'var(--semantic-color-text-primary, #24292f)',
comment: 'var(--semantic-color-text-tertiary, #6e7781)',
keyword: 'var(--semantic-color-brand-primary, #cf222e)',
string: 'var(--semantic-color-success, #0a3069)',
number: 'var(--semantic-color-warning, #0550ae)',
function: 'var(--semantic-color-interactive-primary, #8250df)',
operator: 'var(--semantic-color-text-primary, #24292f)',
punctuation: 'var(--semantic-color-text-secondary, #656d76)',
variable: 'var(--semantic-color-text-primary, #24292f)',
lineNumber: 'var(--semantic-color-text-tertiary, #656d76)',
lineHighlight: 'var(--semantic-color-container-primary, #fff8c5)',
border: 'var(--semantic-color-border-secondary, #d0d7de)'
},
'github-dark': {
background: 'var(--semantic-color-surface-primary, #0d1117)',
surface: 'var(--semantic-color-surface-secondary, #161b22)',
text: 'var(--semantic-color-text-primary, #f0f6fc)',
comment: 'var(--semantic-color-text-tertiary, #8b949e)',
keyword: 'var(--semantic-color-brand-primary, #ff7b72)',
string: 'var(--semantic-color-success, #a5d6ff)',
number: 'var(--semantic-color-warning, #79c0ff)',
function: 'var(--semantic-color-interactive-primary, #d2a8ff)',
operator: 'var(--semantic-color-text-primary, #f0f6fc)',
punctuation: 'var(--semantic-color-text-secondary, #c9d1d9)',
variable: 'var(--semantic-color-text-primary, #f0f6fc)',
lineNumber: 'var(--semantic-color-text-tertiary, #8b949e)',
lineHighlight: 'var(--semantic-color-container-primary, #ffd33d14)',
border: 'var(--semantic-color-border-secondary, #30363d)'
},
'one-dark': {
background: 'var(--semantic-color-surface-primary, #282c34)',
surface: 'var(--semantic-color-surface-secondary, #3e4451)',
text: 'var(--semantic-color-text-primary, #abb2bf)',
comment: 'var(--semantic-color-text-tertiary, #5c6370)',
keyword: 'var(--semantic-color-brand-primary, #c678dd)',
string: 'var(--semantic-color-success, #98c379)',
number: 'var(--semantic-color-warning, #d19a66)',
function: 'var(--semantic-color-interactive-primary, #61afef)',
operator: 'var(--semantic-color-text-primary, #abb2bf)',
punctuation: 'var(--semantic-color-text-secondary, #abb2bf)',
variable: 'var(--semantic-color-text-primary, #e06c75)',
lineNumber: 'var(--semantic-color-text-tertiary, #5c6370)',
lineHighlight: 'var(--semantic-color-container-primary, #3e4451)',
border: 'var(--semantic-color-border-secondary, #3e4451)'
},
'material': {
background: 'var(--semantic-color-surface-primary, #263238)',
surface: 'var(--semantic-color-surface-secondary, #37474f)',
text: 'var(--semantic-color-text-primary, #eeffff)',
comment: 'var(--semantic-color-text-tertiary, #546e7a)',
keyword: 'var(--semantic-color-brand-primary, #c792ea)',
string: 'var(--semantic-color-success, #c3e88d)',
number: 'var(--semantic-color-warning, #f78c6c)',
function: 'var(--semantic-color-interactive-primary, #82aaff)',
operator: 'var(--semantic-color-text-primary, #89ddff)',
punctuation: 'var(--semantic-color-text-secondary, #89ddff)',
variable: 'var(--semantic-color-text-primary, #eeffff)',
lineNumber: 'var(--semantic-color-text-tertiary, #546e7a)',
lineHighlight: 'var(--semantic-color-container-primary, #37474f)',
border: 'var(--semantic-color-border-secondary, #37474f)'
},
'dracula': {
background: 'var(--semantic-color-surface-primary, #282a36)',
surface: 'var(--semantic-color-surface-secondary, #44475a)',
text: 'var(--semantic-color-text-primary, #f8f8f2)',
comment: 'var(--semantic-color-text-tertiary, #6272a4)',
keyword: 'var(--semantic-color-brand-primary, #ff79c6)',
string: 'var(--semantic-color-success, #f1fa8c)',
number: 'var(--semantic-color-warning, #bd93f9)',
function: 'var(--semantic-color-interactive-primary, #8be9fd)',
operator: 'var(--semantic-color-text-primary, #ff79c6)',
punctuation: 'var(--semantic-color-text-secondary, #f8f8f2)',
variable: 'var(--semantic-color-text-primary, #f8f8f2)',
lineNumber: 'var(--semantic-color-text-tertiary, #6272a4)',
lineHighlight: 'var(--semantic-color-container-primary, #44475a)',
border: 'var(--semantic-color-border-secondary, #44475a)'
}
};
setTheme(theme: CodeTheme): void {
this.currentTheme.set(theme);
}
getThemeColors(theme: CodeTheme): ThemeColors {
return this.themes[theme];
}
getAvailableThemes(): CodeTheme[] {
return Object.keys(this.themes) as CodeTheme[];
}
generateThemeCSS(theme: CodeTheme): string {
const colors = this.getThemeColors(theme);
return `
.code-theme-${theme} {
--code-bg: ${colors.background};
--code-surface: ${colors.surface};
--code-text: ${colors.text};
--code-comment: ${colors.comment};
--code-keyword: ${colors.keyword};
--code-string: ${colors.string};
--code-number: ${colors.number};
--code-function: ${colors.function};
--code-operator: ${colors.operator};
--code-punctuation: ${colors.punctuation};
--code-variable: ${colors.variable};
--code-line-number: ${colors.lineNumber};
--code-line-highlight: ${colors.lineHighlight};
--code-border: ${colors.border};
}
`;
}
}

View File

@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { UiCodeDisplayComponent } from './ui-code-display.component';
describe('UiCodeDisplayComponent', () => {
let component: UiCodeDisplayComponent;
let fixture: ComponentFixture<UiCodeDisplayComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [UiCodeDisplayComponent]
})
.compileComponents();
fixture = TestBed.createComponent(UiCodeDisplayComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { Component } from '@angular/core';
@Component({
selector: 'ui-ui-code-display',
imports: [],
template: `
<p>
ui-code-display works!
</p>
`,
styles: ``
})
export class UiCodeDisplayComponent {
}

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { UiCodeDisplayService } from './ui-code-display.service';
describe('UiCodeDisplayService', () => {
let service: UiCodeDisplayService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(UiCodeDisplayService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,9 @@
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class UiCodeDisplayService {
constructor() { }
}

View File

@@ -0,0 +1,19 @@
/*
* Public API Surface of ui-code-display
*/
// Components
export * from './lib/components/code-snippet/code-snippet.component';
export * from './lib/components/inline-code/inline-code.component';
export * from './lib/components/code-block/code-block.component';
// Directives
export * from './lib/directives/copy-button.directive';
// Services
export * from './lib/services/syntax-highlighter.service';
export * from './lib/services/theme.service';
// Legacy exports (to be removed)
export * from './lib/ui-code-display.service';
export * from './lib/ui-code-display.component';