diff --git a/projects/demo-ui-essentials/src/app/demos/demos.routes.ts b/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
index a03f87d..0261efa 100644
--- a/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
+++ b/projects/demo-ui-essentials/src/app/demos/demos.routes.ts
@@ -18,6 +18,7 @@ import { ProgressDemoComponent } from './progress-demo/progress-demo.component';
import { CardDemoComponent } from './card-demo/card-demo.component';
import { ChipDemoComponent } from './chip-demo/chip-demo.component';
import { AppbarDemoComponent } from './appbar-demo/appbar-demo.component';
+import { BottomNavigationDemoComponent } from './bottom-navigation-demo/bottom-navigation-demo.component';
import { FontAwesomeDemoComponent } from './fontawesome-demo/fontawesome-demo.component';
import { ImageContainerDemoComponent } from './image-container-demo/image-container-demo.component';
import { CarouselDemoComponent } from './carousel-demo/carousel-demo.component';
@@ -46,9 +47,14 @@ import { TooltipDemoComponent } from './tooltip-demo/tooltip-demo.component';
import { AccordionDemoComponent } from './accordion-demo/accordion-demo.component';
import { PopoverDemoComponent } from './popover-demo/popover-demo.component';
import { AlertDemoComponent } from './alert-demo/alert-demo.component';
+import { SnackbarDemoComponent } from './snackbar-demo/snackbar-demo.component';
import { ToastDemoComponent } from './toast-demo/toast-demo.component';
import { TreeViewDemoComponent } from './tree-view-demo/tree-view-demo.component';
import { TimelineDemoComponent } from './timeline-demo/timeline-demo.component';
+import { StepperDemoComponent } from './stepper-demo/stepper-demo.component';
+import { FabMenuDemoComponent } from './fab-menu-demo/fab-menu-demo.component';
+import { EnhancedTableDemoComponent } from './enhanced-table-demo/enhanced-table-demo.component';
+import { SplitButtonDemoComponent } from './split-button-demo/split-button-demo.component';
@Component({
@@ -65,6 +71,10 @@ import { TimelineDemoComponent } from './timeline-demo/timeline-demo.component';
}
+ @case ("bottom-navigation") {
+
+ }
+
@case ("avatar") {
}
@@ -231,6 +241,10 @@ import { TimelineDemoComponent } from './timeline-demo/timeline-demo.component';
}
+ @case ("snackbar") {
+
+ }
+
@case ("toast") {
}
@@ -243,6 +257,22 @@ import { TimelineDemoComponent } from './timeline-demo/timeline-demo.component';
}
+ @case ("stepper") {
+
+ }
+
+ @case ("fab-menu") {
+
+ }
+
+ @case ("enhanced-table") {
+
+ }
+
+ @case ("split-button") {
+
+ }
+
}
`,
@@ -251,14 +281,14 @@ import { TimelineDemoComponent } from './timeline-demo/timeline-demo.component';
MenuDemoComponent, InputDemoComponent,
LayoutDemoComponent, RadioDemoComponent, CheckboxDemoComponent,
SearchDemoComponent, SwitchDemoComponent, ProgressDemoComponent,
- AppbarDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
+ AppbarDemoComponent, BottomNavigationDemoComponent, FontAwesomeDemoComponent, ImageContainerDemoComponent,
CarouselDemoComponent, VideoPlayerDemoComponent, ListDemoComponent,
ModalDemoComponent, DrawerDemoComponent, DatePickerDemoComponent, TimePickerDemoComponent,
GridSystemDemoComponent, SpacerDemoComponent, ContainerDemoComponent, PaginationDemoComponent,
SkeletonLoaderDemoComponent, EmptyStateDemoComponent, FileUploadDemoComponent, FormFieldDemoComponent,
AutocompleteDemoComponent, BackdropDemoComponent, OverlayContainerDemoComponent, LoadingSpinnerDemoComponent,
ProgressCircleDemoComponent, RangeSliderDemoComponent, DividerDemoComponent, TooltipDemoComponent, AccordionDemoComponent,
- PopoverDemoComponent, AlertDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent]
+ PopoverDemoComponent, AlertDemoComponent, SnackbarDemoComponent, ToastDemoComponent, TreeViewDemoComponent, TimelineDemoComponent, StepperDemoComponent, FabMenuDemoComponent, EnhancedTableDemoComponent, SplitButtonDemoComponent]
})
diff --git a/projects/demo-ui-essentials/src/app/demos/split-button-demo/split-button-demo.component.scss b/projects/demo-ui-essentials/src/app/demos/split-button-demo/split-button-demo.component.scss
new file mode 100644
index 0000000..3ac5457
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/split-button-demo/split-button-demo.component.scss
@@ -0,0 +1,63 @@
+@use '../../../../../shared-ui/src/styles/semantic/index' as *;
+
+.demo-container {
+ padding: $semantic-spacing-component-lg;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.demo-section {
+ margin-bottom: $semantic-spacing-layout-section-lg;
+
+ h3 {
+ font-family: map-get($semantic-typography-heading-h3, font-family);
+ font-size: map-get($semantic-typography-heading-h3, font-size);
+ font-weight: map-get($semantic-typography-heading-h3, font-weight);
+ line-height: map-get($semantic-typography-heading-h3, line-height);
+ color: $semantic-color-text-primary;
+ margin-bottom: $semantic-spacing-component-md;
+ border-bottom: $semantic-border-width-1 solid $semantic-color-border-secondary;
+ padding-bottom: $semantic-spacing-component-sm;
+ }
+}
+
+.demo-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: $semantic-spacing-component-md;
+ align-items: flex-start;
+ margin-bottom: $semantic-spacing-component-lg;
+}
+
+.demo-results {
+ padding: $semantic-spacing-component-md;
+ background: $semantic-color-surface-secondary;
+ border: $semantic-border-width-1 solid $semantic-color-border-secondary;
+ border-radius: $semantic-border-radius-md;
+
+ p {
+ font-family: map-get($semantic-typography-body-medium, font-family);
+ font-size: map-get($semantic-typography-body-medium, font-size);
+ font-weight: map-get($semantic-typography-body-medium, font-weight);
+ line-height: map-get($semantic-typography-body-medium, line-height);
+ color: $semantic-color-text-primary;
+ margin: $semantic-spacing-component-xs 0;
+
+ strong {
+ font-weight: $semantic-typography-font-weight-semibold;
+ color: $semantic-color-text-primary;
+ }
+ }
+}
+
+// Responsive adjustments
+@media (max-width: 768px) {
+ .demo-row {
+ flex-direction: column;
+ gap: $semantic-spacing-component-sm;
+ }
+
+ .demo-container {
+ padding: $semantic-spacing-component-md;
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/demos/split-button-demo/split-button-demo.component.ts b/projects/demo-ui-essentials/src/app/demos/split-button-demo/split-button-demo.component.ts
new file mode 100644
index 0000000..94ebde1
--- /dev/null
+++ b/projects/demo-ui-essentials/src/app/demos/split-button-demo/split-button-demo.component.ts
@@ -0,0 +1,211 @@
+import { Component } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { faDownload, faSave, faShare, faEdit, faTrash, faFile, faFileText, faPrint } from '@fortawesome/free-solid-svg-icons';
+import { SplitButtonComponent, SplitButtonMenuItem } from '../../../../../ui-essentials/src/lib/components/buttons/split-button';
+
+@Component({
+ selector: 'ui-split-button-demo',
+ standalone: true,
+ imports: [CommonModule, SplitButtonComponent],
+ template: `
+
+
Split Button Demo
+
+
+
+ Sizes
+
+ @for (size of sizes; track size) {
+
+ {{ size | titlecase }} Size
+
+ }
+
+
+
+
+
+ Variants
+
+ @for (variant of variants; track variant) {
+
+ {{ variant | titlecase }}
+
+ }
+
+
+
+
+
+ With Icons
+
+
+ Download
+
+
+
+ Save
+
+
+
+ Share
+
+
+
+
+
+
+ States
+
+
+ Disabled
+
+
+
+ Loading
+
+
+
+ Full Width
+
+
+
+
+
+
+ Complex Examples
+
+
+ Edit Document
+
+
+
+ New File
+
+
+
+
+
+
+ Interactive Results
+
+
Last Action: {{ lastAction || 'None' }}
+
Click Count: {{ clickCount }}
+
+
+
+ `,
+ styleUrl: './split-button-demo.component.scss'
+})
+export class SplitButtonDemoComponent {
+ sizes = ['small', 'medium', 'large'] as const;
+ variants = ['filled', 'tonal', 'outlined'] as const;
+
+ clickCount = 0;
+ lastAction = '';
+
+ // Icons
+ faDownload = faDownload;
+ faSave = faSave;
+ faShare = faShare;
+ faEdit = faEdit;
+ faTrash = faTrash;
+ faFile = faFile;
+ faFileText = faFileText;
+ faPrint = faPrint;
+
+ // Menu configurations
+ basicMenuItems: SplitButtonMenuItem[] = [
+ { label: 'Option 1', value: 'opt1' },
+ { label: 'Option 2', value: 'opt2' },
+ { label: 'Option 3', value: 'opt3' },
+ { label: 'Disabled Option', value: 'disabled', disabled: true }
+ ];
+
+ exportMenuItems: SplitButtonMenuItem[] = [
+ { label: 'Download PDF', value: 'pdf', icon: this.faFile },
+ { label: 'Download CSV', value: 'csv', icon: this.faFileText },
+ { label: 'Download Excel', value: 'excel', icon: this.faFile },
+ { label: 'Print', value: 'print', icon: this.faPrint }
+ ];
+
+ saveMenuItems: SplitButtonMenuItem[] = [
+ { label: 'Save As...', value: 'save-as' },
+ { label: 'Save Copy', value: 'save-copy' },
+ { label: 'Save Template', value: 'save-template' }
+ ];
+
+ shareMenuItems: SplitButtonMenuItem[] = [
+ { label: 'Share Link', value: 'share-link' },
+ { label: 'Share via Email', value: 'share-email' },
+ { label: 'Export & Share', value: 'export-share' }
+ ];
+
+ editMenuItems: SplitButtonMenuItem[] = [
+ { label: 'Quick Edit', value: 'quick-edit', icon: this.faEdit },
+ { label: 'Advanced Edit', value: 'advanced-edit', icon: this.faEdit },
+ { label: 'Delete', value: 'delete', icon: this.faTrash }
+ ];
+
+ fileMenuItems: SplitButtonMenuItem[] = [
+ { label: 'New Text File', value: 'new-text', icon: this.faFileText },
+ { label: 'New Document', value: 'new-doc', icon: this.faFile },
+ { label: 'New from Template', value: 'new-template', icon: this.faFile }
+ ];
+
+ handlePrimaryClick(action: string): void {
+ this.clickCount++;
+ this.lastAction = action;
+ console.log('Primary button clicked:', action);
+ }
+
+ handleMenuItemClick(data: { event: Event; item: SplitButtonMenuItem }): void {
+ this.clickCount++;
+ this.lastAction = `Menu item: ${data.item.label} (value: ${data.item.value})`;
+ console.log('Menu item clicked:', data);
+ }
+}
\ No newline at end of file
diff --git a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
index 635ad54..1f0c112 100644
--- a/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
+++ b/projects/demo-ui-essentials/src/app/features/dashboard/dashboard.component.ts
@@ -10,7 +10,7 @@ import {
faGripVertical, faArrowsAlt, faBoxOpen, faChevronLeft, faSpinner, faExclamationTriangle,
faCloudUploadAlt, faFileText, faListAlt, faCircle, faExpandArrowsAlt, faCircleNotch, faSliders,
faMinus, faInfoCircle, faChevronDown, faCaretUp, faExclamationCircle, faSitemap, faStream,
- faBell
+ faBell, faRoute, faChevronUp, faEllipsisV, faCut
} from '@fortawesome/free-solid-svg-icons';
import { DemoRoutes } from '../../demos';
import { ScrollContainerComponent } from '../../../../../ui-essentials/src/lib/layouts/scroll-container.component';
@@ -100,6 +100,10 @@ export class DashboardComponent {
faSitemap = faSitemap;
faStream = faStream;
faBell = faBell;
+ faRoute = faRoute;
+ faChevronUp = faChevronUp;
+ faEllipsisV = faEllipsisV;
+ faCut = faCut;
menuItems: any = []
@@ -153,6 +157,7 @@ export class DashboardComponent {
const dataDisplayChildren = [
this.createChildItem("accordion", "Accordion", this.faChevronDown),
this.createChildItem("table", "Tables", this.faTable),
+ this.createChildItem("enhanced-table", "Enhanced Table", this.faTable),
this.createChildItem("lists", "Lists", this.faList),
this.createChildItem("progress", "Progress Bars", this.faCheckSquare),
this.createChildItem("badge", "Badges", this.faCertificate),
@@ -168,8 +173,10 @@ export class DashboardComponent {
// Navigation category
const navigationChildren = [
this.createChildItem("appbar", "App Bars", this.faWindowMaximize),
+ this.createChildItem("bottom-navigation", "Bottom Navigation", this.faChevronUp),
this.createChildItem("menu", "Menus", this.faBars),
- this.createChildItem("pagination", "Pagination", this.faChevronLeft)
+ this.createChildItem("pagination", "Pagination", this.faChevronLeft),
+ this.createChildItem("stepper", "Stepper", this.faRoute)
];
this.addMenuItem("navigation", "Navigation", this.faCompass, navigationChildren);
@@ -183,13 +190,16 @@ export class DashboardComponent {
// Actions category
const actionsChildren = [
- this.createChildItem("buttons", "Buttons", this.faMousePointer)
+ this.createChildItem("buttons", "Buttons", this.faMousePointer),
+ this.createChildItem("fab-menu", "FAB Menu", this.faEllipsisV),
+ this.createChildItem("split-button", "Split Button", this.faCut)
];
this.addMenuItem("actions", "Actions", this.faMousePointer, actionsChildren);
// Feedback category
const feedbackChildren = [
this.createChildItem("alert", "Alert", faExclamationCircle),
+ this.createChildItem("snackbar", "Snackbar", this.faBell),
this.createChildItem("toast", "Toast", this.faBell),
this.createChildItem("chips", "Chips", this.faTags),
this.createChildItem("loading-spinner", "Loading Spinner", this.faSpinner),
diff --git a/projects/ui-essentials/src/lib/components/buttons/index.ts b/projects/ui-essentials/src/lib/components/buttons/index.ts
index 6435039..4a83a8a 100644
--- a/projects/ui-essentials/src/lib/components/buttons/index.ts
+++ b/projects/ui-essentials/src/lib/components/buttons/index.ts
@@ -2,4 +2,6 @@ export * from './button.component';
export * from './text-button.component';
export * from './ghost-button.component';
export * from './fab.component';
+export * from './fab-menu';
export * from './simple-button.component';
+export * from './split-button';
diff --git a/projects/ui-essentials/src/lib/components/buttons/split-button/index.ts b/projects/ui-essentials/src/lib/components/buttons/split-button/index.ts
new file mode 100644
index 0000000..67fdfc6
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/buttons/split-button/index.ts
@@ -0,0 +1 @@
+export * from './split-button.component';
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/buttons/split-button/split-button.component.scss b/projects/ui-essentials/src/lib/components/buttons/split-button/split-button.component.scss
new file mode 100644
index 0000000..2533134
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/buttons/split-button/split-button.component.scss
@@ -0,0 +1,393 @@
+@use "../../../../../../shared-ui/src/styles/semantic/index" as *;
+
+.ui-split-button {
+ // Reset and base styles
+ display: inline-flex;
+ align-items: center;
+ position: relative;
+ border-radius: $semantic-border-radius-md;
+ overflow: hidden;
+
+ // Size variants
+ &--small {
+ height: $semantic-spacing-9; // 2.25rem
+ font-size: $semantic-typography-font-size-sm;
+ line-height: $semantic-typography-line-height-tight;
+ }
+
+ &--medium {
+ height: $semantic-spacing-11; // 2.75rem
+ font-size: $semantic-typography-font-size-md;
+ line-height: $semantic-typography-line-height-normal;
+ }
+
+ &--large {
+ height: $semantic-spacing-12; // 3rem
+ font-size: $semantic-typography-font-size-lg;
+ line-height: $semantic-typography-line-height-normal;
+ }
+
+ // Full width variant
+ &--full-width {
+ width: 100%;
+ }
+
+ // Disabled state
+ &--disabled {
+ opacity: 0.38;
+ cursor: not-allowed;
+ pointer-events: none;
+ }
+
+ // Primary button part
+ &__primary {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ cursor: pointer;
+ text-decoration: none;
+ outline: none;
+ flex: 1;
+
+ // Typography
+ font-family: $semantic-typography-font-family-sans;
+ font-weight: $semantic-typography-font-weight-medium;
+ text-align: center;
+ text-transform: none;
+ letter-spacing: $semantic-typography-letter-spacing-normal;
+ white-space: nowrap;
+
+ // Interaction states
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+ box-sizing: border-box;
+
+ // Transitions
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+
+ // Size-specific padding
+ .ui-split-button--small & {
+ padding: 0 $semantic-spacing-3; // 0.75rem
+ }
+
+ .ui-split-button--medium & {
+ padding: 0 $semantic-spacing-4; // 1rem
+ }
+
+ .ui-split-button--large & {
+ padding: 0 $semantic-spacing-6; // 1.5rem
+ }
+ }
+
+ // Dropdown button part
+ &__dropdown {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: none;
+ cursor: pointer;
+ outline: none;
+ position: relative;
+
+ // Size-specific dimensions
+ .ui-split-button--small & {
+ width: $semantic-spacing-9; // 2.25rem
+ padding: 0 $semantic-spacing-2; // 0.5rem
+ }
+
+ .ui-split-button--medium & {
+ width: $semantic-spacing-11; // 2.75rem
+ padding: 0 $semantic-spacing-3; // 0.75rem
+ }
+
+ .ui-split-button--large & {
+ width: $semantic-spacing-12; // 3rem
+ padding: 0 $semantic-spacing-4; // 1rem
+ }
+
+ // Interaction states
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+ box-sizing: border-box;
+
+ // Transitions
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+
+ // Divider between buttons
+ &::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ top: 20%;
+ height: 60%;
+ width: 1px;
+ background-color: currentColor;
+ opacity: 0.3;
+ }
+ }
+
+ // Content wrapper
+ &__content {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ // Icon styles
+ &__icon {
+ display: inline-flex;
+ align-items: center;
+ flex-shrink: 0;
+
+ &--left {
+ margin-right: $semantic-spacing-2; // 0.5rem
+ }
+
+ &--right {
+ margin-left: $semantic-spacing-2; // 0.5rem
+ }
+
+ // Size-specific adjustments
+ .ui-split-button--small & {
+ font-size: $semantic-typography-font-size-xs;
+
+ &--left {
+ margin-right: $semantic-spacing-1-5; // 0.375rem
+ }
+
+ &--right {
+ margin-left: $semantic-spacing-1-5; // 0.375rem
+ }
+ }
+
+ .ui-split-button--medium & {
+ font-size: $semantic-typography-font-size-sm;
+ }
+
+ .ui-split-button--large & {
+ font-size: $semantic-typography-font-size-md;
+
+ &--left {
+ margin-right: $semantic-spacing-2-5; // 0.625rem
+ }
+
+ &--right {
+ margin-left: $semantic-spacing-2-5; // 0.625rem
+ }
+ }
+ }
+
+ // Dropdown arrow
+ &__arrow {
+ display: inline-flex;
+ align-items: center;
+ font-size: 0.75em;
+ transition: transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
+ }
+
+ // Dropdown menu
+ &__menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ min-width: 100%;
+ background: $semantic-color-surface-elevated;
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+ border-radius: $semantic-border-radius-md;
+ box-shadow: $semantic-shadow-dropdown;
+ z-index: $semantic-z-index-dropdown;
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(-$semantic-spacing-2);
+ transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1);
+
+ &--open {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+ }
+ }
+
+ // Menu item
+ &__menu-item {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ padding: $semantic-spacing-3 $semantic-spacing-4;
+ border: none;
+ background: transparent;
+ color: $semantic-color-text-primary;
+ font-family: $semantic-typography-font-family-sans;
+ font-size: $semantic-typography-font-size-sm;
+ font-weight: $semantic-typography-font-weight-normal;
+ text-align: left;
+ cursor: pointer;
+ transition: background-color 200ms cubic-bezier(0.4, 0, 0.2, 1);
+
+ &:hover {
+ background: $semantic-color-surface-container;
+ }
+
+ &:first-child {
+ border-top-left-radius: $semantic-border-radius-md;
+ border-top-right-radius: $semantic-border-radius-md;
+ }
+
+ &:last-child {
+ border-bottom-left-radius: $semantic-border-radius-md;
+ border-bottom-right-radius: $semantic-border-radius-md;
+ }
+ }
+
+ // Filled variant (primary)
+ &--filled {
+ .ui-split-button__primary,
+ .ui-split-button__dropdown {
+ background-color: $semantic-color-interactive-primary;
+ color: $semantic-color-on-primary;
+
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-primary;
+ transform: translateY(-1px);
+ box-shadow: $semantic-shadow-button-hover;
+ }
+
+ &:active:not(:disabled) {
+ transform: scale(0.98);
+ box-shadow: $semantic-shadow-button-active;
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+ }
+
+ .ui-split-button__dropdown:hover .ui-split-button__arrow {
+ transform: rotate(180deg);
+ }
+ }
+
+ // Tonal variant
+ &--tonal {
+ .ui-split-button__primary,
+ .ui-split-button__dropdown {
+ background-color: $semantic-color-surface-container;
+ color: $semantic-color-text-primary;
+
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-surface-container;
+ transform: translateY(-1px);
+ box-shadow: $semantic-shadow-button-hover;
+ filter: brightness(0.95);
+ }
+
+ &:active:not(:disabled) {
+ transform: scale(0.98);
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+ }
+
+ .ui-split-button__dropdown:hover .ui-split-button__arrow {
+ transform: rotate(180deg);
+ }
+ }
+
+ // Outlined variant
+ &--outlined {
+ border: $semantic-border-width-1 solid $semantic-color-border-primary;
+
+ .ui-split-button__primary,
+ .ui-split-button__dropdown {
+ background-color: transparent;
+ color: $semantic-color-interactive-primary;
+
+ &:hover:not(:disabled) {
+ background-color: $semantic-color-surface-container;
+ transform: translateY(-1px);
+ }
+
+ &:active:not(:disabled) {
+ background-color: $semantic-color-surface-container;
+ transform: scale(0.98);
+ filter: brightness(0.9);
+ }
+
+ &:focus-visible {
+ outline: 2px solid $semantic-color-primary;
+ outline-offset: 2px;
+ }
+ }
+
+ .ui-split-button__dropdown:hover .ui-split-button__arrow {
+ transform: rotate(180deg);
+ }
+ }
+
+ // Loading state
+ &--loading {
+ .ui-split-button__primary {
+ cursor: wait;
+
+ .ui-split-button__content {
+ opacity: 0;
+ }
+ }
+
+ .ui-split-button__dropdown {
+ pointer-events: none;
+ opacity: 0.5;
+ }
+ }
+
+ // Loader
+ &__loader {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ &__spinner {
+ width: $semantic-spacing-4; // 1rem
+ height: $semantic-spacing-4; // 1rem
+ border: 2px solid currentColor;
+ border-top: 2px solid transparent;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ opacity: 0.7;
+ }
+
+ @keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+ }
+
+ // Ripple effect
+ .ui-split-button__primary::after,
+ .ui-split-button__dropdown::after {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, transparent 70%);
+ transform: scale(0);
+ opacity: 0;
+ pointer-events: none;
+ transition: transform 0.3s ease, opacity 0.3s ease;
+ }
+
+ .ui-split-button__primary:active:not(:disabled)::after,
+ .ui-split-button__dropdown:active:not(:disabled)::after {
+ transform: scale(1);
+ opacity: 1;
+ transition: none;
+ }
+}
\ No newline at end of file
diff --git a/projects/ui-essentials/src/lib/components/buttons/split-button/split-button.component.ts b/projects/ui-essentials/src/lib/components/buttons/split-button/split-button.component.ts
new file mode 100644
index 0000000..616e5a9
--- /dev/null
+++ b/projects/ui-essentials/src/lib/components/buttons/split-button/split-button.component.ts
@@ -0,0 +1,173 @@
+import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ViewEncapsulation, HostListener } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
+import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
+import { faChevronDown } from '@fortawesome/free-solid-svg-icons';
+
+export type SplitButtonVariant = 'filled' | 'tonal' | 'outlined';
+export type SplitButtonSize = 'small' | 'medium' | 'large';
+export type SplitButtonIconPosition = 'left' | 'right';
+
+export interface SplitButtonMenuItem {
+ label: string;
+ value?: any;
+ icon?: IconDefinition;
+ disabled?: boolean;
+}
+
+@Component({
+ selector: 'ui-split-button',
+ standalone: true,
+ imports: [CommonModule, FontAwesomeModule],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ encapsulation: ViewEncapsulation.None,
+ template: `
+
+ `,
+ styleUrl: './split-button.component.scss'
+})
+export class SplitButtonComponent {
+ @Input() variant: SplitButtonVariant = 'filled';
+ @Input() size: SplitButtonSize = 'medium';
+ @Input() disabled: boolean = false;
+ @Input() loading: boolean = false;
+ @Input() type: 'button' | 'submit' | 'reset' = 'button';
+ @Input() fullWidth: boolean = false;
+ @Input() class: string = '';
+ @Input() icon?: IconDefinition;
+ @Input() iconPosition: SplitButtonIconPosition = 'left';
+ @Input() menuItems: SplitButtonMenuItem[] = [];
+
+ @Output() primaryClicked = new EventEmitter();
+ @Output() menuItemClicked = new EventEmitter<{ event: Event; item: SplitButtonMenuItem }>();
+ @Output() dropdownToggled = new EventEmitter();
+
+ isMenuOpen = false;
+ faChevronDown = faChevronDown;
+
+ @HostListener('document:click', ['$event'])
+ onDocumentClick(event: Event): void {
+ const target = event.target as Element;
+ const splitButton = target.closest('.ui-split-button');
+
+ if (!splitButton || !splitButton.contains(target)) {
+ this.closeMenu();
+ }
+ }
+
+ @HostListener('document:keydown', ['$event'])
+ onDocumentKeydown(event: KeyboardEvent): void {
+ if (event.key === 'Escape' && this.isMenuOpen) {
+ this.closeMenu();
+ }
+ }
+
+ handlePrimaryClick(event: Event): void {
+ if (!this.disabled && !this.loading) {
+ this.primaryClicked.emit(event);
+ this.closeMenu();
+ }
+ }
+
+ handleDropdownClick(event: Event): void {
+ if (!this.disabled && !this.loading) {
+ event.stopPropagation();
+ this.toggleMenu();
+ }
+ }
+
+ handleMenuItemClick(event: Event, item: SplitButtonMenuItem): void {
+ if (!item.disabled) {
+ event.stopPropagation();
+ this.menuItemClicked.emit({ event, item });
+ this.closeMenu();
+ }
+ }
+
+ private toggleMenu(): void {
+ this.isMenuOpen = !this.isMenuOpen;
+ this.dropdownToggled.emit(this.isMenuOpen);
+ }
+
+ private closeMenu(): void {
+ if (this.isMenuOpen) {
+ this.isMenuOpen = false;
+ this.dropdownToggled.emit(false);
+ }
+ }
+}
\ No newline at end of file