wally-ui 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +1 -1
  2. package/playground/showcase/package-lock.json +48 -0
  3. package/playground/showcase/package.json +1 -0
  4. package/playground/showcase/src/app/app.routes.server.ts +4 -0
  5. package/playground/showcase/src/app/components/ai/ai-chat/ai-chat.html +7 -2
  6. package/playground/showcase/src/app/components/ai/ai-chat/ai-chat.ts +12 -1
  7. package/playground/showcase/src/app/components/ai/ai-chat.service.spec.ts +16 -0
  8. package/playground/showcase/src/app/components/ai/ai-chat.service.ts +6 -0
  9. package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.html +14 -7
  10. package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.ts +3 -1
  11. package/playground/showcase/src/app/components/ai/ai-message/ai-message.css +0 -0
  12. package/playground/showcase/src/app/components/ai/ai-message/ai-message.html +165 -0
  13. package/playground/showcase/src/app/components/ai/ai-message/ai-message.spec.ts +23 -0
  14. package/playground/showcase/src/app/components/ai/ai-message/ai-message.ts +51 -0
  15. package/playground/showcase/src/app/components/button/button.html +1 -1
  16. package/playground/showcase/src/app/components/button/button.ts +3 -3
  17. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.html +1 -1
  18. package/playground/showcase/src/app/components/dropdown-menu/dropdown-menu-sub-content/dropdown-menu-sub-content.ts +112 -3
  19. package/playground/showcase/src/app/components/selection-popover/selection-popover.css +0 -0
  20. package/playground/showcase/src/app/components/selection-popover/selection-popover.html +27 -0
  21. package/playground/showcase/src/app/components/selection-popover/selection-popover.spec.ts +23 -0
  22. package/playground/showcase/src/app/components/selection-popover/selection-popover.ts +205 -0
  23. package/playground/showcase/src/app/pages/documentation/chat-sdk/chat-sdk.html +1 -1
  24. package/playground/showcase/src/app/pages/documentation/components/button-docs/button-docs.examples.ts +10 -10
  25. package/playground/showcase/src/app/pages/documentation/components/button-docs/button-docs.html +2 -2
  26. package/playground/showcase/src/app/pages/documentation/components/components.html +27 -0
  27. package/playground/showcase/src/app/pages/documentation/components/components.routes.ts +4 -0
  28. package/playground/showcase/src/app/pages/documentation/components/selection-popover-docs/selection-popover-docs.css +1 -0
  29. package/playground/showcase/src/app/pages/documentation/components/selection-popover-docs/selection-popover-docs.examples.ts +324 -0
  30. package/playground/showcase/src/app/pages/documentation/components/selection-popover-docs/selection-popover-docs.html +506 -0
  31. package/playground/showcase/src/app/pages/documentation/components/selection-popover-docs/selection-popover-docs.ts +96 -0
  32. package/playground/showcase/src/app/pages/home/home.html +2 -2
  33. package/playground/showcase/src/styles.css +1 -0
@@ -1,6 +1,8 @@
1
- import { Component, computed } from '@angular/core';
1
+ import { Component, computed, effect, ElementRef, signal } from '@angular/core';
2
2
  import { DropdownMenuSubService } from '../dropdown-menu-sub.service';
3
3
 
4
+ export type SubmenuPosition = 'right' | 'left' | 'bottom' | 'top';
5
+
4
6
  @Component({
5
7
  selector: 'wally-dropdown-menu-sub-content',
6
8
  imports: [],
@@ -8,12 +10,119 @@ import { DropdownMenuSubService } from '../dropdown-menu-sub.service';
8
10
  styleUrl: './dropdown-menu-sub-content.css'
9
11
  })
10
12
  export class DropdownMenuSubContent {
11
- constructor(public subService: DropdownMenuSubService) {}
13
+ calculatedPosition = signal<SubmenuPosition>('right');
14
+ isPositioned = signal<boolean>(false);
12
15
 
13
16
  positionClasses = computed(() => {
14
- return 'top-0 left-full';
17
+ const position = this.calculatedPosition();
18
+
19
+ const positionMap = {
20
+ right: 'top-0 left-full ml-1',
21
+ left: 'top-0 right-full mr-1',
22
+ bottom: 'left-0 top-full mt-1',
23
+ top: 'left-0 bottom-full mb-1'
24
+ };
25
+
26
+ return positionMap[position];
15
27
  });
16
28
 
29
+ constructor(
30
+ public subService: DropdownMenuSubService,
31
+ private elementRef: ElementRef
32
+ ) {
33
+ effect(() => {
34
+ if (this.subService.isOpen()) {
35
+ this.isPositioned.set(false);
36
+ setTimeout(() => {
37
+ const bestPosition = this.calculateBestPosition();
38
+ this.calculatedPosition.set(bestPosition);
39
+ this.isPositioned.set(true);
40
+ }, 0);
41
+ } else {
42
+ this.isPositioned.set(false);
43
+ }
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Measures available space around the trigger element in all directions.
49
+ * @returns Object containing trigger dimensions and available space, or null if trigger not found
50
+ */
51
+ private measureAvailableSpace(): {
52
+ triggerRect: DOMRect;
53
+ spaceAbove: number;
54
+ spaceBelow: number;
55
+ spaceLeft: number;
56
+ spaceRight: number;
57
+ } | null {
58
+ const triggerElement = this.elementRef.nativeElement.parentElement;
59
+
60
+ if (!triggerElement) {
61
+ return null;
62
+ }
63
+
64
+ const triggerRect = triggerElement.getBoundingClientRect();
65
+ const viewportWidth = window.innerWidth;
66
+ const viewportHeight = window.innerHeight;
67
+
68
+ return {
69
+ triggerRect,
70
+ spaceAbove: triggerRect.top,
71
+ spaceBelow: viewportHeight - triggerRect.bottom,
72
+ spaceLeft: triggerRect.left,
73
+ spaceRight: viewportWidth - triggerRect.right
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Calculates the best position for the submenu based on available viewport space.
79
+ * Prioritizes right/left (horizontal) over top/bottom (vertical) for submenus.
80
+ * Always uses the same priority order: right → left → bottom → top
81
+ * @returns The optimal submenu position
82
+ */
83
+ private calculateBestPosition(): SubmenuPosition {
84
+ const space = this.measureAvailableSpace();
85
+
86
+ if (!space) {
87
+ return 'right';
88
+ }
89
+
90
+ const menuDimensions = this.getMenuDimensions();
91
+ const MENU_MIN_HEIGHT = menuDimensions.height + 20;
92
+ const MENU_MIN_WIDTH = menuDimensions.width + 20;
93
+
94
+ // Always use same priority for submenus: right → left → bottom → top
95
+ if (space.spaceRight >= MENU_MIN_WIDTH) return 'right';
96
+ if (space.spaceLeft >= MENU_MIN_WIDTH) return 'left';
97
+ if (space.spaceBelow >= MENU_MIN_HEIGHT) return 'bottom';
98
+ return 'top';
99
+ }
100
+
101
+ /**
102
+ * Gets the submenu dimensions from the DOM.
103
+ * @returns Height and width of the submenu
104
+ */
105
+ private getMenuDimensions(): {
106
+ height: number;
107
+ width: number;
108
+ } {
109
+ const menuElement = this.elementRef.nativeElement.querySelector('[role="menu"]');
110
+
111
+ if (!menuElement) {
112
+ return {
113
+ height: 200,
114
+ width: 224
115
+ };
116
+ }
117
+
118
+ const rect = menuElement.getBoundingClientRect();
119
+
120
+ return {
121
+ height: rect.height || 200,
122
+ width: rect.width || 224
123
+ };
124
+ }
125
+
17
126
  onMouseEnter(): void {
18
127
  this.subService.setHoveringContent(true);
19
128
  this.subService.open();
@@ -0,0 +1,27 @@
1
+ <div class="relative">
2
+ <!-- Original content (where user will select text) -->
3
+ <ng-content></ng-content>
4
+
5
+ <!-- Floating popover -->
6
+ @if (isVisible()) {
7
+ <div #popover
8
+ class="fixed z-50 bg-white dark:bg-neutral-900 shadow-lg rounded-xl border border-neutral-300 dark:border-neutral-700 p-1 transition-opacity duration-150"
9
+ [class.opacity-0]="!isPositioned()"
10
+ [class.opacity-100]="isPositioned()"
11
+ [style.top.px]="adjustedPosition().top" [style.left.px]="adjustedPosition().left" role="dialog"
12
+ aria-label="Selection actions"
13
+ (click)="onPopoverClick()">
14
+
15
+ <!-- Customizable content via content projection (always rendered) -->
16
+ <div #customActionsSlot [class.hidden]="!hasCustomActionsSignal()">
17
+ <ng-content select="[popoverActions]"></ng-content>
18
+ </div>
19
+
20
+ <!-- Fallback: default button (hidden if custom actions exist) -->
21
+ <button [class.hidden]="hasCustomActionsSignal()"
22
+ class="px-3 py-2 text-[#0a0a0a] text-sm font-mono hover:bg-neutral-100 dark:text-white dark:hover:bg-neutral-800 rounded transition-colors cursor-pointer">
23
+ Default Action
24
+ </button>
25
+ </div>
26
+ }
27
+ </div>
@@ -0,0 +1,23 @@
1
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
2
+
3
+ import { SelectionPopover } from './selection-popover';
4
+
5
+ describe('SelectionPopover', () => {
6
+ let component: SelectionPopover;
7
+ let fixture: ComponentFixture<SelectionPopover>;
8
+
9
+ beforeEach(async () => {
10
+ await TestBed.configureTestingModule({
11
+ imports: [SelectionPopover]
12
+ })
13
+ .compileComponents();
14
+
15
+ fixture = TestBed.createComponent(SelectionPopover);
16
+ component = fixture.componentInstance;
17
+ fixture.detectChanges();
18
+ });
19
+
20
+ it('should create', () => {
21
+ expect(component).toBeTruthy();
22
+ });
23
+ });
@@ -0,0 +1,205 @@
1
+ import { AfterViewInit, Component, computed, ElementRef, HostListener, output, OutputEmitterRef, signal, ViewChild, WritableSignal } from '@angular/core';
2
+
3
+ /**
4
+ * Selection Popover Component
5
+ *
6
+ * Displays a floating action toolbar above selected text (similar to Medium, Notion, Google Docs).
7
+ * Uses Selection API for text detection and supports custom actions via content projection.
8
+ *
9
+ * @example
10
+ * ```html
11
+ * <wally-selection-popover (textSelected)="onTextSelected($event)">
12
+ * <div popoverActions>
13
+ * <button>Custom Action</button>
14
+ * </div>
15
+ * <article>Selectable content...</article>
16
+ * </wally-selection-popover>
17
+ * ```
18
+ */
19
+ @Component({
20
+ selector: 'wally-selection-popover',
21
+ standalone: true,
22
+ imports: [],
23
+ templateUrl: './selection-popover.html',
24
+ styleUrl: './selection-popover.css'
25
+ })
26
+ export class SelectionPopover implements AfterViewInit {
27
+ /** Reference to the popover element for positioning calculations */
28
+ @ViewChild('popover') popoverElement!: ElementRef<HTMLDivElement>;
29
+
30
+ /** Reference to custom actions slot for detecting projected content */
31
+ @ViewChild('customActionsSlot', { read: ElementRef }) customActionsSlot?: ElementRef;
32
+
33
+ /** Emits when text is selected (fallback action only) */
34
+ textSelected: OutputEmitterRef<string> = output<string>();
35
+
36
+ /** Current popover position (top, left in pixels) */
37
+ popoverPosition: WritableSignal<{ top: number; left: number; }> = signal<{ top: number; left: number; }>({ top: 0, left: 0 });
38
+
39
+ /** Whether custom actions are projected */
40
+ hasCustomActionsSignal: WritableSignal<boolean> = signal<boolean>(false);
41
+
42
+ /** Whether popover should be rendered in DOM */
43
+ isVisible: WritableSignal<boolean> = signal<boolean>(false);
44
+
45
+ /** Whether popover is positioned correctly (controls opacity) */
46
+ isPositioned: WritableSignal<boolean> = signal<boolean>(false);
47
+
48
+ /** Currently selected text */
49
+ selectedText: WritableSignal<string> = signal<string>('');
50
+
51
+ /**
52
+ * Computes adjusted position with viewport constraints
53
+ * Prevents popover from overflowing screen edges
54
+ */
55
+ adjustedPosition = computed(() => {
56
+ const position = this.popoverPosition();
57
+ const viewportWidth = window.innerWidth;
58
+
59
+ // Get real popover width if available, otherwise estimate
60
+ const popoverWidth = this.popoverElement?.nativeElement?.offsetWidth || 200;
61
+
62
+ let left = position.left;
63
+
64
+ // Prevent overflow on the right edge
65
+ if (left + popoverWidth > viewportWidth) {
66
+ left = viewportWidth - popoverWidth - 10;
67
+ }
68
+
69
+ // Prevent overflow on the left edge
70
+ if (left < 10) {
71
+ left = 10;
72
+ }
73
+
74
+ return {
75
+ top: position.top,
76
+ left: left
77
+ };
78
+ });
79
+
80
+ /**
81
+ * Handles mouseup event to detect text selection
82
+ * Uses setTimeout to ensure selection is complete
83
+ */
84
+ @HostListener('mouseup', ['$event'])
85
+ onMouseUp(event: MouseEvent) {
86
+ setTimeout(() => {
87
+ this.handleTextSelection();
88
+ }, 10);
89
+ }
90
+
91
+ /**
92
+ * Closes popover when clicking outside
93
+ * @param event - Mouse event from document click
94
+ */
95
+ @HostListener('document:mousedown', ['$event'])
96
+ onDocumentClick(event: MouseEvent): void {
97
+ if (this.isVisible() && this.popoverElement) {
98
+ const clickedInside = this.popoverElement.nativeElement.contains(event.target as Node);
99
+
100
+ if (!clickedInside) {
101
+ this.hide();
102
+ }
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Closes popover when ESC key is pressed
108
+ */
109
+ @HostListener('document:keydown.escape')
110
+ onEscape(): void {
111
+ this.hide();
112
+ }
113
+
114
+ /**
115
+ * Handles text selection and shows popover
116
+ *
117
+ * Two-step positioning algorithm to prevent visual "flash":
118
+ * 1. Render popover invisible (opacity: 0) at selection center
119
+ * 2. Wait for DOM render to get real popover width
120
+ * 3. Recalculate centered position with actual width
121
+ * 4. Fade in popover (opacity: 100) at correct position
122
+ *
123
+ * Uses position: fixed + getBoundingClientRect() for viewport-relative positioning
124
+ * No window.scrollY needed - stays in place during scroll
125
+ */
126
+ handleTextSelection(): void {
127
+ const selection = window.getSelection();
128
+
129
+ if (!selection || selection.toString().trim().length < 3) {
130
+ this.hide();
131
+ return;
132
+ }
133
+
134
+ const text = selection.toString().trim();
135
+ const range = selection.getRangeAt(0);
136
+ const rect = range.getBoundingClientRect();
137
+
138
+ this.selectedText.set(text);
139
+
140
+ // Calculate selection center point
141
+ const selectionCenterX = rect.left + (rect.width / 2);
142
+ const selectionTop = rect.top;
143
+
144
+ // Step 1: Render invisible for width calculation
145
+ this.isVisible.set(true);
146
+ this.isPositioned.set(false); // opacity: 0
147
+
148
+ // Step 2: Recalculate position with real popover width
149
+ setTimeout(() => {
150
+ if (this.popoverElement?.nativeElement) {
151
+ const popoverWidth = this.popoverElement.nativeElement.offsetWidth;
152
+
153
+ // Center popover above selection (60px offset)
154
+ this.popoverPosition.set({
155
+ top: selectionTop - 60,
156
+ left: selectionCenterX - (popoverWidth / 2)
157
+ });
158
+
159
+ // Fade in at correct position
160
+ this.isPositioned.set(true); // opacity: 100
161
+ }
162
+
163
+ // Detect projected custom actions
164
+ if (this.customActionsSlot?.nativeElement) {
165
+ const slot = this.customActionsSlot.nativeElement;
166
+ const firstChild = slot.children[0] as HTMLElement;
167
+ const hasContent = firstChild && firstChild.children.length > 0;
168
+ this.hasCustomActionsSignal.set(hasContent);
169
+ }
170
+ }, 0);
171
+ }
172
+
173
+ /**
174
+ * Hides popover and clears browser text selection
175
+ */
176
+ hide(): void {
177
+ this.isVisible.set(false);
178
+ this.isPositioned.set(false);
179
+
180
+ window.getSelection()?.removeAllRanges();
181
+ }
182
+
183
+ /**
184
+ * Handles any click inside the popover
185
+ * Emits selected text and closes popover
186
+ * This allows both custom actions and fallback button to work
187
+ */
188
+ onPopoverClick() {
189
+ const text = this.selectedText();
190
+ this.textSelected.emit(text);
191
+ this.hide();
192
+ }
193
+
194
+ /**
195
+ * Lifecycle hook - detects custom actions on component init
196
+ */
197
+ ngAfterViewInit(): void {
198
+ if (this.customActionsSlot?.nativeElement) {
199
+ const slot = this.customActionsSlot.nativeElement;
200
+ const firstChild = slot.children[0] as HTMLElement;
201
+ const hasContent = firstChild && firstChild.children.length > 0;
202
+ this.hasCustomActionsSignal.set(hasContent);
203
+ }
204
+ }
205
+ }
@@ -22,7 +22,7 @@
22
22
  </header>
23
23
 
24
24
  <!-- Live Demo -->
25
- <section id="live-demo" class="mb-12" aria-labelledby="demo-heading">
25
+ <section id="live-demo" class="mb-12 font-sans" aria-labelledby="demo-heading">
26
26
  <h2 id="demo-heading" class="text-[10px] sm:text-xs text-neutral-500 dark:text-neutral-500 uppercase tracking-wider mb-4">
27
27
  [ Live Demo ]
28
28
  </h2>
@@ -79,7 +79,7 @@ export const ButtonCodeExamples = {
79
79
  <wally-button
80
80
  variant="secondary"
81
81
  type="button"
82
- (click)="goToSignUp()">
82
+ (buttonClick)="goToSignUp()">
83
83
  Create Account
84
84
  </wally-button>
85
85
  </div>
@@ -109,14 +109,14 @@ export const ButtonCodeExamples = {
109
109
  <div class="flex gap-2 justify-end">
110
110
  <wally-button
111
111
  variant="ghost"
112
- (click)="closeModal()">
112
+ (buttonClick)="closeModal()">
113
113
  Cancel
114
114
  </wally-button>
115
115
 
116
116
  <wally-button
117
117
  variant="destructive"
118
118
  [loading]="isDeleting()"
119
- (click)="confirmDelete()">
119
+ (buttonClick)="confirmDelete()">
120
120
  Delete Account
121
121
  </wally-button>
122
122
  </div>
@@ -125,7 +125,7 @@ export const ButtonCodeExamples = {
125
125
  // Dashboard Actions
126
126
  dashboardExample: `<!-- Dashboard Actions -->
127
127
  <div class="dashboard-header">
128
- <wally-button variant="outline" (click)="exportData()">
128
+ <wally-button variant="outline" (buttonClick)="exportData()">
129
129
  Export
130
130
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
131
131
  stroke-width="1.5" stroke="currentColor" class="size-5">
@@ -134,7 +134,7 @@ export const ButtonCodeExamples = {
134
134
  </svg>
135
135
  </wally-button>
136
136
 
137
- <wally-button (click)="createNew()">
137
+ <wally-button (buttonClick)="createNew()">
138
138
  Create New
139
139
  <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
140
140
  stroke-width="2" stroke="currentColor" class="size-5">
@@ -162,7 +162,7 @@ export const ButtonCodeExamples = {
162
162
 
163
163
  // === EVENTS ===
164
164
 
165
- clickTemplate: `<wally-button (click)="handleClick()">Click Me</wally-button>`,
165
+ clickTemplate: `<wally-button (buttonClick)="handleClick()">Click Me</wally-button>`,
166
166
 
167
167
  clickMethod: `handleClick(): void {
168
168
  console.log('Button clicked!');
@@ -263,7 +263,7 @@ export const ButtonCodeExamples = {
263
263
  </wally-button>
264
264
 
265
265
  <!-- Programmatic navigation -->
266
- <wally-button (click)="navigateToProfile()">
266
+ <wally-button (buttonClick)="navigateToProfile()">
267
267
  View Profile
268
268
  </wally-button>`,
269
269
 
@@ -312,18 +312,18 @@ export class MyComponent {
312
312
  <wally-button
313
313
  [loading]="isLoading()"
314
314
  [disabled]="isDisabled()"
315
- (click)="handleSubmit()">
315
+ (buttonClick)="handleSubmit()">
316
316
  Submit
317
317
  </wally-button>`,
318
318
 
319
319
  // Button vs type="button"
320
320
  buttonTypeExplained: `<!-- GOOD: Explicit type prevents accidental form submission -->
321
- <wally-button type="button" (click)="openModal()">
321
+ <wally-button type="button" (buttonClick)="openModal()">
322
322
  Open
323
323
  </wally-button>
324
324
 
325
325
  <!-- CAUTION: Default type="button" is safe, but explicit is better -->
326
- <wally-button (click)="openModal()">Open</wally-button>
326
+ <wally-button (buttonClick)="openModal()">Open</wally-button>
327
327
 
328
328
  <!-- GOOD: Use type="submit" for form submission -->
329
329
  <form (ngSubmit)="save()">
@@ -484,7 +484,7 @@
484
484
  <div class="p-8 border-2 border-neutral-300 dark:border-neutral-700 bg-white dark:bg-[#0a0a0a]" role="img"
485
485
  aria-label="Live preview of button click event handling with feedback message">
486
486
  <div class="flex flex-col gap-2 text-center">
487
- <wally-button (click)="handleClick()">Click Me</wally-button>
487
+ <wally-button (buttonClick)="handleClick()">Click Me</wally-button>
488
488
  @if (clickMessage()) {
489
489
  <p class="text-sm text-green-600 dark:text-green-400 font-medium">
490
490
  {{ clickMessage() }}
@@ -753,7 +753,7 @@
753
753
  </thead>
754
754
  <tbody>
755
755
  <tr>
756
- <td class="p-4 font-mono text-blue-600 dark:text-blue-400">click</td>
756
+ <td class="p-4 font-mono text-blue-600 dark:text-blue-400">buttonClick</td>
757
757
  <td class="p-4 font-mono text-purple-600 dark:text-purple-400">void</td>
758
758
  <td class="p-4 text-gray-700 dark:text-gray-300">Emitted when button is clicked. Also handles
759
759
  navigation for link variant</td>
@@ -181,6 +181,33 @@
181
181
  </article>
182
182
 
183
183
 
184
+ <!-- Selection Popover Component -->
185
+ <article class="group border-b-2 border-neutral-300 dark:border-neutral-700 last:border-b-0" role="article"
186
+ aria-labelledby="selection-popover-heading">
187
+ <a href="/documentation/components/selection-popover"
188
+ class="block px-4 py-4 sm:py-5 bg-white dark:bg-[#0a0a0a] hover:bg-[#0a0a0a] dark:hover:bg-white transition-all duration-150 cursor-pointer"
189
+ aria-label="Navigate to Selection Popover component documentation with text selection detection, viewport-aware positioning, and zero-flash rendering">
190
+ <div class="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
191
+ <div class="flex-1">
192
+ <div class="flex items-center gap-3 mb-2">
193
+ <h3 id="selection-popover-heading"
194
+ class="text-base sm:text-lg font-bold text-[#0a0a0a] dark:text-white group-hover:text-white dark:group-hover:text-[#0a0a0a] uppercase tracking-wide transition-colors duration-150">
195
+ <span aria-hidden="true">&gt;_ </span>Selection Popover
196
+ </h3>
197
+ <span class="text-[10px] font-bold bg-blue-500 text-white px-2 py-1 uppercase tracking-wider"
198
+ aria-label="Status: New Component">
199
+ NEW
200
+ </span>
201
+ </div>
202
+ <p
203
+ class="text-xs sm:text-sm text-neutral-600 dark:text-neutral-400 group-hover:text-neutral-300 dark:group-hover:text-neutral-600 transition-colors duration-150">
204
+ Floating action toolbar that appears above selected text (Medium/Notion-style). Features Selection API integration, viewport-aware positioning, zero-flash rendering, and custom actions via content projection.
205
+ </p>
206
+ </div>
207
+ </div>
208
+ </a>
209
+ </article>
210
+
184
211
  <!-- Tooltip Component -->
185
212
  <article class="group border-b-2 border-neutral-300 dark:border-neutral-700 last:border-b-0" role="article"
186
213
  aria-labelledby="tooltip-heading">
@@ -28,5 +28,9 @@ export const componentsRoutes: Routes = [
28
28
  {
29
29
  path: 'dropdown-menu',
30
30
  loadComponent: () => import('./dropdown-menu-docs/dropdown-menu-docs').then(m => m.DropdownMenuDocs)
31
+ },
32
+ {
33
+ path: 'selection-popover',
34
+ loadComponent: () => import('./selection-popover-docs/selection-popover-docs').then(m => m.SelectionPopoverDocs)
31
35
  }
32
36
  ];