From d0a15a3d3164edb45ab5fb2468102d2c5f3058aa Mon Sep 17 00:00:00 2001 From: Lumios Digital Date: Fri, 23 Jan 2026 23:19:24 -0300 Subject: [PATCH] Add vanilla JS bundle for script tag usage - Add src/vanilla.ts entry point that auto-initializes on page load - Update tsup.config.ts with vanilla ESM and IIFE builds - Bundle React internally for standalone use (~240KB) - Add CSS reset to prevent global styles from breaking toolbar - Update README with vanilla JS usage examples (Shopify, WordPress, CDN) --- README.md | 138 +- .../page-toolbar-css/styles.module.scss | 1485 +++++++++++++++++ src/vanilla.ts | 22 + tsup.config.ts | 122 ++ 4 files changed, 1766 insertions(+), 1 deletion(-) create mode 100644 src/components/page-toolbar-css/styles.module.scss create mode 100644 src/vanilla.ts create mode 100644 tsup.config.ts diff --git a/README.md b/README.md index cf161ec..85dd565 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ npm install agentation -D ## Usage +### React Component + ```tsx import { Agentation } from 'agentation'; @@ -25,6 +27,70 @@ function App() { The toolbar appears in the bottom-right corner. Click to activate, then click any element to annotate it. +### Vanilla JS / Script Tag + +For non-React projects, use the bundled script that works on any website. Perfect for adding visual feedback to Shopify themes, WordPress sites, or any HTML page. + +#### CDN / Script Tag + +Add this one line anywhere in your HTML: + +```html + +``` + +The script auto-initializes on page load. No configuration needed. + +#### Shopify Theme + +Load only in the Theme Editor for design-time feedback: + +```liquid +{% if request.design_mode %} + +{% endif %} +``` + +Add this to `layout/theme.liquid` before the closing `` tag. + +#### WordPress + +Load only for logged-in editors: + +```php + + + +``` + +Add this to your theme's `footer.php` or use `wp_enqueue_script` in `functions.php`. + +#### Any HTML Page + +Just add the script tag to your HTML: + +```html + + + + My Page + + +

My Website

+ + + + +``` + +**Notes:** +- Auto-initializes on page load (no init call needed) +- Works on ANY website (bundles React internally, no dependencies required) +- ~240KB bundle size (includes React + Agentation) +- All features work: annotations, settings, copy output, markers, area selection, etc. +- Toolbar appears in bottom-right corner +- Intended for development/design tools, not production sites + ## Features - **Click to annotate** – Click any element with automatic selector identification @@ -33,17 +99,87 @@ The toolbar appears in the bottom-right corner. Click to activate, then click an - **Area selection** – Drag to annotate any region, even empty space - **Animation pause** – Freeze CSS animations to capture specific states - **Structured output** – Copy markdown with selectors, positions, and context +- **Programmatic access** – Callback prop for direct integration with tools - **Dark/light mode** – Matches your preference or set manually - **Zero dependencies** – Pure CSS animations, no runtime libraries +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `onAnnotationAdd` | `(annotation: Annotation) => void` | - | Called when an annotation is created | +| `onAnnotationDelete` | `(annotation: Annotation) => void` | - | Called when an annotation is deleted | +| `onAnnotationUpdate` | `(annotation: Annotation) => void` | - | Called when an annotation is edited | +| `onAnnotationsClear` | `(annotations: Annotation[]) => void` | - | Called when all annotations are cleared | +| `onCopy` | `(markdown: string) => void` | - | Callback with markdown output when copy is clicked | +| `copyToClipboard` | `boolean` | `true` | Set to false to prevent writing to clipboard | + +### Programmatic Integration + +Use callbacks to receive annotation data directly: + +```tsx +import { Agentation, type Annotation } from 'agentation'; + +function App() { + const handleAnnotation = (annotation: Annotation) => { + // Structured data - no parsing needed + console.log(annotation.element); // "Button" + console.log(annotation.elementPath); // "body > div > button" + console.log(annotation.boundingBox); // { x, y, width, height } + console.log(annotation.cssClasses); // "btn btn-primary" + + // Send to your agent, API, etc. + sendToAgent(annotation); + }; + + return ( + <> + + + + ); +} +``` + +### Annotation Type + +```typescript +type Annotation = { + id: string; + x: number; // % of viewport width + y: number; // px from top (viewport if fixed) + comment: string; // User's note + element: string; // e.g., "Button" + elementPath: string; // e.g., "body > div > button" + timestamp: number; + + // Optional metadata (when available) + selectedText?: string; + boundingBox?: { x: number; y: number; width: number; height: number }; + nearbyText?: string; + cssClasses?: string; + nearbyElements?: string; + computedStyles?: string; + fullPath?: string; + accessibility?: string; + isMultiSelect?: boolean; + isFixed?: boolean; +}; +``` + ## How it works Agentation captures class names, selectors, and element positions so AI agents can `grep` for the exact code you're referring to. Instead of describing "the blue button in the sidebar," you give the agent `.sidebar > button.primary` and your feedback. ## Requirements -- React 18+ +- React 18+ (for React component usage) - Desktop browser (mobile not supported) +- No requirements for vanilla JS usage (React is bundled) ## Docs diff --git a/src/components/page-toolbar-css/styles.module.scss b/src/components/page-toolbar-css/styles.module.scss new file mode 100644 index 0000000..326e120 --- /dev/null +++ b/src/components/page-toolbar-css/styles.module.scss @@ -0,0 +1,1485 @@ +// Page Feedback Toolbar - CSS-only version (no framer-motion) +// Uses CSS animations for all transitions + +$blue: #3c82f7; +$red: #ff3b30; +$green: #34c759; + +// ============================================================================= +// Animation Keyframes +// ============================================================================= + +@keyframes toolbarEnter { + from { + opacity: 0; + transform: scale(0.5) rotate(90deg); + } + to { + opacity: 1; + transform: scale(1) rotate(0deg); + } +} + +@keyframes badgeEnter { + from { + opacity: 0; + transform: scale(0); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes scaleIn { + from { + opacity: 0; + transform: scale(0.85); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes scaleOut { + from { + opacity: 1; + transform: scale(1); + } + to { + opacity: 0; + transform: scale(0.85); + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: scale(0.85) translateY(8px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +@keyframes slideDown { + from { + opacity: 1; + transform: scale(1) translateY(0); + } + to { + opacity: 0; + transform: scale(0.85) translateY(8px); + } +} + +@keyframes markerIn { + 0% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.3); + } + 100% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } +} + +@keyframes markerOut { + 0% { + opacity: 1; + transform: translate(-50%, -50%) scale(1); + } + 100% { + opacity: 0; + transform: translate(-50%, -50%) scale(0.3); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes fadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} + +@keyframes tooltipIn { + from { + opacity: 0; + transform: translateX(-50%) translateY(2px) scale(0.98); + } + to { + opacity: 1; + transform: translateX(-50%) translateY(0) scale(1); + } +} + +@keyframes hoverHighlightIn { + from { + opacity: 0; + transform: scale(0.98); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes hoverTooltipIn { + from { + opacity: 0; + transform: scale(0.95) translateY(4px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +@keyframes settingsPanelIn { + from { + opacity: 0; + transform: translateY(10px) scale(0.95); + filter: blur(5px); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + filter: blur(0px); + } +} + +@keyframes settingsPanelOut { + from { + opacity: 1; + transform: translateY(0) scale(1); + filter: blur(0px); + } + to { + opacity: 0; + transform: translateY(20px) scale(0.95); + filter: blur(5px); + } +} + +// ============================================================================= +// Toolbar +// ============================================================================= + +.toolbar { + position: fixed; + bottom: 1.25rem; + right: 1.25rem; + width: 257px; + z-index: 100000; + font-family: + system-ui, + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Roboto, + sans-serif; + pointer-events: none; + transition: + left 0s, + top 0s, + right 0s, + bottom 0s; // Instant positioning changes +} + +.toolbarContainer { + user-select: none; + margin-left: auto; + align-self: flex-end; + display: flex; + align-items: center; + justify-content: center; + background: #1a1a1a; + color: #fff; + border: none; + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.2), + 0 4px 16px rgba(0, 0, 0, 0.1); + pointer-events: auto; + cursor: grab; + + // Animated properties + transition: + width 0.4s cubic-bezier(0.19, 1, 0.22, 1), + transform 0.4s cubic-bezier(0.19, 1, 0.22, 1); + + &.dragging { + // Remove transition while dragging for immediate feedback + transition: width 0.4s cubic-bezier(0.19, 1, 0.22, 1); + cursor: grabbing; + } + + &.entrance { + animation: toolbarEnter 0.5s cubic-bezier(0.34, 1.2, 0.64, 1) forwards; + } + + &.collapsed { + width: 44px; + height: 44px; + border-radius: 22px; + padding: 0; + cursor: pointer; + + svg { + margin-top: -1px; + } + + &:hover { + background: #2a2a2a; + } + + &:active { + transform: scale(0.95); + } + } + + &.expanded { + width: calc-size(auto, size); + height: 44px; + border-radius: 1.5rem; + padding: 0.375rem; + @supports not (width: calc-size(auto, size)) { + width: 257px; + } + } +} + +.toggleContent { + position: absolute; + display: flex; + align-items: center; + justify-content: center; + transition: opacity 0.1s cubic-bezier(0.19, 1, 0.22, 1); + + &.visible { + opacity: 1; + visibility: visible; + pointer-events: auto; + } + + &.hidden { + opacity: 0; + pointer-events: none; + } +} + +.controlsContent { + display: flex; + align-items: center; + gap: 0.375rem; + transition: + filter 0.8s cubic-bezier(0.19, 1, 0.22, 1), + opacity 0.8s cubic-bezier(0.19, 1, 0.22, 1), + transform 0.6s cubic-bezier(0.19, 1, 0.22, 1); + + &.visible { + opacity: 1; + filter: blur(0px); + transform: scale(1); + visibility: visible; + pointer-events: auto; + } + + &.hidden { + opacity: 0; + filter: blur(10px); + transform: scale(0.4); + pointer-events: none; + } +} + +.badge { + position: absolute; + top: -16px; + right: -16px; + user-select: none; + min-width: 18px; + height: 18px; + padding: 0 5px; + border-radius: 9px; + background: $blue; + color: white; + font-size: 0.625rem; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15); + opacity: 1; + transition: + transform 0.3s ease, + opacity 0.2s ease; + transform: scale(1); + + &.fadeOut { + opacity: 0; + transform: scale(0); + pointer-events: none; + } + + &.entrance { + animation: badgeEnter 0.3s cubic-bezier(0.34, 1.2, 0.64, 1) 0.4s both; + } +} + +.controlButton { + // Reset global button styles that might leak in + all: unset; + box-sizing: border-box; + cursor: pointer !important; + display: flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + padding: 0; + border-radius: 50%; + border: none; + background: transparent; + color: rgba(255, 255, 255, 0.85); + transition: + background-color 0.15s ease, + color 0.15s ease, + transform 0.1s ease, + opacity 0.2s ease; + + &:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.12); + color: #fff; + } + + &:active:not(:disabled) { + transform: scale(0.92); + } + + &:disabled { + opacity: 0.35; + cursor: not-allowed; + } + + &[data-active="true"] { + color: $blue; + background: rgba($blue, 0.25); + } + + &[data-danger]:hover:not(:disabled) { + background: rgba($red, 0.25); + color: $red; + } +} + +.divider { + width: 1px; + height: 12px; + background: rgba(255, 255, 255, 0.15); + margin: 0 0.125rem; +} + +// ============================================================================= +// Overlay +// ============================================================================= + +.overlay { + position: fixed; + inset: 0; + z-index: 99997; + pointer-events: none; + + > * { + pointer-events: auto; + } +} + +// ============================================================================= +// Hover Highlight +// ============================================================================= + +.hoverHighlight { + position: fixed; + border: 2px solid rgba($blue, 0.5); + border-radius: 4px; + pointer-events: none !important; + background: rgba($blue, 0.04); + box-sizing: border-box; + will-change: opacity; + contain: layout style; + + &.enter { + animation: hoverHighlightIn 0.12s ease-out forwards; + } +} + +.multiSelectOutline { + position: fixed; + border: 2px dashed rgba($green, 0.6); + border-radius: 4px; + pointer-events: none !important; + background: rgba($green, 0.05); + box-sizing: border-box; + will-change: opacity; + + &.enter { + animation: fadeIn 0.15s ease-out forwards; + } + + &.exit { + animation: fadeOut 0.15s ease-out forwards; + } +} + +.singleSelectOutline { + position: fixed; + border: 2px solid rgba($blue, 0.6); + border-radius: 4px; + pointer-events: none !important; + background: rgba($blue, 0.05); + box-sizing: border-box; + will-change: opacity; + + &.enter { + animation: fadeIn 0.15s ease-out forwards; + } + + &.exit { + animation: fadeOut 0.15s ease-out forwards; + } +} + +.hoverTooltip { + position: fixed; + font-size: 0.6875rem; + font-weight: 500; + color: #fff; + background: rgba(0, 0, 0, 0.85); + padding: 0.35rem 0.6rem; + border-radius: 0.375rem; + pointer-events: none !important; + white-space: nowrap; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + + &.enter { + animation: hoverTooltipIn 0.1s ease-out forwards; + } +} + +// ============================================================================= +// Markers Layer +// ============================================================================= + +.markersLayer { + position: absolute; + top: 0; + left: 0; + right: 0; + height: 0; + z-index: 99998; + pointer-events: none; + + > * { + pointer-events: auto; + } +} + +.fixedMarkersLayer { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 99998; + pointer-events: none; + + > * { + pointer-events: auto; + } +} + +// ============================================================================= +// Annotation Markers +// ============================================================================= + +.marker { + position: absolute; + width: 22px; + height: 22px; + background: $blue; + color: white; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.6875rem; + font-weight: 600; + transform: translate(-50%, -50%) scale(1); + opacity: 1; + cursor: pointer; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2); + user-select: none; + will-change: transform, opacity; + contain: layout style; + z-index: 1; + + &:hover { + z-index: 2; + } + + // Only apply transitions when NOT animating + &:not(.enter):not(.exit):not(.clearing) { + transition: + background-color 0.15s ease, + transform 0.1s ease; + } + + &.enter { + animation: markerIn 0.25s cubic-bezier(0.22, 1, 0.36, 1) both; + } + + &.exit { + animation: markerOut 0.2s ease-out both; + pointer-events: none; + } + + &.clearing { + animation: markerOut 0.15s ease-out both; + pointer-events: none; + } + + // Only apply hover scale when not animating + &:not(.enter):not(.exit):not(.clearing):hover { + transform: translate(-50%, -50%) scale(1.1); + } + + &.pending { + position: fixed; + background: $blue; + } + + &.fixed { + position: fixed; + } + + &.multiSelect { + background: $green; + width: 26px; + height: 26px; + border-radius: 6px; + font-size: 0.75rem; + + &.pending { + background: $green; + } + } + + &.hovered { + background: $red; + } +} + +.renumber { + display: block; + animation: renumberRoll 0.2s ease-out; +} + +@keyframes renumberRoll { + 0% { + transform: translateX(-40%); + opacity: 0; + } + 100% { + transform: translateX(0); + opacity: 1; + } +} + +.markerTooltip { + position: absolute; + top: calc(100% + 10px); + left: 50%; + transform: translateX(-50%); + z-index: 100002; + background: #1a1a1a; + padding: 0.625rem 0.75rem; + border-radius: 0.75rem; + box-shadow: + 0 4px 20px rgba(0, 0, 0, 0.3), + 0 0 0 1px rgba(255, 255, 255, 0.08); + min-width: 120px; + max-width: 200px; + pointer-events: none; + cursor: default; + + &.enter { + animation: tooltipIn 0.1s ease-out forwards; + } +} + +.markerQuote { + display: block; + font-size: 0.6875rem; + font-style: italic; + color: rgba(255, 255, 255, 0.5); + margin-bottom: 0.375rem; + line-height: 1.4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.markerNote { + display: block; + font-size: 0.75rem; + font-weight: 450; + line-height: 1.4; + color: #fff; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + padding-bottom: 2px; +} + +.markerHint { + display: block; + font-size: 0.625rem; + font-weight: 400; + color: rgba(255, 255, 255, 0.3); + margin-top: 0.375rem; + white-space: nowrap; +} + +// ============================================================================= +// Settings Panel +// ============================================================================= + +.settingsPanel { + position: absolute; + right: 5px; + bottom: calc(100% + 0.5rem); + background: white; + border-radius: 1rem; + padding: 13px 1rem 16px; + min-width: 205px; + box-shadow: + 0 1px 8px rgba(0, 0, 0, 0.25), + 0 0 0 1px rgba(0, 0, 0, 0.04); + transition: + background 0.25s ease, + box-shadow 0.25s ease; + + // Transition for all child elements + .settingsHeader, + .settingsBrand, + .settingsBrandSlash, + .settingsVersion, + .settingsSection, + .settingsLabel, + .cycleButton, + .cycleDot, + .dropdownButton, + .toggleLabel, + .customCheckbox, + .sliderLabel, + .slider, + .helpIcon, + .themeToggle { + transition: + background 0.25s ease, + color 0.25s ease, + border-color 0.25s ease; + } + + &.enter { + opacity: 1; + transform: translateY(0) scale(1); + filter: blur(0px); + transition: + opacity 0.2s ease, + transform 0.2s ease, + filter 0.2s ease; + } + + &.exit { + opacity: 0; + transform: translateY(8px) scale(0.95); + filter: blur(5px); + pointer-events: none; + transition: + opacity 0.1s ease, + transform 0.1s ease, + filter 0.1s ease; + } + + &.dark { + background: #1a1a1a; + box-shadow: + 0 4px 20px rgba(0, 0, 0, 0.3), + 0 0 0 1px rgba(255, 255, 255, 0.08); + + .settingsLabel { + color: rgba(255, 255, 255, 0.6); + } + + .settingsOption { + color: rgba(255, 255, 255, 0.85); + + &:hover { + background: rgba(255, 255, 255, 0.1); + } + + &.selected { + background: rgba(255, 255, 255, 0.15); + color: #fff; + } + } + + .toggleLabel { + color: rgba(255, 255, 255, 0.85); + } + } +} + +.settingsHeader { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 24px; + margin-bottom: 0.5rem; + padding-bottom: 9px; + border-bottom: 1px solid rgba(255, 255, 255, 0.07); +} + +.settingsBrand { + font-size: 0.8125rem; + font-weight: 600; + letter-spacing: -0.0094em; + color: #fff; +} + +.settingsBrandSlash { + color: rgba(255, 255, 255, 0.5); +} + +.settingsVersion { + font-size: 0.6875rem; + font-weight: 400; + color: rgba(255, 255, 255, 0.4); + margin-left: auto; + letter-spacing: -0.0094em; +} + +.settingsSection { + & + & { + margin-top: 0.5rem; + padding-top: calc(0.5rem + 2px); + border-top: 1px solid rgba(255, 255, 255, 0.07); + } +} + +.settingsRow { + display: flex; + align-items: center; + justify-content: space-between; + min-height: 24px; +} + +.dropdownContainer { + position: relative; +} + +.dropdownButton { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.25rem 0.5rem; + border: none; + border-radius: 0.375rem; + background: transparent; + font-size: 0.8125rem; + font-weight: 600; + color: #fff; + cursor: pointer; + transition: + background-color 0.15s ease, + color 0.15s ease; + letter-spacing: -0.0094em; + + &:hover { + background: rgba(255, 255, 255, 0.08); + } + + svg { + opacity: 0.6; + } +} + +// Old dropdown styles kept for potential future use +// @keyframes dropdownTextFade removed - using cycle button instead + +.cycleButton { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0; + border: none; + background: transparent; + font-size: 0.8125rem; + font-weight: 500; + color: #fff; + cursor: pointer; + letter-spacing: -0.0094em; + + &.light { + color: rgba(0, 0, 0, 0.85); + } +} + +@keyframes cycleTextIn { + 0% { + opacity: 0; + transform: translateY(-6px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.cycleButtonText { + display: inline-block; + animation: cycleTextIn 0.2s ease-out; +} + +.cycleDots { + display: flex; + flex-direction: column; + gap: 2px; +} + +.cycleDot { + width: 3px; + height: 3px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.3); + transform: scale(0.667); + transition: + background-color 0.25s ease-out, + transform 0.25s ease-out; + + &.active { + background: #fff; + transform: scale(1); + } + + &.light { + background: rgba(0, 0, 0, 0.2); + + &.active { + background: rgba(0, 0, 0, 0.7); + } + } +} + +.dropdownMenu { + position: absolute; + right: 0; + top: calc(100% + 0.25rem); + background: #1a1a1a; + border-radius: 0.5rem; + padding: 0.25rem; + min-width: 120px; + box-shadow: + 0 4px 20px rgba(0, 0, 0, 0.3), + 0 0 0 1px rgba(255, 255, 255, 0.1); + z-index: 10; + animation: scaleIn 0.15s ease-out; +} + +.dropdownItem { + width: 100%; + display: flex; + align-items: center; + padding: 0.5rem 0.625rem; + border: none; + border-radius: 0.375rem; + background: transparent; + font-size: 0.8125rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.85); + cursor: pointer; + text-align: left; + transition: + background-color 0.15s ease, + color 0.15s ease; + letter-spacing: -0.0094em; + + &:hover { + background: rgba(255, 255, 255, 0.08); + } + + &.selected { + background: rgba(255, 255, 255, 0.12); + color: #fff; + font-weight: 600; + } +} + +.settingsLabel { + font-size: 0.8125rem; + font-weight: 400; + letter-spacing: -0.0094em; + color: rgba(255, 255, 255, 0.5); + display: flex; + align-items: center; + gap: 0.125rem; + + &.light { + color: rgba(0, 0, 0, 0.5); + } +} + +.settingsLabelMarker { + margin-bottom: 10px; +} + +.settingsOptions { + display: flex; + gap: 0.25rem; +} + +.settingsOption { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + padding: 0.375rem 0.5rem; + border: none; + border-radius: 0.375rem; + background: transparent; + font-size: 0.6875rem; + font-weight: 500; + color: rgba(0, 0, 0, 0.7); + cursor: pointer; + transition: + background-color 0.15s ease, + color 0.15s ease; + + &:hover { + background: rgba(0, 0, 0, 0.05); + } + + &.selected { + background: rgba($blue, 0.15); + color: $blue; + } +} + +.sliderContainer { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 4px; + background: rgba(255, 255, 255, 0.15); + border-radius: 2px; + outline: none; + cursor: pointer; + + &::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 14px; + height: 14px; + background: white; + border-radius: 50%; + cursor: pointer; + transition: + transform 0.15s ease, + box-shadow 0.15s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + } + + &::-moz-range-thumb { + width: 14px; + height: 14px; + background: white; + border: none; + border-radius: 50%; + cursor: pointer; + transition: + transform 0.15s ease, + box-shadow 0.15s ease; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + } + + &:hover::-webkit-slider-thumb { + transform: scale(1.15); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4); + } + + &:hover::-moz-range-thumb { + transform: scale(1.15); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4); + } +} + +.sliderLabels { + display: flex; + justify-content: space-between; +} + +.sliderLabel { + font-size: 0.625rem; + font-weight: 500; + color: rgba(255, 255, 255, 0.4); + cursor: pointer; + transition: color 0.15s ease; + + &:hover { + color: rgba(255, 255, 255, 0.7); + } + + &.active { + color: rgba(255, 255, 255, 0.9); + } +} + +.colorOptions { + display: flex; + gap: 0.5rem; + margin-top: 0.375rem; + margin-bottom: 1px; +} + +.colorOption { + display: block; + width: 20px; + height: 20px; + border-radius: 50%; + border: 2px solid transparent; + cursor: pointer; + transition: transform 0.2s cubic-bezier(0.25, 1, 0.5, 1); + + &:hover { + transform: scale(1.15); + } + + &.selected { + transform: scale(0.83); + // border-color: rgba(255, 255, 255, 0.4); + } +} + +.colorOptionRing { + display: flex; + width: 24px; + height: 24px; + border: 2px solid transparent; + border-radius: 50%; + transition: border-color 0.3s ease; + + &.selected { + } +} + +.settingsToggle { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + + & + & { + margin-top: calc(0.5rem + 6px); + } + + input[type="checkbox"] { + position: absolute; + opacity: 0; + width: 0; + height: 0; + } +} + +.customCheckbox { + position: relative; + width: 14px; + height: 14px; + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 4px; + background: rgba(255, 255, 255, 0.05); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + transition: + background 0.25s ease, + border-color 0.25s ease; + + svg { + color: #1a1a1a; + opacity: 1; + transition: opacity 0.15s ease; + } + + input[type="checkbox"]:checked + & { + border-color: rgba(255, 255, 255, 0.3); + background: rgba(255, 255, 255, 1); + } + + // Light mode variant (unchecked state) + &.light { + border: 1px solid rgba(0, 0, 0, 0.15); + background: #fff; + + // Checked state in light mode + &.checked { + border-color: #1a1a1a; + background: #1a1a1a; + + svg { + color: #fff; + } + } + } +} + +.toggleLabel { + font-size: 0.8125rem; + font-weight: 400; + color: rgba(255, 255, 255, 0.5); + letter-spacing: -0.0094em; + display: flex; + align-items: center; + gap: 0.25rem; + + &.light { + color: rgba(0, 0, 0, 0.5); + } +} + +.helpIcon { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: help; + margin-left: 0; + + svg { + display: block; + transform: translateY(1px); + color: rgba(255, 255, 255, 0.4); + transition: color 0.15s ease; + } + + &:hover svg { + color: rgba(255, 255, 255, 0.7); + } + + &::after { + content: attr(data-tooltip); + position: absolute; + right: calc(100% + 8px); + top: 50%; + transform: translateY(-50%); + padding: 8px 10px; + background: #383838; + color: rgba(255, 255, 255, 0.7); + font-size: 11px; + font-weight: 400; + line-height: 1.4; + border-radius: 10px; + white-space: normal; + width: 152px; + text-align: left; + opacity: 0; + visibility: hidden; + transition: + opacity 0.15s ease, + visibility 0.15s ease; + pointer-events: none; + z-index: 100; + box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.28); + } + + &:hover::after { + opacity: 1; + visibility: visible; + transition-delay: 0.5s; + } +} + +// ============================================================================= +// Drag Selection +// ============================================================================= + +.dragSelection { + position: fixed; + top: 0; + left: 0; + border: 2px solid rgba($green, 0.6); + border-radius: 4px; + background: rgba($green, 0.08); + pointer-events: none; + z-index: 99997; + will-change: transform, width, height; + contain: layout style; +} + +.dragCount { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: $green; + color: white; + font-size: 0.875rem; + font-weight: 600; + padding: 0.25rem 0.5rem; + border-radius: 1rem; + min-width: 1.5rem; + text-align: center; +} + +.highlightsContainer { + position: fixed; + top: 0; + left: 0; + pointer-events: none; + z-index: 99996; +} + +.selectedElementHighlight { + position: fixed; + top: 0; + left: 0; + border: 2px solid rgba($green, 0.5); + border-radius: 4px; + background: rgba($green, 0.06); + pointer-events: none; + will-change: transform, width, height; + contain: layout style; +} + +// ============================================================================= +// Light Mode +// ============================================================================= + +.light { + // Toolbar container + &.toolbarContainer { + background: #fff; + color: rgba(0, 0, 0, 0.85); + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.08), + 0 4px 16px rgba(0, 0, 0, 0.06), + 0 0 0 1px rgba(0, 0, 0, 0.04); + + &.collapsed:hover { + background: #f5f5f5; + } + } + + // Control buttons in toolbar + &.controlButton { + color: rgba(0, 0, 0, 0.5); + + &:hover:not(:disabled) { + background: rgba(0, 0, 0, 0.06); + color: rgba(0, 0, 0, 0.85); + } + + &[data-active="true"] { + color: $blue; + background: rgba($blue, 0.15); + } + + &[data-danger]:hover:not(:disabled) { + background: rgba($red, 0.15); + color: $red; + } + } + + // Divider + &.divider { + background: rgba(0, 0, 0, 0.1); + } + + // Marker tooltip + &.markerTooltip { + background: #fff; + box-shadow: + 0 4px 20px rgba(0, 0, 0, 0.12), + 0 0 0 1px rgba(0, 0, 0, 0.06); + + .markerQuote { + color: rgba(0, 0, 0, 0.5); + } + + .markerNote { + color: rgba(0, 0, 0, 0.85); + } + + .markerHint { + color: rgba(0, 0, 0, 0.35); + } + } + + // Settings panel + &.settingsPanel { + background: #fff; + box-shadow: + 0 2px 8px rgba(0, 0, 0, 0.08), + 0 4px 16px rgba(0, 0, 0, 0.06), + 0 0 0 1px rgba(0, 0, 0, 0.04); + + .settingsHeader { + border-bottom-color: rgba(0, 0, 0, 0.08); + } + + .settingsBrand { + color: rgba(0, 0, 0, 0.85); + } + + .settingsBrandSlash { + color: rgba(0, 0, 0, 0.4); + } + + .settingsVersion { + color: rgba(0, 0, 0, 0.4); + } + + .settingsSection { + border-top-color: rgba(0, 0, 0, 0.08); + } + + .settingsLabel { + color: rgba(0, 0, 0, 0.5); + } + + .cycleButton { + color: rgba(0, 0, 0, 0.85); + } + + .cycleDot { + background: rgba(0, 0, 0, 0.2); + + &.active { + background: rgba(0, 0, 0, 0.7); + } + } + + .dropdownButton { + color: rgba(0, 0, 0, 0.85); + + &:hover { + background: rgba(0, 0, 0, 0.05); + } + } + + .toggleLabel { + color: rgba(0, 0, 0, 0.5); + } + + .customCheckbox { + border: 1px solid rgba(0, 0, 0, 0.15); + background: #fff; + + &.checked { + border-color: #1a1a1a; + background: #1a1a1a; + + svg { + color: #fff; + } + } + } + + .sliderLabel { + color: rgba(0, 0, 0, 0.4); + + &:hover { + color: rgba(0, 0, 0, 0.7); + } + + &.active { + color: rgba(0, 0, 0, 0.9); + } + } + + .slider { + background: rgba(0, 0, 0, 0.1); + + &::-webkit-slider-thumb { + background: #1a1a1a; + } + + &::-moz-range-thumb { + background: #1a1a1a; + } + } + + .helpIcon svg { + color: rgba(0, 0, 0, 0.6); + } + + .helpIcon:hover svg { + color: rgba(0, 0, 0, 0.7); + } + } +} + +// Theme toggle button (works in both modes) +.themeToggle { + display: flex; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + margin-left: 0.5rem; + border: none; + border-radius: 6px; + background: transparent; + color: rgba(255, 255, 255, 0.4); + cursor: pointer; + transition: + background-color 0.15s ease, + color 0.15s ease; + + &:hover { + background: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.8); + } + + .light & { + color: rgba(0, 0, 0, 0.4); + + &:hover { + background: rgba(0, 0, 0, 0.06); + color: rgba(0, 0, 0, 0.7); + } + } +} diff --git a/src/vanilla.ts b/src/vanilla.ts new file mode 100644 index 0000000..46a5700 --- /dev/null +++ b/src/vanilla.ts @@ -0,0 +1,22 @@ +import React from "react"; +import { createRoot } from "react-dom/client"; +import { PageFeedbackToolbarCSS } from "./components/page-toolbar-css"; + +export function init() { + if (typeof document === "undefined") return; + + const container = document.createElement("div"); + container.id = "agentation-root"; + document.body.appendChild(container); + + const root = createRoot(container); + root.render(React.createElement(PageFeedbackToolbarCSS)); +} + +if (typeof document !== "undefined") { + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", init); + } else { + init(); + } +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..86326d2 --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,122 @@ +import { defineConfig } from "tsup"; +import * as sass from "sass"; +import postcss from "postcss"; +import postcssModules from "postcss-modules"; +import * as path from "path"; +import * as fs from "fs"; +import type { Plugin } from "esbuild"; + +const pkg = JSON.parse(fs.readFileSync("./package.json", "utf-8")); +const VERSION = pkg.version; + +function scssModulesPlugin(): Plugin { + return { + name: "scss-modules", + setup(build) { + build.onLoad({ filter: /\.scss$/ }, async (args) => { + const isModule = args.path.includes(".module."); + const parentDir = path.basename(path.dirname(args.path)); + const baseName = path.basename(args.path, isModule ? ".module.scss" : ".scss"); + const styleId = `${parentDir}-${baseName}`; + + const result = sass.compile(args.path); + let css = result.css; + + if (isModule) { + let classNames: Record = {}; + const postcssResult = await postcss([ + postcssModules({ + getJSON(cssFileName, json) { + classNames = json; + }, + generateScopedName: "[name]__[local]___[hash:base64:5]", + }), + ]).process(css, { from: args.path }); + + css = postcssResult.css; + + const contents = ` +const css = ${JSON.stringify(css)}; +const classNames = ${JSON.stringify(classNames)}; + +if (typeof document !== 'undefined') { + let style = document.getElementById('feedback-tool-styles-${styleId}'); + if (!style) { + style = document.createElement('style'); + style.id = 'feedback-tool-styles-${styleId}'; + style.textContent = css; + document.head.appendChild(style); + } +} + +export default classNames; +`; + return { contents, loader: "js" }; + } + + const contents = ` +const css = ${JSON.stringify(css)}; +if (typeof document !== 'undefined') { + let style = document.getElementById('feedback-tool-styles-${styleId}'); + if (!style) { + style = document.createElement('style'); + style.id = 'feedback-tool-styles-${styleId}'; + style.textContent = css; + document.head.appendChild(style); + } +} +export default {}; +`; + return { contents, loader: "js" }; + }); + }, + }; +} + +export default defineConfig((options) => [ + { + entry: ["src/index.ts"], + format: ["cjs", "esm"], + dts: true, + splitting: false, + sourcemap: true, + clean: !options.watch, + external: ["react", "react-dom"], + esbuildPlugins: [scssModulesPlugin()], + define: { + __VERSION__: JSON.stringify(VERSION), + }, + banner: { + js: '"use client";', + }, + }, + { + entry: ["src/vanilla.ts"], + format: ["esm"], + outDir: "dist", + dts: true, + splitting: false, + sourcemap: true, + clean: false, + esbuildPlugins: [scssModulesPlugin()], + define: { + __VERSION__: JSON.stringify(VERSION), + "process.env.NODE_ENV": JSON.stringify("production"), + }, + }, + { + entry: { agentation: "src/vanilla.ts" }, + format: ["iife"], + outDir: "dist", + globalName: "Agentation", + splitting: false, + sourcemap: true, + clean: false, + minify: true, + esbuildPlugins: [scssModulesPlugin()], + define: { + __VERSION__: JSON.stringify(VERSION), + "process.env.NODE_ENV": JSON.stringify("production"), + }, + }, +]);