bakery-ui
Version:
🥖 A delicious Angular UI component library with bakery-inspired theming and Storybook integration
1,131 lines (1,080 loc) • 44.2 kB
JavaScript
import * as i0 from '@angular/core';
import { Component, EventEmitter, Output, Input, ChangeDetectionStrategy, forwardRef } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import * as i2 from '@angular/material/core';
import { MatRippleModule } from '@angular/material/core';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatCardModule } from '@angular/material/card';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { trigger, state, transition, style, animate } from '@angular/animations';
class BakeryUi {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BakeryUi, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: BakeryUi, isStandalone: true, selector: "lib-bakery-ui", ngImport: i0, template: `
<p>
bakery-ui works!
</p>
`, isInline: true, styles: [""] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BakeryUi, decorators: [{
type: Component,
args: [{ selector: 'lib-bakery-ui', imports: [], template: `
<p>
bakery-ui works!
</p>
` }]
}] });
class BaguetteButton {
variant = 'primary';
size = 'md';
disabled = false;
loading = false;
fullWidth = false;
leftIcon;
rightIcon;
ariaLabel;
ariaDescribedBy;
clicked = new EventEmitter();
handleClick(event) {
if (!this.disabled && !this.loading) {
this.clicked.emit(event);
}
}
get buttonClasses() {
const baseClasses = [
'inline-flex items-center justify-center font-medium rounded-lg',
'transition-all duration-200 ease-in-out',
'focus:outline-none focus:ring-2 focus:ring-offset-2',
'disabled:opacity-50 disabled:cursor-not-allowed',
'transform hover:scale-105 active:scale-95',
'shadow-sm hover:shadow-md'
];
// Size classes - made more compact
const sizeClasses = {
sm: 'px-2 py-1 text-xs',
md: 'px-3 py-1.5 text-sm',
lg: 'px-4 py-2 text-base',
xl: 'px-5 py-2.5 text-lg'
};
// Variant classes with bakery theme
const variantClasses = {
primary: [
'bg-gradient-to-r from-amber-400 to-yellow-500',
'hover:from-amber-500 hover:to-yellow-600',
'text-amber-900 font-semibold',
'border border-amber-300',
'focus:ring-amber-500',
'shadow-amber-200'
],
secondary: [
'bg-gradient-to-r from-orange-100 to-amber-100',
'hover:from-orange-200 hover:to-amber-200',
'text-amber-800 font-medium',
'border border-amber-200',
'focus:ring-amber-400'
],
outline: [
'bg-transparent border-2 border-amber-400',
'hover:bg-amber-50 hover:border-amber-500',
'text-amber-700 font-medium',
'focus:ring-amber-400'
],
ghost: [
'bg-transparent hover:bg-amber-50',
'text-amber-700 font-medium',
'focus:ring-amber-400'
],
danger: [
'bg-gradient-to-r from-red-500 to-red-600',
'hover:from-red-600 hover:to-red-700',
'text-white font-medium',
'border border-red-400',
'focus:ring-red-500',
'shadow-red-200'
]
};
const classes = [
...baseClasses,
sizeClasses[this.size],
...variantClasses[this.variant]
];
if (this.fullWidth) {
classes.push('w-full');
}
return classes.join(' ');
}
get contentClasses() {
return this.loading ? 'opacity-70' : '';
}
get rippleColor() {
const rippleColors = {
primary: 'rgba(245, 158, 11, 0.3)',
secondary: 'rgba(217, 119, 6, 0.3)',
outline: 'rgba(245, 158, 11, 0.2)',
ghost: 'rgba(245, 158, 11, 0.1)',
danger: 'rgba(239, 68, 68, 0.3)'
};
return rippleColors[this.variant];
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BaguetteButton, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: BaguetteButton, isStandalone: true, selector: "baguette-button", inputs: { variant: "variant", size: "size", disabled: "disabled", loading: "loading", fullWidth: "fullWidth", leftIcon: "leftIcon", rightIcon: "rightIcon", ariaLabel: "ariaLabel", ariaDescribedBy: "ariaDescribedBy" }, outputs: { clicked: "clicked" }, ngImport: i0, template: `
<button
[class]="buttonClasses"
[disabled]="disabled || loading"
[attr.aria-label]="ariaLabel"
[attr.aria-describedby]="ariaDescribedBy"
(click)="handleClick($event)"
matRipple
[matRippleDisabled]="disabled || loading"
[matRippleColor]="rippleColor">
<!-- Loading spinner -->
<svg
*ngIf="loading"
class="animate-spin -ml-1 mr-2 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<!-- Left icon -->
<span *ngIf="leftIcon && !loading" [innerHTML]="leftIcon" class="mr-2"></span>
<!-- Button content -->
<span [class]="contentClasses">
<ng-content></ng-content>
</span>
<!-- Right icon -->
<span *ngIf="rightIcon" [innerHTML]="rightIcon" class="ml-2"></span>
</button>
`, isInline: true, styles: [":host{display:inline-block}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i2.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BaguetteButton, decorators: [{
type: Component,
args: [{ selector: 'baguette-button', standalone: true, imports: [CommonModule, MatButtonModule, MatRippleModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
<button
[class]="buttonClasses"
[disabled]="disabled || loading"
[attr.aria-label]="ariaLabel"
[attr.aria-describedby]="ariaDescribedBy"
(click)="handleClick($event)"
matRipple
[matRippleDisabled]="disabled || loading"
[matRippleColor]="rippleColor">
<!-- Loading spinner -->
<svg
*ngIf="loading"
class="animate-spin -ml-1 mr-2 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<!-- Left icon -->
<span *ngIf="leftIcon && !loading" [innerHTML]="leftIcon" class="mr-2"></span>
<!-- Button content -->
<span [class]="contentClasses">
<ng-content></ng-content>
</span>
<!-- Right icon -->
<span *ngIf="rightIcon" [innerHTML]="rightIcon" class="ml-2"></span>
</button>
`, styles: [":host{display:inline-block}\n"] }]
}], propDecorators: { variant: [{
type: Input
}], size: [{
type: Input
}], disabled: [{
type: Input
}], loading: [{
type: Input
}], fullWidth: [{
type: Input
}], leftIcon: [{
type: Input
}], rightIcon: [{
type: Input
}], ariaLabel: [{
type: Input
}], ariaDescribedBy: [{
type: Input
}], clicked: [{
type: Output
}] } });
class BagelInput {
label;
placeholder;
type = 'text';
size = 'md';
disabled = false;
readonly = false;
required = false;
clearable = false;
leftIcon;
rightIcon;
helperText;
error;
ariaLabel;
ariaDescribedBy;
autocomplete;
maxLength;
minLength;
min;
max;
step;
showCharCount = false;
inputId = `bagel-input-${Math.random().toString(36).substring(2, 9)}`;
valueChange = new EventEmitter();
inputFocus = new EventEmitter();
inputBlur = new EventEmitter();
enterPressed = new EventEmitter();
value = '';
onChange = (value) => { };
onTouched = () => { };
// ControlValueAccessor implementation
writeValue(value) {
this.value = value || '';
}
registerOnChange(fn) {
this.onChange = fn;
}
registerOnTouched(fn) {
this.onTouched = fn;
}
setDisabledState(isDisabled) {
this.disabled = isDisabled;
}
onInput(event) {
const target = event.target;
this.value = target.value;
this.onChange(this.value);
this.valueChange.emit(this.value);
}
onBlur() {
this.onTouched();
this.inputBlur.emit();
}
onFocus() {
this.inputFocus.emit();
}
onEnter() {
this.enterPressed.emit();
}
clearValue() {
this.value = '';
this.onChange(this.value);
this.valueChange.emit(this.value);
}
get labelClasses() {
return [
'block text-xs font-medium mb-1',
this.error ? 'text-red-700' : 'text-amber-800'
].join(' ');
}
get containerClasses() {
return 'relative';
}
get inputClasses() {
const baseClasses = [
'block w-full rounded-lg border-2 transition-all duration-200',
'focus:outline-none focus:ring-2 focus:ring-offset-1',
'disabled:opacity-50 disabled:cursor-not-allowed',
'placeholder-amber-400'
];
const sizeClasses = {
sm: 'px-2 py-1 text-xs',
md: 'px-3 py-1.5 text-sm',
lg: 'px-4 py-2 text-base'
};
const stateClasses = this.error
? [
'border-red-300 bg-red-50',
'focus:border-red-500 focus:ring-red-200',
'text-red-900'
]
: [
'border-amber-300 bg-amber-50',
'focus:border-amber-500 focus:ring-amber-200',
'text-amber-900',
'hover:border-amber-400'
];
const paddingAdjustment = [];
if (this.leftIcon)
paddingAdjustment.push('pl-10');
if (this.rightIcon || this.clearable)
paddingAdjustment.push('pr-10');
return [
...baseClasses,
sizeClasses[this.size],
...stateClasses,
...paddingAdjustment
].join(' ');
}
get helperClasses() {
return 'mt-0.5 text-xs text-amber-600';
}
get errorClasses() {
return 'mt-0.5 text-xs text-red-600 flex items-center';
}
get charCountClasses() {
const isNearLimit = this.maxLength && this.value?.length > this.maxLength * 0.8;
return [
'mt-1 text-xs text-right',
isNearLimit ? 'text-red-500' : 'text-amber-500'
].join(' ');
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BagelInput, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: BagelInput, isStandalone: true, selector: "bagel-input", inputs: { label: "label", placeholder: "placeholder", type: "type", size: "size", disabled: "disabled", readonly: "readonly", required: "required", clearable: "clearable", leftIcon: "leftIcon", rightIcon: "rightIcon", helperText: "helperText", error: "error", ariaLabel: "ariaLabel", ariaDescribedBy: "ariaDescribedBy", autocomplete: "autocomplete", maxLength: "maxLength", minLength: "minLength", min: "min", max: "max", step: "step", showCharCount: "showCharCount", inputId: "inputId" }, outputs: { valueChange: "valueChange", inputFocus: "inputFocus", inputBlur: "inputBlur", enterPressed: "enterPressed" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BagelInput),
multi: true
}
], ngImport: i0, template: `
<div class="w-full">
<!-- Label -->
<label
*ngIf="label"
[for]="inputId"
[class]="labelClasses">
{{ label }}
<span *ngIf="required" class="text-red-500 ml-1">*</span>
</label>
<!-- Input Container -->
<div [class]="containerClasses">
<!-- Left Icon -->
<div *ngIf="leftIcon" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span [innerHTML]="leftIcon" class="text-amber-600"></span>
</div>
<!-- Input Field -->
<input
[id]="inputId"
[type]="type"
[value]="value"
[placeholder]="placeholder"
[disabled]="disabled"
[readonly]="readonly"
[class]="inputClasses"
[attr.aria-label]="ariaLabel"
[attr.aria-describedby]="ariaDescribedBy"
[attr.autocomplete]="autocomplete"
[attr.maxlength]="maxLength"
[attr.minlength]="minLength"
[attr.min]="min"
[attr.max]="max"
[attr.step]="step"
(input)="onInput($event)"
(blur)="onBlur()"
(focus)="onFocus()"
(keyup.enter)="onEnter()"
/>
<!-- Right Icon / Clear Button -->
<div *ngIf="rightIcon || (clearable && value)" class="absolute inset-y-0 right-0 flex items-center">
<button
*ngIf="clearable && value && !disabled"
type="button"
class="pr-3 text-amber-600 hover:text-amber-700 focus:outline-none"
(click)="clearValue()"
[attr.aria-label]="'Clear ' + (label || 'input')">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
<span
*ngIf="rightIcon"
[innerHTML]="rightIcon"
class="pr-3 text-amber-600"></span>
</div>
</div>
<!-- Helper Text -->
<p *ngIf="helperText && !error" [class]="helperClasses">
{{ helperText }}
</p>
<!-- Error Message -->
<p *ngIf="error" [class]="errorClasses">
{{ error }}
</p>
<!-- Character Count -->
<p *ngIf="maxLength && showCharCount" [class]="charCountClasses">
{{ value?.length || 0 }}/{{ maxLength }}
</p>
</div>
`, isInline: true, styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatInputModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: BagelInput, decorators: [{
type: Component,
args: [{ selector: 'bagel-input', standalone: true, imports: [CommonModule, MatFormFieldModule, MatInputModule], changeDetection: ChangeDetectionStrategy.OnPush, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => BagelInput),
multi: true
}
], template: `
<div class="w-full">
<!-- Label -->
<label
*ngIf="label"
[for]="inputId"
[class]="labelClasses">
{{ label }}
<span *ngIf="required" class="text-red-500 ml-1">*</span>
</label>
<!-- Input Container -->
<div [class]="containerClasses">
<!-- Left Icon -->
<div *ngIf="leftIcon" class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<span [innerHTML]="leftIcon" class="text-amber-600"></span>
</div>
<!-- Input Field -->
<input
[id]="inputId"
[type]="type"
[value]="value"
[placeholder]="placeholder"
[disabled]="disabled"
[readonly]="readonly"
[class]="inputClasses"
[attr.aria-label]="ariaLabel"
[attr.aria-describedby]="ariaDescribedBy"
[attr.autocomplete]="autocomplete"
[attr.maxlength]="maxLength"
[attr.minlength]="minLength"
[attr.min]="min"
[attr.max]="max"
[attr.step]="step"
(input)="onInput($event)"
(blur)="onBlur()"
(focus)="onFocus()"
(keyup.enter)="onEnter()"
/>
<!-- Right Icon / Clear Button -->
<div *ngIf="rightIcon || (clearable && value)" class="absolute inset-y-0 right-0 flex items-center">
<button
*ngIf="clearable && value && !disabled"
type="button"
class="pr-3 text-amber-600 hover:text-amber-700 focus:outline-none"
(click)="clearValue()"
[attr.aria-label]="'Clear ' + (label || 'input')">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
<span
*ngIf="rightIcon"
[innerHTML]="rightIcon"
class="pr-3 text-amber-600"></span>
</div>
</div>
<!-- Helper Text -->
<p *ngIf="helperText && !error" [class]="helperClasses">
{{ helperText }}
</p>
<!-- Error Message -->
<p *ngIf="error" [class]="errorClasses">
{{ error }}
</p>
<!-- Character Count -->
<p *ngIf="maxLength && showCharCount" [class]="charCountClasses">
{{ value?.length || 0 }}/{{ maxLength }}
</p>
</div>
`, styles: [":host{display:block;width:100%}\n"] }]
}], propDecorators: { label: [{
type: Input
}], placeholder: [{
type: Input
}], type: [{
type: Input
}], size: [{
type: Input
}], disabled: [{
type: Input
}], readonly: [{
type: Input
}], required: [{
type: Input
}], clearable: [{
type: Input
}], leftIcon: [{
type: Input
}], rightIcon: [{
type: Input
}], helperText: [{
type: Input
}], error: [{
type: Input
}], ariaLabel: [{
type: Input
}], ariaDescribedBy: [{
type: Input
}], autocomplete: [{
type: Input
}], maxLength: [{
type: Input
}], minLength: [{
type: Input
}], min: [{
type: Input
}], max: [{
type: Input
}], step: [{
type: Input
}], showCharCount: [{
type: Input
}], inputId: [{
type: Input
}], valueChange: [{
type: Output
}], inputFocus: [{
type: Output
}], inputBlur: [{
type: Output
}], enterPressed: [{
type: Output
}] } });
class CupcakeCard {
variant = 'default';
size = 'md';
title;
subtitle;
image;
imageAlt;
imageLoading = 'lazy';
imageBadge;
avatar;
avatarAlt;
clickable = false;
disabled = false;
loading = false;
headerAction = false;
hasHeader = false;
hasFooter = false;
ariaLabel;
fullWidth = false;
cardClick = new EventEmitter();
handleClick(event) {
if (this.clickable && !this.disabled && !this.loading) {
this.cardClick.emit(event);
}
}
get cardClasses() {
const baseClasses = [
'rounded-xl overflow-hidden transition-all duration-200',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-amber-500'
];
const sizeClasses = {
sm: 'max-w-xs',
md: 'max-w-sm',
lg: 'max-w-md'
};
const variantClasses = {
default: [
'bg-white border border-amber-200',
'hover:border-amber-300 hover:shadow-md'
],
outlined: [
'bg-transparent border-2 border-amber-300',
'hover:border-amber-400 hover:bg-amber-50'
],
elevated: [
'bg-white shadow-lg shadow-amber-100',
'hover:shadow-xl hover:shadow-amber-200',
'border border-amber-100'
],
filled: [
'bg-gradient-to-br from-amber-50 to-orange-50',
'border border-amber-200',
'hover:from-amber-100 hover:to-orange-100'
]
};
const interactionClasses = [];
if (this.clickable) {
interactionClasses.push('cursor-pointer', 'hover:transform hover:scale-105', 'active:scale-95');
}
if (this.disabled) {
interactionClasses.push('opacity-50 cursor-not-allowed');
}
if (this.loading) {
interactionClasses.push('opacity-70 cursor-wait');
}
const classes = [
...baseClasses,
...variantClasses[this.variant],
...interactionClasses
];
if (!this.fullWidth) {
classes.push(sizeClasses[this.size]);
}
return classes.join(' ');
}
get headerClasses() {
const sizeClasses = {
sm: 'p-2 pb-1',
md: 'p-3 pb-2',
lg: 'p-4 pb-2'
};
return [
'border-b border-amber-100',
sizeClasses[this.size]
].join(' ');
}
get contentClasses() {
const sizeClasses = {
sm: 'p-2',
md: 'p-3',
lg: 'p-4'
};
const spacingClasses = [];
if (this.hasHeader && !this.image)
spacingClasses.push('pt-2');
if (this.hasFooter)
spacingClasses.push('pb-2');
if (this.image && !this.hasHeader)
spacingClasses.push('pt-0');
return [
sizeClasses[this.size],
...spacingClasses
].join(' ');
}
get footerClasses() {
const sizeClasses = {
sm: 'p-2 pt-1',
md: 'p-3 pt-2',
lg: 'p-4 pt-2'
};
return [
'border-t border-amber-100',
sizeClasses[this.size]
].join(' ');
}
get imageClasses() {
return [
'w-full h-32 object-cover',
'transition-transform duration-200',
this.clickable ? 'hover:scale-105' : ''
].join(' ');
}
get rippleColor() {
return 'rgba(245, 158, 11, 0.2)';
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CupcakeCard, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: CupcakeCard, isStandalone: true, selector: "cupcake-card", inputs: { variant: "variant", size: "size", title: "title", subtitle: "subtitle", image: "image", imageAlt: "imageAlt", imageLoading: "imageLoading", imageBadge: "imageBadge", avatar: "avatar", avatarAlt: "avatarAlt", clickable: "clickable", disabled: "disabled", loading: "loading", headerAction: "headerAction", hasHeader: "hasHeader", hasFooter: "hasFooter", ariaLabel: "ariaLabel", fullWidth: "fullWidth" }, outputs: { cardClick: "cardClick" }, ngImport: i0, template: `
<div
[class]="cardClasses"
[attr.role]="clickable ? 'button' : null"
[attr.tabindex]="clickable ? 0 : null"
[attr.aria-label]="ariaLabel"
(click)="handleClick($event)"
(keyup.enter)="handleClick($event)"
(keyup.space)="handleClick($event)"
matRipple
[matRippleDisabled]="!clickable"
[matRippleColor]="rippleColor">
<!-- Header -->
<div *ngIf="hasHeader" [class]="headerClasses">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div *ngIf="avatar" class="flex-shrink-0">
<img [src]="avatar" [alt]="avatarAlt" class="w-10 h-10 rounded-full object-cover">
</div>
<div>
<h3 *ngIf="title" class="text-lg font-semibold text-amber-900">{{ title }}</h3>
<p *ngIf="subtitle" class="text-sm text-amber-600">{{ subtitle }}</p>
</div>
</div>
<div *ngIf="headerAction" class="flex-shrink-0">
<ng-content select="[slot=header-action]"></ng-content>
</div>
</div>
</div>
<!-- Image -->
<div *ngIf="image" class="relative overflow-hidden">
<img
[src]="image"
[alt]="imageAlt"
[class]="imageClasses"
[loading]="imageLoading">
<div *ngIf="imageBadge" class="absolute top-2 right-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800">
{{ imageBadge }}
</span>
</div>
</div>
<!-- Content -->
<div [class]="contentClasses">
<ng-content></ng-content>
</div>
<!-- Footer -->
<div *ngIf="hasFooter" [class]="footerClasses">
<ng-content select="[slot=footer]"></ng-content>
</div>
</div>
`, isInline: true, styles: [":host{display:block;width:100%}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "ngmodule", type: MatRippleModule }, { kind: "directive", type: i2.MatRipple, selector: "[mat-ripple], [matRipple]", inputs: ["matRippleColor", "matRippleUnbounded", "matRippleCentered", "matRippleRadius", "matRippleAnimation", "matRippleDisabled", "matRippleTrigger"], exportAs: ["matRipple"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: CupcakeCard, decorators: [{
type: Component,
args: [{ selector: 'cupcake-card', standalone: true, imports: [CommonModule, MatCardModule, MatRippleModule], changeDetection: ChangeDetectionStrategy.OnPush, template: `
<div
[class]="cardClasses"
[attr.role]="clickable ? 'button' : null"
[attr.tabindex]="clickable ? 0 : null"
[attr.aria-label]="ariaLabel"
(click)="handleClick($event)"
(keyup.enter)="handleClick($event)"
(keyup.space)="handleClick($event)"
matRipple
[matRippleDisabled]="!clickable"
[matRippleColor]="rippleColor">
<!-- Header -->
<div *ngIf="hasHeader" [class]="headerClasses">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div *ngIf="avatar" class="flex-shrink-0">
<img [src]="avatar" [alt]="avatarAlt" class="w-10 h-10 rounded-full object-cover">
</div>
<div>
<h3 *ngIf="title" class="text-lg font-semibold text-amber-900">{{ title }}</h3>
<p *ngIf="subtitle" class="text-sm text-amber-600">{{ subtitle }}</p>
</div>
</div>
<div *ngIf="headerAction" class="flex-shrink-0">
<ng-content select="[slot=header-action]"></ng-content>
</div>
</div>
</div>
<!-- Image -->
<div *ngIf="image" class="relative overflow-hidden">
<img
[src]="image"
[alt]="imageAlt"
[class]="imageClasses"
[loading]="imageLoading">
<div *ngIf="imageBadge" class="absolute top-2 right-2">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800">
{{ imageBadge }}
</span>
</div>
</div>
<!-- Content -->
<div [class]="contentClasses">
<ng-content></ng-content>
</div>
<!-- Footer -->
<div *ngIf="hasFooter" [class]="footerClasses">
<ng-content select="[slot=footer]"></ng-content>
</div>
</div>
`, styles: [":host{display:block;width:100%}\n"] }]
}], propDecorators: { variant: [{
type: Input
}], size: [{
type: Input
}], title: [{
type: Input
}], subtitle: [{
type: Input
}], image: [{
type: Input
}], imageAlt: [{
type: Input
}], imageLoading: [{
type: Input
}], imageBadge: [{
type: Input
}], avatar: [{
type: Input
}], avatarAlt: [{
type: Input
}], clickable: [{
type: Input
}], disabled: [{
type: Input
}], loading: [{
type: Input
}], headerAction: [{
type: Input
}], hasHeader: [{
type: Input
}], hasFooter: [{
type: Input
}], ariaLabel: [{
type: Input
}], fullWidth: [{
type: Input
}], cardClick: [{
type: Output
}] } });
class ToastNotification {
type = 'info';
title;
message = '';
duration = 5000; // 5 seconds
closable = true;
actionText;
position = 'top-right';
persistent = false; // Don't auto-hide
visible = true;
closed = new EventEmitter();
action = new EventEmitter();
timeoutId;
ngOnInit() {
if (!this.persistent && this.duration > 0) {
this.timeoutId = window.setTimeout(() => {
this.onClose();
}, this.duration);
}
}
ngOnDestroy() {
if (this.timeoutId) {
clearTimeout(this.timeoutId);
}
}
onClose() {
this.visible = false;
this.closed.emit();
}
onAction() {
this.action.emit();
}
get toastClasses() {
const baseClasses = [
'max-w-xs w-full bg-white shadow-md rounded-md pointer-events-auto',
'ring-1 ring-black ring-opacity-5 overflow-hidden',
'transition-all duration-300 ease-in-out'
];
const typeClasses = {
success: 'border-l-2 border-green-400',
error: 'border-l-2 border-red-400',
warning: 'border-l-2 border-yellow-400',
info: 'border-l-2 border-blue-400'
};
return [
...baseClasses,
typeClasses[this.type],
'p-2'
].join(' ');
}
get iconClasses() {
const baseClasses = 'h-4 w-4';
const typeClasses = {
success: 'text-green-400',
error: 'text-red-400',
warning: 'text-yellow-400',
info: 'text-blue-400'
};
return [baseClasses, typeClasses[this.type]].join(' ');
}
get titleClasses() {
return 'text-xs font-medium text-gray-900';
}
get messageClasses() {
return this.title ? 'mt-0.5 text-xs text-gray-500' : 'text-xs text-gray-900';
}
get actionButtonClasses() {
const typeClasses = {
success: 'text-green-600 hover:text-green-500 focus:ring-green-500',
error: 'text-red-600 hover:text-red-500 focus:ring-red-500',
warning: 'text-yellow-600 hover:text-yellow-500 focus:ring-yellow-500',
info: 'text-blue-600 hover:text-blue-500 focus:ring-blue-500'
};
return [
'text-sm font-medium rounded-md',
'focus:outline-none focus:ring-2 focus:ring-offset-2',
'transition-colors duration-200',
typeClasses[this.type]
].join(' ');
}
get closeButtonClasses() {
return [
'bg-white rounded-md inline-flex text-gray-400',
'hover:text-gray-500 focus:outline-none',
'focus:ring-2 focus:ring-offset-2 focus:ring-amber-500',
'transition-colors duration-200'
].join(' ');
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToastNotification, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.14", type: ToastNotification, isStandalone: true, selector: "toast-notification", inputs: { type: "type", title: "title", message: "message", duration: "duration", closable: "closable", actionText: "actionText", position: "position", persistent: "persistent", visible: "visible" }, outputs: { closed: "closed", action: "action" }, ngImport: i0, template: `
<div
*ngIf="visible"
[@slideIn]
[class]="toastClasses"
[attr.role]="'alert'"
[attr.aria-live]="'polite'"
[attr.aria-atomic]="'true'">
<!-- Icon -->
<div class="flex-shrink-0">
<svg [class]="iconClasses" fill="currentColor" viewBox="0 0 20 20">
<!-- Success Icon -->
<path *ngIf="type === 'success'"
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
<!-- Error Icon -->
<path *ngIf="type === 'error'"
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd" />
<!-- Warning Icon -->
<path *ngIf="type === 'warning'"
fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd" />
<!-- Info Icon -->
<path *ngIf="type === 'info'"
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd" />
</svg>
</div>
<!-- Content -->
<div class="ml-2 flex-1">
<h4 *ngIf="title" [class]="titleClasses">{{ title }}</h4>
<p [class]="messageClasses">{{ message }}</p>
<!-- Action Button -->
<div *ngIf="actionText" class="mt-1">
<button
type="button"
[class]="actionButtonClasses"
(click)="onAction()">
{{ actionText }}
</button>
</div>
</div>
<!-- Close Button -->
<div *ngIf="closable" class="ml-2 flex-shrink-0 flex">
<button
type="button"
[class]="closeButtonClasses"
(click)="onClose()"
[attr.aria-label]="'Close notification'">
<svg class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
`, isInline: true, styles: [":host{display:block;position:fixed;z-index:1000}:host(.top-right){top:1rem;right:1rem}:host(.top-left){top:1rem;left:1rem}:host(.bottom-right){bottom:1rem;right:1rem}:host(.bottom-left){bottom:1rem;left:1rem}:host(.top-center){top:1rem;left:50%;transform:translate(-50%)}:host(.bottom-center){bottom:1rem;left:50%;transform:translate(-50%)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: MatSnackBarModule }], animations: [
trigger('slideIn', [
state('in', style({ transform: 'translateX(0)', opacity: 1 })),
transition('void => *', [
style({ transform: 'translateX(100%)', opacity: 0 }),
animate('300ms ease-in-out')
]),
transition('* => void', [
animate('300ms ease-in-out', style({ transform: 'translateX(100%)', opacity: 0 }))
])
])
], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.14", ngImport: i0, type: ToastNotification, decorators: [{
type: Component,
args: [{ selector: 'toast-notification', standalone: true, imports: [CommonModule, MatSnackBarModule], changeDetection: ChangeDetectionStrategy.OnPush, animations: [
trigger('slideIn', [
state('in', style({ transform: 'translateX(0)', opacity: 1 })),
transition('void => *', [
style({ transform: 'translateX(100%)', opacity: 0 }),
animate('300ms ease-in-out')
]),
transition('* => void', [
animate('300ms ease-in-out', style({ transform: 'translateX(100%)', opacity: 0 }))
])
])
], template: `
<div
*ngIf="visible"
[@slideIn]
[class]="toastClasses"
[attr.role]="'alert'"
[attr.aria-live]="'polite'"
[attr.aria-atomic]="'true'">
<!-- Icon -->
<div class="flex-shrink-0">
<svg [class]="iconClasses" fill="currentColor" viewBox="0 0 20 20">
<!-- Success Icon -->
<path *ngIf="type === 'success'"
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd" />
<!-- Error Icon -->
<path *ngIf="type === 'error'"
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
clip-rule="evenodd" />
<!-- Warning Icon -->
<path *ngIf="type === 'warning'"
fill-rule="evenodd"
d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z"
clip-rule="evenodd" />
<!-- Info Icon -->
<path *ngIf="type === 'info'"
fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clip-rule="evenodd" />
</svg>
</div>
<!-- Content -->
<div class="ml-2 flex-1">
<h4 *ngIf="title" [class]="titleClasses">{{ title }}</h4>
<p [class]="messageClasses">{{ message }}</p>
<!-- Action Button -->
<div *ngIf="actionText" class="mt-1">
<button
type="button"
[class]="actionButtonClasses"
(click)="onAction()">
{{ actionText }}
</button>
</div>
</div>
<!-- Close Button -->
<div *ngIf="closable" class="ml-2 flex-shrink-0 flex">
<button
type="button"
[class]="closeButtonClasses"
(click)="onClose()"
[attr.aria-label]="'Close notification'">
<svg class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
`, styles: [":host{display:block;position:fixed;z-index:1000}:host(.top-right){top:1rem;right:1rem}:host(.top-left){top:1rem;left:1rem}:host(.bottom-right){bottom:1rem;right:1rem}:host(.bottom-left){bottom:1rem;left:1rem}:host(.top-center){top:1rem;left:50%;transform:translate(-50%)}:host(.bottom-center){bottom:1rem;left:50%;transform:translate(-50%)}\n"] }]
}], propDecorators: { type: [{
type: Input
}], title: [{
type: Input
}], message: [{
type: Input
}], duration: [{
type: Input
}], closable: [{
type: Input
}], actionText: [{
type: Input
}], position: [{
type: Input
}], persistent: [{
type: Input
}], visible: [{
type: Input
}], closed: [{
type: Output
}], action: [{
type: Output
}] } });
/*
* Public API Surface of bakery-ui
*/
/**
* Generated bundle index. Do not edit.
*/
export { BagelInput, BaguetteButton, BakeryUi, CupcakeCard, ToastNotification };
//# sourceMappingURL=bakery-ui.mjs.map