wally-ui 1.13.1 → 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.
- package/package.json +1 -1
- package/playground/showcase/package-lock.json +48 -0
- package/playground/showcase/package.json +1 -0
- package/playground/showcase/src/app/app.routes.server.ts +4 -0
- package/playground/showcase/src/app/components/ai/ai-chat/ai-chat.html +7 -2
- package/playground/showcase/src/app/components/ai/ai-chat/ai-chat.ts +12 -1
- package/playground/showcase/src/app/components/ai/ai-chat.service.spec.ts +16 -0
- package/playground/showcase/src/app/components/ai/ai-chat.service.ts +6 -0
- package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.html +14 -7
- package/playground/showcase/src/app/components/ai/ai-composer/ai-composer.ts +3 -1
- package/playground/showcase/src/app/components/ai/ai-message/ai-message.css +0 -0
- package/playground/showcase/src/app/components/ai/ai-message/ai-message.html +165 -0
- package/playground/showcase/src/app/components/ai/ai-message/ai-message.spec.ts +23 -0
- package/playground/showcase/src/app/components/ai/ai-message/ai-message.ts +51 -0
- package/playground/showcase/src/app/components/button/button.html +1 -1
- package/playground/showcase/src/app/components/button/button.ts +3 -3
- package/playground/showcase/src/app/components/selection-popover/selection-popover.css +0 -0
- package/playground/showcase/src/app/components/selection-popover/selection-popover.html +27 -0
- package/playground/showcase/src/app/components/selection-popover/selection-popover.spec.ts +23 -0
- package/playground/showcase/src/app/components/selection-popover/selection-popover.ts +205 -0
- package/playground/showcase/src/app/pages/documentation/chat-sdk/chat-sdk.html +1 -1
- package/playground/showcase/src/app/pages/documentation/components/button-docs/button-docs.examples.ts +10 -10
- package/playground/showcase/src/app/pages/documentation/components/button-docs/button-docs.html +2 -2
- package/playground/showcase/src/app/pages/documentation/components/components.html +27 -0
- package/playground/showcase/src/app/pages/documentation/components/components.routes.ts +4 -0
- package/playground/showcase/src/app/pages/documentation/components/selection-popover-docs/selection-popover-docs.css +1 -0
- package/playground/showcase/src/app/pages/documentation/components/selection-popover-docs/selection-popover-docs.examples.ts +324 -0
- package/playground/showcase/src/app/pages/documentation/components/selection-popover-docs/selection-popover-docs.html +506 -0
- package/playground/showcase/src/app/pages/documentation/components/selection-popover-docs/selection-popover-docs.ts +96 -0
- package/playground/showcase/src/app/pages/home/home.html +2 -2
- package/playground/showcase/src/styles.css +1 -0
|
@@ -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
|
-
(
|
|
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
|
-
(
|
|
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
|
-
(
|
|
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" (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
(
|
|
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" (
|
|
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 (
|
|
326
|
+
<wally-button (buttonClick)="openModal()">Open</wally-button>
|
|
327
327
|
|
|
328
328
|
<!-- GOOD: Use type="submit" for form submission -->
|
|
329
329
|
<form (ngSubmit)="save()">
|
package/playground/showcase/src/app/pages/documentation/components/button-docs/button-docs.html
CHANGED
|
@@ -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 (
|
|
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">
|
|
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">>_ </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
|
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/* Selection Popover Documentation Styles */
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// Selection Popover documentation code examples
|
|
2
|
+
export const SelectionPopoverCodeExamples = {
|
|
3
|
+
// Installation
|
|
4
|
+
installation: `npx wally-ui add selection-popover`,
|
|
5
|
+
|
|
6
|
+
// Import examples
|
|
7
|
+
import: `import { SelectionPopover } from './components/wally-ui/selection-popover/selection-popover';`,
|
|
8
|
+
componentImport: `@Component({
|
|
9
|
+
selector: 'app-example',
|
|
10
|
+
imports: [SelectionPopover],
|
|
11
|
+
templateUrl: './example.html'
|
|
12
|
+
})`,
|
|
13
|
+
|
|
14
|
+
// Basic usage
|
|
15
|
+
basicUsage: `<wally-selection-popover>
|
|
16
|
+
<article>
|
|
17
|
+
<h1>Article Title</h1>
|
|
18
|
+
<p>Select any text in this content to see the popover appear...</p>
|
|
19
|
+
</article>
|
|
20
|
+
</wally-selection-popover>`,
|
|
21
|
+
|
|
22
|
+
// === CUSTOM ACTIONS ===
|
|
23
|
+
|
|
24
|
+
customActions: `<wally-selection-popover (textSelected)="onTextSelected($event)">
|
|
25
|
+
<!-- Custom actions toolbar -->
|
|
26
|
+
<div popoverActions class="flex gap-1">
|
|
27
|
+
<button
|
|
28
|
+
class="px-3 py-2 text-sm text-[#0a0a0a] dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors">
|
|
29
|
+
Ask AI
|
|
30
|
+
</button>
|
|
31
|
+
<button
|
|
32
|
+
class="px-3 py-2 text-sm text-[#0a0a0a] dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors">
|
|
33
|
+
Copy
|
|
34
|
+
</button>
|
|
35
|
+
<button
|
|
36
|
+
class="px-3 py-2 text-sm text-[#0a0a0a] dark:text-white hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded-lg transition-colors">
|
|
37
|
+
Share
|
|
38
|
+
</button>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- Selectable content -->
|
|
42
|
+
<article class="prose dark:prose-invert">
|
|
43
|
+
<p>Select text to see custom actions...</p>
|
|
44
|
+
</article>
|
|
45
|
+
</wally-selection-popover>`,
|
|
46
|
+
|
|
47
|
+
customActionsTs: `export class MyComponent {
|
|
48
|
+
onTextSelected(text: string) {
|
|
49
|
+
console.log('Selected:', text);
|
|
50
|
+
// Handle the selected text
|
|
51
|
+
// This is called when any action is clicked
|
|
52
|
+
}
|
|
53
|
+
}`,
|
|
54
|
+
|
|
55
|
+
// === WITH WALLY BUTTON ===
|
|
56
|
+
|
|
57
|
+
withWallyButton: `<wally-selection-popover (textSelected)="askAboutSelection($event)">
|
|
58
|
+
<!-- Custom action with Wally Button -->
|
|
59
|
+
<div popoverActions>
|
|
60
|
+
<wally-button variant="ghost" size="sm">
|
|
61
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
|
|
62
|
+
stroke-width="1.5" stroke="currentColor" class="size-4">
|
|
63
|
+
<path stroke-linecap="round" stroke-linejoin="round"
|
|
64
|
+
d="M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z" />
|
|
65
|
+
</svg>
|
|
66
|
+
Ask AI
|
|
67
|
+
</wally-button>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Content -->
|
|
71
|
+
<article>
|
|
72
|
+
<p>Select text to ask AI about it...</p>
|
|
73
|
+
</article>
|
|
74
|
+
</wally-selection-popover>`,
|
|
75
|
+
|
|
76
|
+
withWallyButtonTs: `export class MyComponent {
|
|
77
|
+
askAboutSelection(text: string) {
|
|
78
|
+
// Send to AI chat
|
|
79
|
+
this.aiService.ask(\`Explain: \${text}\`);
|
|
80
|
+
}
|
|
81
|
+
}`,
|
|
82
|
+
|
|
83
|
+
// === PRODUCTION EXAMPLES ===
|
|
84
|
+
|
|
85
|
+
blogExample: `<!-- Blog Article with Selection Actions -->
|
|
86
|
+
<wally-selection-popover (textSelected)="handleAction($event)">
|
|
87
|
+
<div popoverActions class="flex gap-1">
|
|
88
|
+
<button
|
|
89
|
+
(click)="highlight()"
|
|
90
|
+
class="px-3 py-2 text-sm text-[#0a0a0a] dark:text-white hover:bg-yellow-100 dark:hover:bg-yellow-900 rounded transition-colors"
|
|
91
|
+
aria-label="Highlight text">
|
|
92
|
+
Highlight
|
|
93
|
+
</button>
|
|
94
|
+
<button
|
|
95
|
+
(click)="addComment()"
|
|
96
|
+
class="px-3 py-2 text-sm text-[#0a0a0a] dark:text-white hover:bg-blue-100 dark:hover:bg-blue-900 rounded transition-colors"
|
|
97
|
+
aria-label="Add comment">
|
|
98
|
+
Comment
|
|
99
|
+
</button>
|
|
100
|
+
<button
|
|
101
|
+
(click)="share()"
|
|
102
|
+
class="px-3 py-2 text-sm text-[#0a0a0a] dark:text-white hover:bg-green-100 dark:hover:bg-green-900 rounded transition-colors"
|
|
103
|
+
aria-label="Share selection">
|
|
104
|
+
Share
|
|
105
|
+
</button>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<article class="max-w-2xl mx-auto prose dark:prose-invert">
|
|
109
|
+
<h1>Understanding Angular Signals</h1>
|
|
110
|
+
<p>
|
|
111
|
+
Angular Signals represent a major shift in how we handle reactivity...
|
|
112
|
+
</p>
|
|
113
|
+
</article>
|
|
114
|
+
</wally-selection-popover>`,
|
|
115
|
+
|
|
116
|
+
blogExampleTs: `export class BlogComponent {
|
|
117
|
+
selectedText = signal<string>('');
|
|
118
|
+
|
|
119
|
+
handleAction(text: string) {
|
|
120
|
+
this.selectedText.set(text);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
highlight() {
|
|
124
|
+
console.log('Highlighting:', this.selectedText());
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
addComment() {
|
|
128
|
+
console.log('Adding comment to:', this.selectedText());
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
share() {
|
|
132
|
+
console.log('Sharing:', this.selectedText());
|
|
133
|
+
}
|
|
134
|
+
}`,
|
|
135
|
+
|
|
136
|
+
documentationExample: `<!-- Documentation with AI Assistant -->
|
|
137
|
+
<wally-selection-popover (textSelected)="askAI($event)">
|
|
138
|
+
<div popoverActions>
|
|
139
|
+
<wally-button variant="ghost" size="sm">
|
|
140
|
+
<svg class="size-4">...</svg>
|
|
141
|
+
Explain with AI
|
|
142
|
+
</wally-button>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div class="documentation-content">
|
|
146
|
+
<h2>API Reference</h2>
|
|
147
|
+
<pre><code>function useState<T>(initialValue: T): [T, (value: T) => void]</code></pre>
|
|
148
|
+
<p>Select any technical term to get AI explanation...</p>
|
|
149
|
+
</div>
|
|
150
|
+
</wally-selection-popover>`,
|
|
151
|
+
|
|
152
|
+
readingModeExample: `<!-- Reading Mode with Actions -->
|
|
153
|
+
<wally-selection-popover (textSelected)="onSelect($event)">
|
|
154
|
+
<div popoverActions class="flex items-center gap-2 px-2">
|
|
155
|
+
<button class="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded">
|
|
156
|
+
<svg class="size-4"><!-- Dictionary icon --></svg>
|
|
157
|
+
</button>
|
|
158
|
+
<button class="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded">
|
|
159
|
+
<svg class="size-4"><!-- Translate icon --></svg>
|
|
160
|
+
</button>
|
|
161
|
+
<button class="p-2 hover:bg-neutral-100 dark:hover:bg-neutral-800 rounded">
|
|
162
|
+
<svg class="size-4"><!-- Read aloud icon --></svg>
|
|
163
|
+
</button>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<div class="reading-content text-lg leading-relaxed">
|
|
167
|
+
<p>Select text to see reading assistance tools...</p>
|
|
168
|
+
</div>
|
|
169
|
+
</wally-selection-popover>`,
|
|
170
|
+
|
|
171
|
+
// === ADVANCED ===
|
|
172
|
+
|
|
173
|
+
minSelectionLength: `<!-- Custom minimum selection length -->
|
|
174
|
+
<wally-selection-popover>
|
|
175
|
+
<!-- By default, requires 3+ characters -->
|
|
176
|
+
<!-- Customize by handling selection in your code -->
|
|
177
|
+
|
|
178
|
+
<article>
|
|
179
|
+
<p>Select at least 3 characters...</p>
|
|
180
|
+
</article>
|
|
181
|
+
</wally-selection-popover>
|
|
182
|
+
|
|
183
|
+
<!-- Note: The component has a built-in 3-character minimum
|
|
184
|
+
to prevent accidental popover triggers on short selections -->`,
|
|
185
|
+
|
|
186
|
+
positioningBehavior: `<!-- Automatic viewport-aware positioning -->
|
|
187
|
+
<wally-selection-popover>
|
|
188
|
+
<!-- Popover automatically:
|
|
189
|
+
1. Centers above selection
|
|
190
|
+
2. Prevents overflow on screen edges
|
|
191
|
+
3. Adjusts position if too close to viewport boundaries
|
|
192
|
+
4. Uses position: fixed for scroll stability
|
|
193
|
+
-->
|
|
194
|
+
|
|
195
|
+
<article>
|
|
196
|
+
<p>Try selecting text near screen edges...</p>
|
|
197
|
+
</article>
|
|
198
|
+
</wally-selection-popover>`,
|
|
199
|
+
|
|
200
|
+
keyboardAccessibility: `<!-- Keyboard interaction -->
|
|
201
|
+
<wally-selection-popover (textSelected)="onSelect($event)">
|
|
202
|
+
<!-- Supports:
|
|
203
|
+
- ESC key to close popover
|
|
204
|
+
- Click outside to dismiss
|
|
205
|
+
- Full ARIA dialog attributes
|
|
206
|
+
- role="dialog" with aria-label
|
|
207
|
+
-->
|
|
208
|
+
|
|
209
|
+
<article>
|
|
210
|
+
<p>Select text and press ESC to close...</p>
|
|
211
|
+
</article>
|
|
212
|
+
</wally-selection-popover>`,
|
|
213
|
+
|
|
214
|
+
eventHandling: `<!-- Event handling example -->
|
|
215
|
+
<wally-selection-popover (textSelected)="handleSelection($event)">
|
|
216
|
+
<div popoverActions>
|
|
217
|
+
<wally-button variant="ghost" (buttonClick)="performAction()">
|
|
218
|
+
Custom Action
|
|
219
|
+
</wally-button>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<article>
|
|
223
|
+
<p>Content here...</p>
|
|
224
|
+
</article>
|
|
225
|
+
</wally-selection-popover>`,
|
|
226
|
+
|
|
227
|
+
eventHandlingTs: `export class MyComponent {
|
|
228
|
+
// This event fires when:
|
|
229
|
+
// 1. Any custom action is clicked
|
|
230
|
+
// 2. The fallback button is clicked (if no custom actions)
|
|
231
|
+
handleSelection(text: string) {
|
|
232
|
+
console.log('User selected:', text);
|
|
233
|
+
|
|
234
|
+
// The popover automatically closes after this event
|
|
235
|
+
// No need to manually clear selection
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
performAction() {
|
|
239
|
+
// Custom action logic
|
|
240
|
+
// The textSelected event will still fire with the selected text
|
|
241
|
+
}
|
|
242
|
+
}`,
|
|
243
|
+
|
|
244
|
+
// === API USAGE ===
|
|
245
|
+
|
|
246
|
+
fullExample: `<!-- Complete example with all features -->
|
|
247
|
+
<wally-selection-popover (textSelected)="onTextSelected($event)">
|
|
248
|
+
<!-- Custom actions (optional) -->
|
|
249
|
+
<div popoverActions class="flex gap-1">
|
|
250
|
+
<wally-button
|
|
251
|
+
variant="ghost"
|
|
252
|
+
size="sm"
|
|
253
|
+
[ariaLabel]="'Ask AI about selection'">
|
|
254
|
+
<svg class="size-4">...</svg>
|
|
255
|
+
Ask AI
|
|
256
|
+
</wally-button>
|
|
257
|
+
|
|
258
|
+
<wally-button
|
|
259
|
+
variant="ghost"
|
|
260
|
+
size="sm"
|
|
261
|
+
[ariaLabel]="'Copy to clipboard'">
|
|
262
|
+
<svg class="size-4">...</svg>
|
|
263
|
+
Copy
|
|
264
|
+
</wally-button>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
<!-- Selectable content -->
|
|
268
|
+
<article class="prose dark:prose-invert max-w-none">
|
|
269
|
+
<h1>Your Content</h1>
|
|
270
|
+
<p>Select any text to see the action toolbar...</p>
|
|
271
|
+
|
|
272
|
+
<h2>Features</h2>
|
|
273
|
+
<ul>
|
|
274
|
+
<li>Automatic positioning</li>
|
|
275
|
+
<li>Viewport awareness</li>
|
|
276
|
+
<li>Keyboard accessible</li>
|
|
277
|
+
<li>Zero flash rendering</li>
|
|
278
|
+
</ul>
|
|
279
|
+
</article>
|
|
280
|
+
</wally-selection-popover>`,
|
|
281
|
+
|
|
282
|
+
fullExampleTs: `export class MyComponent {
|
|
283
|
+
selectedText = signal<string>('');
|
|
284
|
+
|
|
285
|
+
onTextSelected(text: string) {
|
|
286
|
+
this.selectedText.set(text);
|
|
287
|
+
|
|
288
|
+
// Perform action based on user's custom button click
|
|
289
|
+
// For example: send to AI, copy to clipboard, etc.
|
|
290
|
+
|
|
291
|
+
console.log('Selected text:', text);
|
|
292
|
+
}
|
|
293
|
+
}`,
|
|
294
|
+
|
|
295
|
+
// === STYLING ===
|
|
296
|
+
|
|
297
|
+
customStyling: `<!-- Custom popover styling -->
|
|
298
|
+
<wally-selection-popover (textSelected)="onSelect($event)">
|
|
299
|
+
<!-- Use Tailwind classes for custom styling -->
|
|
300
|
+
<div popoverActions class="flex items-center gap-2 px-3 py-2">
|
|
301
|
+
<!-- Primary action with custom colors -->
|
|
302
|
+
<button class="
|
|
303
|
+
px-3 py-2 rounded-lg
|
|
304
|
+
bg-blue-500 text-white
|
|
305
|
+
hover:bg-blue-600
|
|
306
|
+
transition-colors">
|
|
307
|
+
Primary
|
|
308
|
+
</button>
|
|
309
|
+
|
|
310
|
+
<!-- Secondary action -->
|
|
311
|
+
<button class="
|
|
312
|
+
px-3 py-2 rounded-lg
|
|
313
|
+
text-neutral-700 dark:text-neutral-300
|
|
314
|
+
hover:bg-neutral-100 dark:hover:bg-neutral-800
|
|
315
|
+
transition-colors">
|
|
316
|
+
Secondary
|
|
317
|
+
</button>
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<article>
|
|
321
|
+
<p>Content...</p>
|
|
322
|
+
</article>
|
|
323
|
+
</wally-selection-popover>`,
|
|
324
|
+
};
|