diff --git a/CLAUDE.md b/CLAUDE.md
index 92a745d..9a2fe77 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -100,21 +100,23 @@ ScrollStop monitors three distinct categories of sites with different blocking b
- Extended 4-hour block duration as stronger deterrent
- Comprehensive blocklist: Pornhub, OnlyFans, Xvideos, cam sites, etc.
-### iOS Welcome Walkthrough
+### Cross-Platform Configuration
-The iOS app includes an interactive setup walkthrough that guides users through:
+ScrollStop now features browser-based configuration that works identically on both iOS and macOS:
-1. **Welcome screen** - Explains ScrollStop's purpose
-2. **Safari extension setup** - Step-by-step instructions for enabling in Settings
-3. **iOS Shortcuts guidance** - Explains why/how to redirect social media apps to Safari
-4. **Completion screen** - Confirms setup and explains how ScrollStop works
+**Browser-Based Questionnaire:**
+
+1. **Choice Dialog Integration** - "Configure Activities" option in choice dialog
+2. **Multi-Step Setup** - 6 categories: household tasks, hobbies, current tasks, friends, goals, books
+3. **Cross-Platform Storage** - Uses browser.storage.local with localStorage fallback
+4. **Personalized Blocking Screen** - Shows user-specific activity suggestions
**Key Features:**
-- Remembers completion state using localStorage
-- Shows simplified version after walkthrough is completed
-- Includes placeholder for YouTube tutorial link
-- Responsive design with dark mode support
+- Identical functionality on iOS and macOS extensions
+- Real-time data persistence during configuration
+- Smart suggestion algorithm prioritizing current tasks
+- Fallback to default suggestions when no personal data available
### File Structure Notes
@@ -130,164 +132,110 @@ The iOS app includes an interactive setup walkthrough that guides users through:
- The build process flattens all files into one directory, so paths with directories will break
- Even though files are organized in subdirectories during development, manifest must reference them by filename only
-## Memories
-
-- **UI Components Policy**: Only use UIComponentsJS components for all web UI elements. No other UI frameworks or plain HTML/CSS elements. iOS app uses SwiftUI natively.
-- **Choice Dialog Feature**: When accessing blocked sites, users see a dialog with 3 options: Continue with ScrollStop (full functionality), Timer Only (no blocking), or Block Now (immediate block). Appears on every page reload.
-- **Adult Sites Blocking**: Third site category with 4-hour block duration (vs 1-hour for social/news). Comprehensive 89+ site blocklist including major platforms, streaming, cam sites, and hentai. Timer tracking and choice dialog work identically to other categories.
-- **Precommit Workflow**: ALWAYS run `npm run precommit` before committing. This automatically formats code with Prettier, then runs full validation (ESLint, tests, manifest validation). Never commit without this.
-- **Feature Branch Workflow**: ⚠️ **CRITICAL WORKFLOW RULE** ⚠️ When working with Claude Code, ALWAYS create feature branches for new development using descriptive names like `fix/reddit-choice-dialog-text-alignment` or `feature/add-system-language-detection`. Create pull requests to trigger CI/CD validation before merging to main. This ensures all tests pass and code quality is maintained. **NEVER COMMIT DIRECTLY TO MAIN BRANCH.** Use feature branches for EVERY bug fix or new feature. **BEFORE ANY `git commit` COMMAND, VERIFY YOU ARE ON A FEATURE BRANCH WITH `git branch --show-current`**
-- **npm Caching**: CI pipeline uses comprehensive caching strategy (setup-node + actions/cache) to reduce npm install time from ~5 minutes to ~30 seconds on cache hits.
-- **Dependabot Dependency Management**: Use GitHub Dependabot for automatic dependency updates instead of manual package version management. Prevents CI issues with bleeding-edge versions by using stable, tested version ranges.
-- **Stable Package Versions**: Use tilde (~) for patch version pinning on critical packages like Puppeteer to avoid registry sync issues with brand-new releases.
-
-### iOS App Personalization Features Implementation (Session Summary)
-
-**What We Built Today:**
-
-- **Comprehensive Questionnaire System**: Created QuestionnaireView with 5 detailed categories for personalized recommendations
-- **Bilingual Support**: Full German/English language support with LanguageManager
-- **Personalized Blocking Screen**: Updated blocking screen to show user-specific suggestions based on questionnaire data
-- **iOS UI Improvements**: Removed welcome screen and disabled dark mode for better UX
-
-**Key Features Implemented:**
-
-**1. QuestionnaireView System:**
-
-- **5 Categories**: Household tasks, hobbies, current tasks, friends, goals, books
-- **Smart Data Management**: Auto-deletion of current tasks after 2 weeks
-- **Progress Tracking**: Visual progress indicator with step-by-step navigation
-- **Real-time Persistence**: Data saved immediately as user adds items
-- **Bilingual Interface**: Complete German/English support throughout
+## Component Architecture
-**2. Personalized Blocking Screen:**
+**CRITICAL RULE: Use only HeadlessButton components for all interactive UI elements.**
-- **Smart Recommendations**: Shows actual user tasks instead of generic suggestions
-- **Priority System**: Current tasks → household → friends → hobbies → books → goals
-- **Randomization**: Picks random items from each category for variety
-- **Fallback System**: Default suggestions when no personal data available
-- **Cross-Platform Data**: Bridge between iOS app and Safari extension storage
+### Available Components
-**3. Bilingual Support (LanguageManager):**
+**Component Locations:**
-- **Complete Translation**: All UI elements, placeholders, descriptions in German/English
-- **Auto-Detection**: Detects system language preference
-- **Manual Override**: Users can switch languages in questionnaire
-- **Consistent Keys**: Standardized localization key system
+- **Primary**: `/Shared (Extension)/Resources/components/` - Extension components
+- **Source**: `/UIComponentsJs/` - Full component library (source of truth)
-**4. iOS App UX Improvements:**
+**Core Components:**
-- **Streamlined Walkthrough**: Removed welcome screen, starts directly with extension setup
-- **Light Mode Only**: Disabled dark mode for consistent appearance
-- **Integrated Questionnaire**: Replaced simple ProfileSetupView with comprehensive QuestionnaireView
+- `HeadlessButton` - All buttons must use this component
+- `HeadlessDialog` - Dialog containers with glassmorphism styling
+- `HeadlessDivider` - Section dividers (soft/hard styles)
+- `SwipeCards` - Activity card interface for blocking screen
+- `ActivityTimer` - 5-minute focus timer component
+- `QuestionnaireConfig` - Browser-based configuration interface
-**Technical Architecture:**
+### Component Usage Policy
-- **Data Bridge**: iOS app stores questionnaire data with `scrollstop_` prefixes for extension access
-- **Storage Strategy**: UserDefaults for iOS, browser.storage.local for extension with localStorage fallback
-- **Async Blocking Screen**: Updated blocking screen to handle async data loading
-- **Smart Suggestions**: Algorithm that prioritizes current tasks and mixes categories
+```javascript
+// ✅ CORRECT - Use HeadlessButton
+const button = new HeadlessButton('Configure Activities', {
+ color: 'blue',
+ onClick: () => this.handleClick(),
+});
-**User Experience Flow:**
+// ❌ WRONG - Never use custom CSS buttons
+;
+```
-1. **Setup Extension**: User enables Safari extension
-2. **Setup Shortcuts**: User configures iOS shortcuts for social media redirection
-3. **Personal Questionnaire**: User fills out 5 categories of personal data
-4. **Personalized Blocking**: When blocked, sees specific tasks from their questionnaire
-5. **Meaningful Alternatives**: Easy transition from scrolling to productive activities
+**CRITICAL RULE: If component is not available, make it available - NEVER write custom CSS.**
-**Data Categories & Examples:**
+Examples:
-- **Household Tasks**: "Do laundry", "Clean table", "Empty dishwasher"
-- **Hobbies**: "Cycling", "Gym", "Programming", "Guitar"
-- **Current Tasks**: "Study for exam", "Buy groceries" (auto-deleted after 2 weeks)
-- **Friends**: "Sister", "Flavio", "Samu" → becomes "Call Sister"
-- **Goals**: "Be more patient", "Exercise daily" → becomes "Work on: Be more patient"
-- **Books**: "Atomic Habits", "The Lean Startup" → becomes "Read 'Atomic Habits'"
+- Popup context: Load `button.js` and make `window.HeadlessButton` available
+- Missing component: Copy from `/UIComponentsJs/` to `/components/` and add to manifest.json
+- Always use standard components, never fallback to custom styling
-**Implementation Benefits:**
+### Color Scheme
-- **Higher Engagement**: Personal tasks more motivating than generic suggestions
-- **Cultural Support**: German users get native language experience
-- **Smart Prioritization**: Current tasks appear first when user is blocked
-- **Reduced Friction**: Easier to leave social media when you see specific alternatives
-- **Data Persistence**: Questionnaire data survives app updates and device changes
+- **Primary**: `color: 'blue'` (ScrollStop brand green)
+- **Secondary**: `color: 'zinc', outline: true` (gray outline)
+- **Destructive**: `color: 'red'` (delete/cancel actions)
-**Technical Lessons:**
+## Implementation Standards
-- **Async Patterns**: Updated blocking screen methods to handle async suggestion generation
-- **Cross-Platform Storage**: Used consistent key prefixes for data sharing between app and extension
-- **Language Management**: Centralized translation system with fallback to keys
-- **State Management**: Proper state handling for multi-step questionnaire flow
+- **Choice Dialog**: Continue (blue), Configure Activities (zinc outline), Block Now (red outline) with HeadlessDivider separation
+- **Questionnaire**: Add (blue), Previous (zinc outline), Cancel (red outline), Next/Finish (blue) with fallback support
+- **Popup**: Configure Activities (blue) with minimal CSS, HeadlessButton for all interactive elements
+- **Cross-Platform**: Extensions use HeadlessButton, iOS app uses SwiftUI
+- **Adult Sites Blocking**: 4-hour blocks, 89+ sites, timer tracking and choice dialog
+- **Workflow**: Feature branches, `npm run precommit`, PR validation, Dependabot updates
-**Code Quality Improvements:**
+## Custom CSS Cleanup
-- **Comprehensive Error Handling**: Graceful fallbacks when personal data unavailable
-- **Memory Management**: Proper cleanup of old current tasks
-- **Type Safety**: Strong typing throughout Swift components
-- **Modular Design**: Separate concerns for data management, UI, and storage
+**NEVER use custom CSS for buttons.** Files requiring cleanup:
-**User Impact:**
-The app now transforms from a simple blocking tool into a personalized productivity assistant that knows the user's specific goals, tasks, and relationships, making it much easier to break the scrolling habit by providing meaningful, actionable alternatives.
+- `/modules/choice-dialog/choice-dialog.js` - Simple dialog fallback has extensive custom CSS
+- `/modules/blocking-screen/blocking-screen.js` - Custom button styles
+- `/components/activity-timer.js` - Custom button implementations
+- **Always use HeadlessButton components. No fallback strategies needed - if you have a component, you have a component.**
-### GitHub Pages Component Integration Lessons (Session Summary)
+### Key Features
-**Critical Web Component Styling Rules:**
+**Personalized Blocking Screen:**
-**NEVER override TailwindUI/HeadlessUI component styles with aggressive CSS:**
+- Browser-based questionnaire system with 6 categories: household tasks, hobbies, current tasks, friends, goals, books
+- Swipeable activity cards showing user's personal data instead of generic suggestions
+- Cross-platform compatibility - identical on iOS and macOS
+- Smart suggestion algorithm prioritizing current tasks
-- Components have sophisticated built-in dark/light mode styling (e.g., `text-zinc-950 dark:text-white`)
-- Custom CSS with `!important` rules breaks component functionality
-- Components are designed to handle their own theming automatically
+**Modern Blocking Interface:**
-**Proper Implementation:**
+- Tinder-like swipe cards for activity selection (right = accept, left = reject)
+- 5-minute focus timer for selected activities with completion tracking
+- Choice dialog: "Continue with ScrollStop", "Configure Activities", or "Block Now"
+- Grayscale filter activates after 5 minutes only in "Continue" mode
+- Browser-based configuration system replaces iOS-specific questionnaire
-- Let components manage their own styling through their built-in classes
-- Use system dark mode detection: `window.matchMedia('(prefers-color-scheme: dark)')`
-- Apply `dark` class to `document.documentElement` for proper Tailwind dark mode
-- Add minimal custom CSS only for branding (accent colors, fonts)
-- Test components in isolation to verify proper rendering
+### Technical Notes
-**Common Mistakes to Avoid:**
+**Component Architecture:**
-- Forcing text colors with `!important` overrides
-- Overriding component `data-slot` attributes with custom styles
-- Fighting component's natural theming system
-- Not testing dark/light mode transitions
+- Components in `/components/`, modules in `/modules/`
+- HeadlessButton components must use `window.` prefix in extension context
+- SwipeCards and ActivityTimer components moved to proper folders for build process
-**Best Practices:**
+**Build Process:**
-- Create test pages to verify component functionality
-- Use component's built-in color and styling options
-- Respect component design systems and let them work as intended
-- Focus custom styling on layout and branding, not core component appearance
+- Xcode flattens directory structure for Safari extension
+- Manifest.json must use flat file names only (no folder paths)
+- Files organized in subdirectories during development but referenced by filename only
-### Choice Dialog Reddit Mobile Bug Fix (Session Summary)
+**Recent Changes:**
-**Issue:** Button text in choice dialog was cut off/misaligned on Reddit mobile, showing only half of "Timer Only" text.
-
-**Root Cause:** Reddit's aggressive CSS was interfering with button text positioning and alignment in the fallback simple dialog implementation.
-
-**Solution Applied:**
-
-- Added defensive CSS styling with `!important` declarations for all button properties
-- Used `display: flex !important` with `align-items: center` and `justify-content: center` for proper text centering
-- Added `min-height: 44px` for consistent button height on mobile
-- Reset all potential conflicting properties: `line-height`, `vertical-align`, `text-align`, `text-indent`, etc.
-- Used `setProperty()` with `!important` in hover effects to maintain alignment
-- Added `appearance: none` and `-webkit-appearance: none` to override browser defaults
-
-**Key Defensive Properties:**
-
-- `line-height: 1.2 !important` - Prevents text spacing issues
-- `text-align: center !important` - Centers text horizontally
-- `vertical-align: middle !important` - Centers text vertically
-- `display: flex !important` - Enables proper flexbox centering
-- `align-items: center !important` - Centers content vertically in flex container
-- `justify-content: center !important` - Centers content horizontally in flex container
-- `text-rendering: auto !important` - Prevents text rendering conflicts
-
-**Why This Happened:** Third-party sites like Reddit have CSS that can override extension styles, especially affecting text positioning in buttons. Using `!important` and defensive styling prevents these conflicts.
+- Removed "Timer Only" option from choice dialog - simplified to 2 options
+- Grayscale filter now only activates when user chooses "Continue with ScrollStop"
+- Eliminated timer-only mode logic from coordinator and timer-tracker modules
+- Added ScrollStop branding title to blocking screen
+- Implemented dark mode support for blocking screen
+- Platform-specific features: activities/swipe cards only show on iOS (has companion app), macOS shows clean countdown-only interface
## Design System
@@ -314,55 +262,3 @@ All extension UI elements use consistent glassmorphism styling:
- Hover: `scale(1.02)` with `box-shadow: 0px 6px 20px rgba(0, 0, 0, 0.3)`
- Border radius: `20px` for dialogs, `12px` for buttons
- Transitions: `all 0.2s ease`
-
-### Timer Tracker Feature Implementation (Session Summary)
-
-**What We Built Today:**
-
-- **Timer Tracker Feature**: A persistent, draggable timer that tracks cumulative time spent on social media sites
-- **HeadlessButton Integration**: Replaced all HTML buttons with a standardized HeadlessButton component across the iOS app
-- **Glassmorphism Timer Component**: Enhanced existing timer.js with better visibility on bright backgrounds
-
-**Key Features Implemented:**
-
-1. **Persistent Time Tracking**: Timer accumulates time across different social media sites (Instagram → YouTube → Twitter, etc.)
-2. **Daily Reset**: Automatically resets at 12 AM each day
-3. **Click to Hide**: Click timer to hide until next page reload
-4. **Drag & Drop**: Grab and move timer anywhere on screen, position persists
-5. **Smart Dragging**: Prevents accidental hiding during drag operations
-6. **Cross-Browser Storage**: Compatible with Safari extension APIs with localStorage fallback
-7. **Position Reset**: Timer returns to default center-top position on page reload
-
-**Technical Architecture:**
-
-- **timer-tracker.js**: Main module managing timer logic, storage, and UI interactions
-- **StorageWrapper**: Cross-browser storage compatibility layer
-- **Integration**: Added to content.js coordinator and manifest.json loading order
-- **Visual Improvements**: Dark glassmorphism design with white text and strong shadows for visibility
-
-**Button System Overhaul:**
-
-- **Standardized Design**: All iOS app buttons now use HeadlessButton component
-- **Consistent Styling**: Blue primary buttons, outline secondary buttons
-- **Fallback Support**: macOS app includes fallback if HeadlessButton fails to load
-- **Touch Targets**: Enhanced mobile accessibility
-
-**Build Process Notes:**
-
-- **Critical Lesson**: manifest.json must use flat file names only (no folder paths) due to build flattening
-- **File Organization**: Development files in subdirectories, but manifest references by filename only
-- **Xcode Integration**: Remember to add new files to build targets in Xcode
-
-**User Experience:**
-
-- **Awareness Tool**: Continuous visibility of time spent on social media
-- **Non-Intrusive**: Can be hidden with single click, reappears on reload
-- **Customizable Position**: User can drag to preferred screen location
-- **Persistent Data**: Time accumulates across sessions and different sites
-
-**iOS App UI Recommendation:**
-
-- **Issue Identified**: Web-based walkthrough with JavaScript buttons is unreliable on iOS
-- **Recommended Solution**: Replace HTML/CSS/JS walkthrough with native SwiftUI interface
-- **Benefits**: More reliable, better iOS integration, proper native styling, no script loading issues
-- **Implementation**: Create SwiftUI views for welcome, setup steps, and completion screens
diff --git a/ScrollStop.xcodeproj/project.pbxproj b/ScrollStop.xcodeproj/project.pbxproj
index c1b3551..1c791d9 100644
--- a/ScrollStop.xcodeproj/project.pbxproj
+++ b/ScrollStop.xcodeproj/project.pbxproj
@@ -113,8 +113,13 @@
membershipExceptions = (
Resources/_locales,
Resources/background.js,
+ "Resources/components/activity-timer.js",
+ Resources/components/badge.js,
Resources/components/button.js,
Resources/components/dialog.js,
+ Resources/components/divider.js,
+ "Resources/components/questionnaire-config.js",
+ "Resources/components/swipe-cards.js",
Resources/components/timer.js,
Resources/content.js,
Resources/images,
@@ -132,7 +137,9 @@
"Resources/modules/transition-screen/transition-screen.js",
"Resources/modules/utils/storage-helper.js",
"Resources/modules/utils/time-manager.js",
+ Resources/popup.css,
Resources/popup.html,
+ Resources/popup.js,
Resources/sites.json,
SafariWebExtensionHandler.swift,
);
@@ -143,8 +150,13 @@
membershipExceptions = (
Resources/_locales,
Resources/background.js,
+ "Resources/components/activity-timer.js",
+ Resources/components/badge.js,
Resources/components/button.js,
Resources/components/dialog.js,
+ Resources/components/divider.js,
+ "Resources/components/questionnaire-config.js",
+ "Resources/components/swipe-cards.js",
Resources/components/timer.js,
Resources/content.js,
Resources/images,
@@ -162,7 +174,9 @@
"Resources/modules/transition-screen/transition-screen.js",
"Resources/modules/utils/storage-helper.js",
"Resources/modules/utils/time-manager.js",
+ Resources/popup.css,
Resources/popup.html,
+ Resources/popup.js,
Resources/sites.json,
SafariWebExtensionHandler.swift,
);
diff --git a/ScrollStop.xcodeproj/xcuserdata/oliverstaub.xcuserdatad/xcschemes/xcschememanagement.plist b/ScrollStop.xcodeproj/xcuserdata/oliverstaub.xcuserdatad/xcschemes/xcschememanagement.plist
index fa55ff4..9dd4baa 100644
--- a/ScrollStop.xcodeproj/xcuserdata/oliverstaub.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/ScrollStop.xcodeproj/xcuserdata/oliverstaub.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -7,12 +7,12 @@
ScrollStop (iOS).xcscheme_^#shared#^_
orderHint
- 1
+ 0
ScrollStop (macOS).xcscheme_^#shared#^_
orderHint
- 0
+ 1
diff --git a/Shared (Extension)/Resources/components/badge.js b/Shared (Extension)/Resources/components/badge.js
new file mode 100644
index 0000000..9817cbb
--- /dev/null
+++ b/Shared (Extension)/Resources/components/badge.js
@@ -0,0 +1,239 @@
+/**
+ * HeadlessBadge - A comprehensive badge component
+ *
+ * USAGE EXAMPLES:
+ *
+ * // Basic badge
+ * new HeadlessBadge('New').render('#container');
+ *
+ * // Colored badge
+ * new HeadlessBadge('Error', { color: 'red' }).render('#status');
+ *
+ * // Badge button with delete functionality
+ * new HeadlessBadgeButton('Clickable', {
+ * color: 'blue',
+ * onClick: () => console.log('Badge clicked!')
+ * }).render('#actions');
+ */
+class HeadlessBadge {
+ constructor(text, options = {}) {
+ this.text = text;
+ this.options = {
+ color: options.color || 'zinc',
+ className: options.className || '',
+ ...options,
+ };
+
+ this.element = this.createElement();
+ }
+
+ static colors = {
+ red: 'bg-red-500/15 text-red-700 group-data-hover:bg-red-500/25 dark:bg-red-500/10 dark:text-red-400 dark:group-data-hover:bg-red-500/20',
+ orange:
+ 'bg-orange-500/15 text-orange-700 group-data-hover:bg-orange-500/25 dark:bg-orange-500/10 dark:text-orange-400 dark:group-data-hover:bg-orange-500/20',
+ amber:
+ 'bg-amber-400/20 text-amber-700 group-data-hover:bg-amber-400/30 dark:bg-amber-400/10 dark:text-amber-400 dark:group-data-hover:bg-amber-400/15',
+ yellow:
+ 'bg-yellow-400/20 text-yellow-700 group-data-hover:bg-yellow-400/30 dark:bg-yellow-400/10 dark:text-yellow-300 dark:group-data-hover:bg-yellow-400/15',
+ lime: 'bg-lime-400/20 text-lime-700 group-data-hover:bg-lime-400/30 dark:bg-lime-400/10 dark:text-lime-300 dark:group-data-hover:bg-lime-400/15',
+ green:
+ 'bg-green-500/15 text-green-700 group-data-hover:bg-green-500/25 dark:bg-green-500/10 dark:text-green-400 dark:group-data-hover:bg-green-500/20',
+ emerald:
+ 'bg-emerald-500/15 text-emerald-700 group-data-hover:bg-emerald-500/25 dark:bg-emerald-500/10 dark:text-emerald-400 dark:group-data-hover:bg-emerald-500/20',
+ teal: 'bg-teal-500/15 text-teal-700 group-data-hover:bg-teal-500/25 dark:bg-teal-500/10 dark:text-teal-300 dark:group-data-hover:bg-teal-500/20',
+ cyan: 'bg-cyan-400/20 text-cyan-700 group-data-hover:bg-cyan-400/30 dark:bg-cyan-400/10 dark:text-cyan-300 dark:group-data-hover:bg-cyan-400/15',
+ sky: 'bg-sky-500/15 text-sky-700 group-data-hover:bg-sky-500/25 dark:bg-sky-500/10 dark:text-sky-300 dark:group-data-hover:bg-sky-500/20',
+ blue: 'bg-blue-500/15 text-blue-700 group-data-hover:bg-blue-500/25 dark:text-blue-400 dark:group-data-hover:bg-blue-500/25',
+ indigo:
+ 'bg-indigo-500/15 text-indigo-700 group-data-hover:bg-indigo-500/25 dark:text-indigo-400 dark:group-data-hover:bg-indigo-500/20',
+ violet:
+ 'bg-violet-500/15 text-violet-700 group-data-hover:bg-violet-500/25 dark:text-violet-400 dark:group-data-hover:bg-violet-500/20',
+ purple:
+ 'bg-purple-500/15 text-purple-700 group-data-hover:bg-purple-500/25 dark:text-purple-400 dark:group-data-hover:bg-purple-500/20',
+ fuchsia:
+ 'bg-fuchsia-400/15 text-fuchsia-700 group-data-hover:bg-fuchsia-400/25 dark:bg-fuchsia-400/10 dark:text-fuchsia-400 dark:group-data-hover:bg-fuchsia-400/20',
+ pink: 'bg-pink-400/15 text-pink-700 group-data-hover:bg-pink-400/25 dark:bg-pink-400/10 dark:text-pink-400 dark:group-data-hover:bg-pink-400/20',
+ rose: 'bg-rose-400/15 text-rose-700 group-data-hover:bg-rose-400/25 dark:bg-rose-400/10 dark:text-rose-400 dark:group-data-hover:bg-rose-400/20',
+ zinc: 'bg-zinc-600/10 text-zinc-700 group-data-hover:bg-zinc-600/20 dark:bg-white/5 dark:text-zinc-400 dark:group-data-hover:bg-white/10',
+ };
+
+ static styles = {
+ base: [
+ 'inline-flex items-center gap-x-1.5 rounded-md px-1.5 py-0.5 text-sm/5 font-medium sm:text-xs/5',
+ ],
+ };
+
+ createElement() {
+ const span = document.createElement('span');
+ span.className = this.getClasses();
+ span.textContent = this.text;
+ return span;
+ }
+
+ getClasses() {
+ const classes = [...HeadlessBadge.styles.base];
+
+ const colorClass = HeadlessBadge.colors[this.options.color];
+ if (colorClass) {
+ classes.push(colorClass);
+ }
+
+ if (this.options.className) {
+ classes.push(this.options.className);
+ }
+
+ return classes.join(' ');
+ }
+
+ setText(text) {
+ this.text = text;
+ this.element.textContent = text;
+ return this;
+ }
+
+ setColor(color) {
+ this.options.color = color;
+ this.element.className = this.getClasses();
+ return this;
+ }
+
+ render(container) {
+ if (typeof container === 'string') {
+ container = document.querySelector(container);
+ }
+
+ if (!container) {
+ throw new Error('Container not found');
+ }
+
+ container.appendChild(this.element);
+ return this;
+ }
+
+ remove() {
+ if (this.element && this.element.parentNode) {
+ this.element.parentNode.removeChild(this.element);
+ }
+ return this;
+ }
+
+ getElement() {
+ return this.element;
+ }
+}
+
+class HeadlessBadgeButton {
+ constructor(text, options = {}) {
+ this.text = text;
+ this.options = {
+ color: options.color || 'zinc',
+ className: options.className || '',
+ onClick: options.onClick || null,
+ href: options.href || null,
+ target: options.target || null,
+ ...options,
+ };
+
+ this.badge = new HeadlessBadge(text, { color: this.options.color });
+ this.element = this.createElement();
+ }
+
+ static styles = {
+ base: [
+ 'group relative inline-flex rounded-md focus:outline-2 focus:outline-offset-2 focus:outline-blue-500',
+ ],
+ touchTarget: [
+ 'absolute top-1/2 left-1/2 size-[max(100%,2.75rem)] -translate-x-1/2 -translate-y-1/2 pointer-fine:hidden',
+ ],
+ };
+
+ createElement() {
+ const element = this.options.href
+ ? document.createElement('a')
+ : document.createElement('button');
+
+ element.className = this.getClasses();
+
+ if (this.options.href) {
+ element.href = this.options.href;
+ if (this.options.target) {
+ element.target = this.options.target;
+ }
+ } else {
+ element.type = 'button';
+ }
+
+ if (this.options.onClick) {
+ element.addEventListener('click', this.options.onClick);
+ }
+
+ // Add touch target
+ const touchTarget = document.createElement('span');
+ touchTarget.className = HeadlessBadgeButton.styles.touchTarget.join(' ');
+ touchTarget.setAttribute('aria-hidden', 'true');
+ element.appendChild(touchTarget);
+
+ // Add badge
+ element.appendChild(this.badge.getElement());
+
+ return element;
+ }
+
+ getClasses() {
+ const classes = [...HeadlessBadgeButton.styles.base];
+
+ if (this.options.className) {
+ classes.push(this.options.className);
+ }
+
+ return classes.join(' ');
+ }
+
+ setText(text) {
+ this.text = text;
+ this.badge.setText(text);
+ return this;
+ }
+
+ setColor(color) {
+ this.options.color = color;
+ this.badge.setColor(color);
+ return this;
+ }
+
+ render(container) {
+ if (typeof container === 'string') {
+ container = document.querySelector(container);
+ }
+
+ if (!container) {
+ throw new Error('Container not found');
+ }
+
+ container.appendChild(this.element);
+ return this;
+ }
+
+ remove() {
+ if (this.element && this.element.parentNode) {
+ this.element.parentNode.removeChild(this.element);
+ }
+ return this;
+ }
+
+ getElement() {
+ return this.element;
+ }
+
+ getBadge() {
+ return this.badge;
+ }
+}
+
+// Export for use in modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = { HeadlessBadge, HeadlessBadgeButton };
+} else {
+ window.HeadlessBadge = HeadlessBadge;
+ window.HeadlessBadgeButton = HeadlessBadgeButton;
+}
diff --git a/Shared (Extension)/Resources/components/divider.js b/Shared (Extension)/Resources/components/divider.js
new file mode 100644
index 0000000..65f3a88
--- /dev/null
+++ b/Shared (Extension)/Resources/components/divider.js
@@ -0,0 +1,109 @@
+/**
+ * HeadlessDivider - A simple divider component inspired by Headless UI
+ *
+ * USAGE EXAMPLES:
+ *
+ * // Basic divider
+ * new HeadlessDivider().render('#container');
+ *
+ * // Soft divider (lighter color)
+ * new HeadlessDivider({
+ * soft: true
+ * }).render(document.body);
+ *
+ * // Hard divider (default, darker color)
+ * new HeadlessDivider({
+ * soft: false
+ * }).render('#content');
+ */
+class HeadlessDivider {
+ constructor(options = {}) {
+ this.options = {
+ soft: options.soft !== undefined ? options.soft : false,
+ className: options.className || '',
+ id: options.id || null,
+ ...options,
+ };
+
+ this.element = this.createElement();
+ }
+
+ // Style configurations (converted from the original React component)
+ static styles = {
+ base: ['w-full border-t'],
+
+ soft: ['border-zinc-950/5 dark:border-white/5'],
+
+ hard: ['border-zinc-950/10 dark:border-white/10'],
+ };
+
+ createElement() {
+ // Create the divider element (hr)
+ const divider = document.createElement('hr');
+ divider.setAttribute('role', 'presentation');
+
+ // Apply CSS classes
+ divider.className = this.getClasses();
+
+ // Set id if provided
+ if (this.options.id) {
+ divider.id = this.options.id;
+ }
+
+ // Set any additional attributes (excluding the ones we handle specially)
+ Object.keys(this.options).forEach((key) => {
+ if (!['soft', 'className', 'id'].includes(key)) {
+ divider.setAttribute(key, this.options[key]);
+ }
+ });
+
+ return divider;
+ }
+
+ getClasses() {
+ const classes = [...HeadlessDivider.styles.base];
+
+ // Add soft or hard styles
+ if (this.options.soft) {
+ classes.push(...HeadlessDivider.styles.soft);
+ } else {
+ classes.push(...HeadlessDivider.styles.hard);
+ }
+
+ // Add custom classes
+ if (this.options.className) {
+ classes.push(this.options.className);
+ }
+
+ return classes.join(' ');
+ }
+
+ // Method to render the divider to a container
+ render(container) {
+ if (typeof container === 'string') {
+ container = document.querySelector(container);
+ }
+
+ if (!container) {
+ throw new Error('Container not found');
+ }
+
+ container.appendChild(this.element);
+ return this;
+ }
+
+ // Method to remove from DOM
+ remove() {
+ if (this.element.parentNode) {
+ this.element.parentNode.removeChild(this.element);
+ }
+ return this;
+ }
+}
+
+// Export for use in modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = HeadlessDivider;
+} else {
+ window.HeadlessDivider = HeadlessDivider;
+}
diff --git a/Shared (Extension)/Resources/components/questionnaire-config.js b/Shared (Extension)/Resources/components/questionnaire-config.js
new file mode 100644
index 0000000..4ef5aed
--- /dev/null
+++ b/Shared (Extension)/Resources/components/questionnaire-config.js
@@ -0,0 +1,469 @@
+// components/questionnaire-config.js
+// Browser-based questionnaire configuration interface
+
+class QuestionnaireConfig {
+ constructor(options = {}) {
+ this.options = {
+ onComplete: options.onComplete || null,
+ onCancel: options.onCancel || null,
+ ...options,
+ };
+
+ this.element = null;
+ this.currentStep = 0;
+ this.data = {
+ householdTasks: [],
+ hobbies: [],
+ currentTasks: [],
+ friends: [],
+ goals: [],
+ books: [],
+ };
+
+ this.steps = [
+ {
+ key: 'householdTasks',
+ title: 'Household Tasks',
+ placeholder: 'e.g., Do laundry, Clean kitchen',
+ emoji: '🏠',
+ defaults: ['Do laundry', 'Clean kitchen', 'Take out trash', 'Vacuum'],
+ },
+ {
+ key: 'hobbies',
+ title: 'Hobbies & Interests',
+ placeholder: 'e.g., Reading, Guitar, Cycling',
+ emoji: '🎯',
+ defaults: ['Reading', 'Exercise', 'Music', 'Cooking'],
+ },
+ {
+ key: 'currentTasks',
+ title: 'Current Tasks',
+ placeholder: 'e.g., Study for exam, Buy groceries',
+ emoji: '✅',
+ defaults: ['Buy groceries', 'Pay bills', 'Call doctor'],
+ },
+ {
+ key: 'friends',
+ title: 'Friends & Family',
+ placeholder: 'e.g., Mom, Sarah, Alex',
+ emoji: '👥',
+ defaults: ['Mom', 'Dad', 'Best friend'],
+ },
+ {
+ key: 'goals',
+ title: 'Personal Goals',
+ placeholder: 'e.g., Exercise daily, Learn Spanish',
+ emoji: '🎯',
+ defaults: ['Exercise daily', 'Read more', 'Learn new skill'],
+ },
+ {
+ key: 'books',
+ title: 'Books to Read',
+ placeholder: 'e.g., Atomic Habits, 1984',
+ emoji: '📚',
+ defaults: ['Atomic Habits', 'The Lean Startup', '1984'],
+ },
+ ];
+
+ // Load existing data
+ this.loadExistingData();
+ }
+
+ async loadExistingData() {
+ try {
+ if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) {
+ const result = await browser.storage.local.get([
+ 'scrollstop_householdTasks',
+ 'scrollstop_hobbies',
+ 'scrollstop_currentTasks',
+ 'scrollstop_friends',
+ 'scrollstop_goals',
+ 'scrollstop_books',
+ ]);
+
+ this.data.householdTasks = result.scrollstop_householdTasks || [];
+ this.data.hobbies = result.scrollstop_hobbies || [];
+ this.data.currentTasks = result.scrollstop_currentTasks || [];
+ this.data.friends = result.scrollstop_friends || [];
+ this.data.goals = result.scrollstop_goals || [];
+ this.data.books = result.scrollstop_books || [];
+ } else {
+ // Fallback to localStorage
+ this.data.householdTasks = JSON.parse(
+ localStorage.getItem('scrollstop_householdTasks') || '[]'
+ );
+ this.data.hobbies = JSON.parse(localStorage.getItem('scrollstop_hobbies') || '[]');
+ this.data.currentTasks = JSON.parse(
+ localStorage.getItem('scrollstop_currentTasks') || '[]'
+ );
+ this.data.friends = JSON.parse(localStorage.getItem('scrollstop_friends') || '[]');
+ this.data.goals = JSON.parse(localStorage.getItem('scrollstop_goals') || '[]');
+ this.data.books = JSON.parse(localStorage.getItem('scrollstop_books') || '[]');
+ }
+ } catch (error) {
+ console.error('Error loading questionnaire data:', error);
+ }
+ }
+
+ render(container) {
+ if (!container) {
+ // Create full-screen overlay
+ container = document.createElement('div');
+ container.style.cssText = `
+ position: fixed !important;
+ top: 0 !important;
+ left: 0 !important;
+ width: 100% !important;
+ height: 100% !important;
+ background: rgba(0, 0, 0, 0.9) !important;
+ z-index: 2147483647 !important;
+ display: flex !important;
+ align-items: center !important;
+ justify-content: center !important;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
+ `;
+ document.body.appendChild(container);
+ this.overlay = container;
+ }
+
+ this.element = document.createElement('div');
+ this.element.style.cssText = `
+ background: white !important;
+ border-radius: 16px !important;
+ padding: 32px !important;
+ max-width: 500px !important;
+ width: 90% !important;
+ max-height: 80vh !important;
+ overflow-y: auto !important;
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3) !important;
+ color: #1f2937 !important;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
+ `;
+
+ this.renderCurrentStep();
+ container.appendChild(this.element);
+ return this.element;
+ }
+
+ renderCurrentStep() {
+ if (this.currentStep >= this.steps.length) {
+ this.renderComplete();
+ return;
+ }
+
+ const step = this.steps[this.currentStep];
+ const currentItems = this.data[step.key];
+
+ this.element.innerHTML = `
+
+
${step.emoji}
+
+ ${step.title}
+
+
+ Step ${this.currentStep + 1} of ${this.steps.length}
+
+
+
+
+
+
+
+
+ `;
+
+ // Create HeadlessButton components with fallback
+ const addBtnContainer = this.element.querySelector('#add-btn-container');
+ const buttonContainer = this.element.querySelector('#button-container');
+
+ // Add button
+ if (window.HeadlessButton && addBtnContainer) {
+ try {
+ const addBtn = new window.HeadlessButton('Add', {
+ color: 'blue',
+ onClick: () => this.addItem(),
+ });
+ addBtnContainer.appendChild(addBtn.element);
+ } catch (error) {
+ console.error('HeadlessButton failed, using fallback:', error);
+ this.createFallbackButton(addBtnContainer, 'Add', () => this.addItem(), 'blue');
+ }
+ } else {
+ // Fallback if HeadlessButton not available
+ this.createFallbackButton(addBtnContainer, 'Add', () => this.addItem(), 'blue');
+ }
+
+ // Navigation buttons
+ if (window.HeadlessButton && buttonContainer) {
+ try {
+ // Previous button
+ if (this.currentStep > 0) {
+ const prevBtn = new window.HeadlessButton('Previous', {
+ color: 'zinc',
+ outline: true,
+ onClick: () => this.previousStep(),
+ });
+ buttonContainer.appendChild(prevBtn.element);
+ } else {
+ // Empty div for spacing
+ const spacer = document.createElement('div');
+ buttonContainer.appendChild(spacer);
+ }
+
+ // Cancel button
+ const cancelBtn = new window.HeadlessButton('Cancel', {
+ color: 'red',
+ outline: true,
+ onClick: () => this.cancel(),
+ });
+ buttonContainer.appendChild(cancelBtn.element);
+
+ // Next/Finish button
+ const nextBtn = new window.HeadlessButton(
+ this.currentStep === this.steps.length - 1 ? 'Finish' : 'Next',
+ {
+ color: 'blue',
+ onClick: () => this.nextStep(),
+ }
+ );
+ buttonContainer.appendChild(nextBtn.element);
+ } catch (error) {
+ console.error('HeadlessButton failed, using fallback:', error);
+ this.createFallbackButtons(buttonContainer);
+ }
+ } else {
+ // Fallback if HeadlessButton not available
+ this.createFallbackButtons(buttonContainer);
+ }
+
+ // Set up input handlers
+ const input = this.element.querySelector('#new-item-input');
+ if (input) {
+ input.addEventListener('keypress', (e) => {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ this.addItem();
+ }
+ });
+ input.focus();
+ }
+
+ // Create badge buttons for items
+ const itemsList = this.element.querySelector('#items-list');
+ if (itemsList && window.HeadlessBadgeButton) {
+ currentItems.forEach((item, index) => {
+ const badgeButton = new window.HeadlessBadgeButton(`${item} ×`, {
+ color: 'zinc',
+ onClick: () => this.removeItem(index),
+ });
+ itemsList.appendChild(badgeButton.element);
+ });
+ }
+
+ // Add default items button if list is empty
+ if (currentItems.length === 0 && itemsList && step.defaults) {
+ const addDefaultsBtn = new window.HeadlessButton('Add suggested items', {
+ color: 'zinc',
+ outline: true,
+ onClick: () => this.addDefaultItems(),
+ });
+ itemsList.appendChild(addDefaultsBtn.element);
+ }
+
+ // Store reference for global access
+ window.questionnaireConfig = this;
+ }
+
+ // Fallback button creation methods - still try to use HeadlessButton
+ createFallbackButton(container, text, onClick, color) {
+ if (!container) return;
+
+ // Try to use HeadlessButton again with a delay
+ setTimeout(() => {
+ if (window.HeadlessButton) {
+ container.innerHTML = ''; // Clear any existing content
+ const button = new window.HeadlessButton(text, {
+ color: color,
+ outline: color !== 'blue',
+ onClick: onClick,
+ });
+ container.appendChild(button.element);
+ }
+ }, 100);
+
+ // Immediate basic button as emergency fallback
+ const button = document.createElement('button');
+ button.textContent = text;
+ button.addEventListener('click', (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onClick();
+ });
+ container.appendChild(button);
+ }
+
+ createFallbackButtons(container) {
+ if (!container) return;
+
+ // Previous button
+ if (this.currentStep > 0) {
+ this.createFallbackButton(container, 'Previous', () => this.previousStep(), 'zinc');
+ } else {
+ const spacer = document.createElement('div');
+ container.appendChild(spacer);
+ }
+
+ // Cancel button
+ this.createFallbackButton(container, 'Cancel', () => this.cancel(), 'red');
+
+ // Next/Finish button
+ const nextText = this.currentStep === this.steps.length - 1 ? 'Finish' : 'Next';
+ this.createFallbackButton(container, nextText, () => this.nextStep(), 'blue');
+ }
+
+ addItem() {
+ const input = this.element.querySelector('#new-item-input');
+ const value = input.value.trim();
+
+ if (value) {
+ const step = this.steps[this.currentStep];
+ this.data[step.key].push(value);
+ input.value = '';
+ this.renderCurrentStep();
+ }
+ }
+
+ addDefaultItems() {
+ const step = this.steps[this.currentStep];
+ if (step.defaults) {
+ this.data[step.key] = [...step.defaults];
+ this.renderCurrentStep();
+ }
+ }
+
+ removeItem(index) {
+ const step = this.steps[this.currentStep];
+ this.data[step.key].splice(index, 1);
+ this.renderCurrentStep();
+ }
+
+ nextStep() {
+ this.currentStep++;
+ this.renderCurrentStep();
+ }
+
+ previousStep() {
+ if (this.currentStep > 0) {
+ this.currentStep--;
+ this.renderCurrentStep();
+ }
+ }
+
+ cancel() {
+ if (this.options.onCancel) {
+ this.options.onCancel();
+ }
+ this.remove();
+ }
+
+ renderComplete() {
+ this.element.innerHTML = `
+
+
🎉
+
+ Configuration Complete!
+
+
+ Your personalized activities have been saved. You'll now see customized suggestions when ScrollStop blocks a site.
+
+
+
+ `;
+
+ // Create HeadlessButton for done button that just closes the window
+ const doneBtnContainer = this.element.querySelector('#done-btn-container');
+ if (window.HeadlessButton && doneBtnContainer) {
+ const doneBtn = new window.HeadlessButton('Finish', {
+ color: 'green',
+ onClick: () => {
+ // Save data and close window without callback
+ this.saveData().then(() => {
+ this.remove();
+ });
+ },
+ });
+ doneBtnContainer.appendChild(doneBtn.element);
+ }
+ }
+
+ async complete() {
+ await this.saveData();
+ if (this.options.onComplete) {
+ this.options.onComplete();
+ }
+ this.remove();
+ }
+
+ async saveData() {
+ try {
+ const storageData = {
+ scrollstop_householdTasks: this.data.householdTasks,
+ scrollstop_hobbies: this.data.hobbies,
+ scrollstop_currentTasks: this.data.currentTasks,
+ scrollstop_friends: this.data.friends,
+ scrollstop_goals: this.data.goals,
+ scrollstop_books: this.data.books,
+ };
+
+ if (typeof browser !== 'undefined' && browser.storage && browser.storage.local) {
+ await browser.storage.local.set(storageData);
+ } else {
+ // Fallback to localStorage
+ for (const [key, value] of Object.entries(storageData)) {
+ localStorage.setItem(key, JSON.stringify(value));
+ }
+ }
+
+ console.log('Questionnaire data saved successfully');
+ } catch (error) {
+ console.error('Error saving questionnaire data:', error);
+ }
+ }
+
+ remove() {
+ if (this.overlay && this.overlay.parentNode) {
+ this.overlay.parentNode.removeChild(this.overlay);
+ } else if (this.element && this.element.parentNode) {
+ this.element.parentNode.removeChild(this.element);
+ }
+ delete window.questionnaireConfig;
+ }
+}
+
+// Export for use in modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = QuestionnaireConfig;
+} else {
+ window.QuestionnaireConfig = QuestionnaireConfig;
+}
diff --git a/Shared (Extension)/Resources/components/swipe-cards.js b/Shared (Extension)/Resources/components/swipe-cards.js
new file mode 100644
index 0000000..be4e308
--- /dev/null
+++ b/Shared (Extension)/Resources/components/swipe-cards.js
@@ -0,0 +1,460 @@
+// UIComponentsJs/swipe-cards.js
+// Tinder-like swipeable card component for activity suggestions
+
+class SwipeCards {
+ constructor(cards, options = {}) {
+ this.cards = cards || [];
+ this.currentIndex = 0;
+ this.options = {
+ containerClass: options.containerClass || '',
+ onSwipeRight: options.onSwipeRight || null,
+ onSwipeLeft: options.onSwipeLeft || null,
+ onComplete: options.onComplete || null,
+ ...options,
+ };
+
+ this.element = null;
+ this.isDragging = false;
+ this.startX = 0;
+ this.startY = 0;
+ this.currentX = 0;
+ this.currentY = 0;
+ this.offset = 0;
+
+ // Bind methods
+ this.handleTouchStart = this.handleTouchStart.bind(this);
+ this.handleTouchMove = this.handleTouchMove.bind(this);
+ this.handleTouchEnd = this.handleTouchEnd.bind(this);
+ this.handleMouseDown = this.handleMouseDown.bind(this);
+ this.handleMouseMove = this.handleMouseMove.bind(this);
+ this.handleMouseUp = this.handleMouseUp.bind(this);
+ }
+
+ render(container) {
+ if (!container) {
+ throw new Error('Container element is required');
+ }
+
+ this.element = document.createElement('div');
+ this.element.className = `swipe-cards-container ${this.options.containerClass}`;
+ this.element.style.cssText = `
+ position: relative;
+ width: 100%;
+ height: 400px;
+ overflow: hidden;
+ touch-action: none;
+ user-select: none;
+ `;
+
+ this.createCards();
+ this.setupEventListeners();
+
+ container.appendChild(this.element);
+ return this.element;
+ }
+
+ createCards() {
+ if (!this.element) {
+ return;
+ }
+
+ this.element.innerHTML = '';
+
+ if (this.cards.length === 0) {
+ this.showEmptyState();
+ return;
+ }
+
+ // Create cards stack (show up to 3 cards)
+ const visibleCards = this.cards.slice(this.currentIndex, this.currentIndex + 3);
+
+ visibleCards.forEach((card, index) => {
+ const cardElement = this.createCard(card, index);
+ this.element.appendChild(cardElement);
+ });
+
+ // Add instructions
+ this.addInstructions();
+ }
+
+ createCard(card, stackIndex) {
+ const cardElement = document.createElement('div');
+ cardElement.className = 'swipe-card';
+ cardElement.dataset.stackIndex = stackIndex;
+
+ const zIndex = 10 - stackIndex;
+ const scale = 1 - stackIndex * 0.05;
+ const yOffset = stackIndex * 10;
+
+ cardElement.style.cssText = `
+ position: absolute;
+ top: ${yOffset}px;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: white;
+ border-radius: 16px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
+ border: 1px solid rgba(0, 0, 0, 0.08);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ padding: 32px;
+ cursor: grab;
+ transform: scale(${scale});
+ transform-origin: center;
+ z-index: ${zIndex};
+ transition: transform 0.3s ease, opacity 0.3s ease;
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ `;
+
+ cardElement.innerHTML = `
+
+ ${card.emoji}
+
+
+ ${card.text}
+
+ ${
+ card.description
+ ? `
+
+ ${card.description}
+
+ `
+ : ''
+ }
+ `;
+
+ // Only make the top card interactive
+ if (stackIndex === 0) {
+ this.makeCardInteractive(cardElement);
+ }
+
+ return cardElement;
+ }
+
+ makeCardInteractive(cardElement) {
+ // Touch events
+ cardElement.addEventListener('touchstart', this.handleTouchStart, { passive: false });
+ cardElement.addEventListener('touchmove', this.handleTouchMove, { passive: false });
+ cardElement.addEventListener('touchend', this.handleTouchEnd, { passive: false });
+
+ // Mouse events for desktop
+ cardElement.addEventListener('mousedown', this.handleMouseDown);
+ cardElement.addEventListener('mousemove', this.handleMouseMove);
+ cardElement.addEventListener('mouseup', this.handleMouseUp);
+ cardElement.addEventListener('mouseleave', this.handleMouseUp);
+
+ // Prevent default drag behavior
+ cardElement.addEventListener('dragstart', (e) => e.preventDefault());
+ }
+
+ addInstructions() {
+ const instructions = document.createElement('div');
+ instructions.className = 'swipe-instructions';
+ instructions.style.cssText = `
+ position: absolute;
+ bottom: -60px;
+ left: 0;
+ right: 0;
+ text-align: center;
+ font-size: 0.9rem;
+ color: #6b7280;
+ display: flex;
+ justify-content: space-between;
+ padding: 0 20px;
+ `;
+
+ instructions.innerHTML = `
+
+ ← Pass
+
+
+ Swipe to choose
+
+
+ Do it! →
+
+ `;
+
+ this.element.appendChild(instructions);
+ }
+
+ showEmptyState() {
+ this.element.innerHTML = `
+
+
🎯
+
No more activities
+
+ `;
+ }
+
+ setupEventListeners() {
+ // Global mouse events for when dragging outside card
+ document.addEventListener('mousemove', this.handleMouseMove);
+ document.addEventListener('mouseup', this.handleMouseUp);
+ }
+
+ handleTouchStart(e) {
+ this.isDragging = true;
+ this.startX = e.touches[0].clientX;
+ this.startY = e.touches[0].clientY;
+ this.currentX = this.startX;
+ this.currentY = this.startY;
+
+ e.target.style.cursor = 'grabbing';
+ e.preventDefault();
+ }
+
+ handleTouchMove(e) {
+ if (!this.isDragging) {
+ return;
+ }
+
+ this.currentX = e.touches[0].clientX;
+ this.currentY = e.touches[0].clientY;
+
+ this.updateCardPosition(e.target);
+ e.preventDefault();
+ }
+
+ handleTouchEnd(e) {
+ if (!this.isDragging) {
+ return;
+ }
+
+ this.isDragging = false;
+ this.finishSwipe(e.target);
+ e.target.style.cursor = 'grab';
+ }
+
+ handleMouseDown(e) {
+ this.isDragging = true;
+ this.startX = e.clientX;
+ this.startY = e.clientY;
+ this.currentX = this.startX;
+ this.currentY = this.startY;
+
+ e.target.style.cursor = 'grabbing';
+ e.preventDefault();
+ }
+
+ handleMouseMove(e) {
+ if (!this.isDragging) {
+ return;
+ }
+
+ this.currentX = e.clientX;
+ this.currentY = e.clientY;
+
+ const topCard = this.element.querySelector('[data-stack-index="0"]');
+ if (topCard) {
+ this.updateCardPosition(topCard);
+ }
+ }
+
+ handleMouseUp(_e) {
+ if (!this.isDragging) {
+ return;
+ }
+
+ this.isDragging = false;
+ const topCard = this.element.querySelector('[data-stack-index="0"]');
+ if (topCard) {
+ this.finishSwipe(topCard);
+ topCard.style.cursor = 'grab';
+ }
+ }
+
+ updateCardPosition(cardElement) {
+ const deltaX = this.currentX - this.startX;
+ const deltaY = this.currentY - this.startY;
+
+ // Limit vertical movement
+ const limitedDeltaY = Math.max(-50, Math.min(50, deltaY));
+
+ this.offset = deltaX;
+
+ const rotation = deltaX * 0.1; // Subtle rotation
+ const opacity = Math.max(0.7, 1 - Math.abs(deltaX) / 200);
+
+ cardElement.style.transform = `translateX(${deltaX}px) translateY(${limitedDeltaY}px) rotate(${rotation}deg)`;
+ cardElement.style.opacity = opacity;
+
+ // Update visual feedback
+ this.updateSwipeFeedback(deltaX);
+ }
+
+ updateSwipeFeedback(deltaX) {
+ const threshold = 100;
+
+ // Remove any existing feedback
+ this.clearSwipeFeedback();
+
+ if (Math.abs(deltaX) > threshold) {
+ const feedback = document.createElement('div');
+ feedback.className = 'swipe-feedback';
+ feedback.style.cssText = `
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ font-size: 2rem;
+ font-weight: bold;
+ z-index: 1000;
+ pointer-events: none;
+ transition: opacity 0.1s ease;
+ `;
+
+ if (deltaX > threshold) {
+ // Swiping right (accept)
+ feedback.style.right = '20px';
+ feedback.style.color = '#10b981';
+ feedback.textContent = 'DO IT! ✓';
+ } else {
+ // Swiping left (reject)
+ feedback.style.left = '20px';
+ feedback.style.color = '#ef4444';
+ feedback.textContent = 'PASS ✗';
+ }
+
+ this.element.appendChild(feedback);
+ }
+ }
+
+ clearSwipeFeedback() {
+ const feedback = this.element.querySelector('.swipe-feedback');
+ if (feedback) {
+ feedback.remove();
+ }
+ }
+
+ finishSwipe(cardElement) {
+ const threshold = 100;
+ const shouldSwipe = Math.abs(this.offset) > threshold;
+
+ this.clearSwipeFeedback();
+
+ if (shouldSwipe) {
+ this.completeSwipe(cardElement, this.offset > 0 ? 'right' : 'left');
+ } else {
+ this.resetCard(cardElement);
+ }
+
+ this.offset = 0;
+ }
+
+ completeSwipe(cardElement, direction) {
+ const finalX = direction === 'right' ? window.innerWidth : -window.innerWidth;
+ const rotation = direction === 'right' ? 30 : -30;
+
+ cardElement.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
+ cardElement.style.transform = `translateX(${finalX}px) rotate(${rotation}deg)`;
+ cardElement.style.opacity = '0';
+
+ // Get current card data
+ const currentCard = this.cards[this.currentIndex];
+
+ // Trigger callbacks
+ if (direction === 'right' && this.options.onSwipeRight) {
+ this.options.onSwipeRight(currentCard, this.currentIndex);
+ } else if (direction === 'left' && this.options.onSwipeLeft) {
+ this.options.onSwipeLeft(currentCard, this.currentIndex);
+ }
+
+ // Move to next card after animation
+ setTimeout(() => {
+ this.nextCard();
+ }, 300);
+ }
+
+ resetCard(cardElement) {
+ cardElement.style.transition = 'transform 0.3s ease, opacity 0.3s ease';
+ cardElement.style.transform = 'translateX(0px) translateY(0px) rotate(0deg)';
+ cardElement.style.opacity = '1';
+
+ setTimeout(() => {
+ cardElement.style.transition = '';
+ }, 300);
+ }
+
+ nextCard() {
+ this.currentIndex++;
+
+ if (this.currentIndex >= this.cards.length) {
+ // All cards swiped
+ if (this.options.onComplete) {
+ this.options.onComplete();
+ }
+ return;
+ }
+
+ // Recreate cards with new stack
+ this.createCards();
+ }
+
+ // Public methods
+ addCard(card) {
+ this.cards.push(card);
+ if (this.element && this.currentIndex >= this.cards.length - 1) {
+ this.createCards();
+ }
+ }
+
+ reset() {
+ this.currentIndex = 0;
+ if (this.element) {
+ this.createCards();
+ }
+ }
+
+ getCurrentCard() {
+ return this.cards[this.currentIndex];
+ }
+
+ getRemainingCards() {
+ return this.cards.slice(this.currentIndex);
+ }
+
+ destroy() {
+ if (this.element) {
+ // Remove global event listeners
+ document.removeEventListener('mousemove', this.handleMouseMove);
+ document.removeEventListener('mouseup', this.handleMouseUp);
+
+ // Remove element
+ if (this.element.parentNode) {
+ this.element.parentNode.removeChild(this.element);
+ }
+ this.element = null;
+ }
+ }
+}
+
+// Export for use in modules
+if (typeof module !== 'undefined' && module.exports) {
+ module.exports = SwipeCards;
+} else {
+ window.SwipeCards = SwipeCards;
+}
diff --git a/Shared (Extension)/Resources/components/timer.js b/Shared (Extension)/Resources/components/timer.js
deleted file mode 100644
index d55e1be..0000000
--- a/Shared (Extension)/Resources/components/timer.js
+++ /dev/null
@@ -1,446 +0,0 @@
-/**
- * GlassmorphismTimer - A customizable timer component with Face ID styling
- *
- * USAGE EXAMPLES:
- *
- * // Basic 30-second timer with default size
- * new GlassmorphismTimer(30).render('#container');
- *
- * // Custom size and click handler
- * new GlassmorphismTimer(120, {
- * width: '80px',
- * height: '36px',
- * fontSize: '16px',
- * onClick: (timer) => console.log('Timer clicked!', timer.getTime())
- * }).render(document.body);
- *
- * // Small timer with callbacks
- * new GlassmorphismTimer(60, {
- * width: '48px',
- * height: '24px',
- * fontSize: '10px',
- * onStart: () => console.log('Timer started'),
- * onStop: () => console.log('Timer stopped'),
- * onComplete: () => console.log('Timer finished!'),
- * onTick: (timeLeft) => console.log(`${timeLeft}s remaining`)
- * }).render('.timer-container');
- *
- * // Manual control with large size
- * const timer = new GlassmorphismTimer(300, {
- * width: '120px',
- * height: '48px',
- * fontSize: '20px',
- * padding: '12px 24px'
- * });
- * timer.render('#app');
- * timer.start();
- *
- * SIZE OPTIONS:
- * width: CSS width value (default: '56px')
- * height: CSS height value (default: '27px')
- * fontSize: CSS font-size (default: '12px')
- * padding: CSS padding (default: '5px 10px')
- * borderRadius: CSS border-radius (default: '35px')
- *
- * METHODS:
- * start() - Start the timer
- * stop() - Stop/pause the timer
- * reset() - Reset to initial time
- * setTime(seconds) - Set new time
- * getTime() - Get current time remaining
- * isRunning() - Check if timer is active
- * destroy() - Clean up and remove from DOM
- */
-class GlassmorphismTimer {
- constructor(initialSeconds, options = {}) {
- this.initialTime = initialSeconds;
- this.currentTime = initialSeconds;
- this.isActive = false;
- this.intervalId = null;
-
- this.options = {
- width: options.width || '56px',
- height: options.height || '27px',
- fontSize: options.fontSize || '12px',
- padding: options.padding || '5px 10px',
- borderRadius: options.borderRadius || '35px',
- onClick: options.onClick || null,
- onStart: options.onStart || null,
- onStop: options.onStop || null,
- onComplete: options.onComplete || null,
- onTick: options.onTick || null,
- className: options.className || '',
- clickToToggle: options.clickToToggle !== false, // default true
- ...options,
- };
-
- this.element = null;
- this.timeDisplay = null;
- this.createElement();
- }
-
- // Base styles
- static styles = {
- container: [
- 'position: relative',
- 'display: inline-flex',
- 'align-items: center',
- 'justify-content: center',
- 'cursor: pointer',
- 'user-select: none',
- 'transition: all 0.2s ease',
- 'outline: none',
- ],
-
- background: [
- 'background: rgba(0, 0, 0, 0.25)',
- 'backdrop-filter: blur(12px) saturate(150%)',
- '-webkit-backdrop-filter: blur(12px) saturate(150%)',
- 'border: 1.5px solid rgba(255, 255, 255, 0.22)',
- 'box-shadow: 0px 4px 32px rgba(0, 0, 0, 0.35), 0px 8px 64px rgba(0, 0, 0, 0.12), inset 0px 1px 0px rgba(255, 255, 255, 0.27)',
- 'position: absolute',
- 'top: 0',
- 'left: 0',
- 'width: 100%',
- 'height: 100%',
- ],
-
- timeDisplay: [
- 'color: #34c759',
- 'text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.8)',
- 'font-family: "SF Mono", "Monaco", "Consolas", monospace',
- 'font-weight: 500',
- 'text-align: center',
- 'line-height: 1.2',
- 'position: absolute',
- 'top: 50%',
- 'left: 50%',
- 'transform: translate(-48%, -43%)',
- 'z-index: 1',
- 'white-space: nowrap',
- 'width: 100%',
- 'display: flex',
- 'align-items: center',
- 'justify-content: center',
- ],
-
- hover: ['transform: scale(1.02)', 'box-shadow: 0px 6px 40px rgba(0, 0, 0, 0.2)'],
-
- active: ['transform: scale(0.98)'],
-
- running: [
- 'box-shadow: 0px 4px 32px rgba(0, 0, 0, 0.35), 0px 8px 64px rgba(0, 0, 0, 0.12), 0px 0px 20px rgba(52, 199, 89, 0.6), inset 0px 1px 0px rgba(255, 255, 255, 0.27)',
- ],
- };
-
- createElement() {
- // Create main container
- this.element = document.createElement('div');
- this.element.className = 'glassmorphism-timer';
- this.element.style.cssText = this.getContainerStyles();
-
- // Create background element
- const background = document.createElement('div');
- background.className = 'timer-background';
- background.style.cssText = this.getBackgroundStyles();
-
- // Create time display
- this.timeDisplay = document.createElement('div');
- this.timeDisplay.className = 'timer-display';
- this.timeDisplay.style.cssText = this.getTimeDisplayStyles();
- this.timeDisplay.textContent = this.formatTime(this.currentTime);
-
- // Assemble elements
- this.element.appendChild(background);
- this.element.appendChild(this.timeDisplay);
-
- // Add event listeners
- this.addEventListeners();
-
- // Add custom classes
- if (this.options.className) {
- this.element.classList.add(this.options.className);
- }
- }
-
- getContainerStyles() {
- const size = this.getSizeConfig();
- const baseStyles = GlassmorphismTimer.styles.container.join('; ');
-
- return `${baseStyles}; width: ${size.width}; height: ${size.height}; border-radius: ${size.borderRadius};`;
- }
-
- getBackgroundStyles() {
- const size = this.getSizeConfig();
- const baseStyles = GlassmorphismTimer.styles.background.join('; ');
-
- return `${baseStyles}; border-radius: ${size.borderRadius};`;
- }
-
- getTimeDisplayStyles() {
- const size = this.getSizeConfig();
- const baseStyles = GlassmorphismTimer.styles.timeDisplay.join('; ');
-
- return `${baseStyles}; font-size: ${size.fontSize}; padding: ${size.padding};`;
- }
-
- getSizeConfig() {
- return {
- width: this.options.width,
- height: this.options.height,
- fontSize: this.options.fontSize,
- padding: this.options.padding,
- borderRadius: this.options.borderRadius,
- };
- }
-
- addEventListeners() {
- // Click handler
- this.element.addEventListener('click', (e) => {
- e.preventDefault();
-
- if (this.options.clickToToggle) {
- this.toggle();
- }
-
- if (this.options.onClick) {
- this.options.onClick(this);
- }
- });
-
- // Hover effects
- this.element.addEventListener('mouseenter', () => {
- this.element.style.transform = 'scale(1.02)';
- const bg = this.element.querySelector('.timer-background');
- bg.style.boxShadow = '0px 6px 40px rgba(0, 0, 0, 0.2)';
- });
-
- this.element.addEventListener('mouseleave', () => {
- this.element.style.transform = 'scale(1)';
- this.updateBackgroundShadow();
- });
-
- // Active state
- this.element.addEventListener('mousedown', () => {
- this.element.style.transform = 'scale(0.98)';
- });
-
- this.element.addEventListener('mouseup', () => {
- this.element.style.transform = 'scale(1.02)';
- });
-
- // Keyboard support
- this.element.addEventListener('keydown', (e) => {
- if (e.key === 'Enter' || e.key === ' ') {
- e.preventDefault();
- this.element.click();
- }
- });
-
- // Make focusable
- this.element.tabIndex = 0;
- }
-
- formatTime(seconds) {
- const mins = Math.floor(seconds / 60);
- const secs = seconds % 60;
- return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
- }
-
- updateDisplay() {
- if (this.timeDisplay) {
- this.timeDisplay.textContent = this.formatTime(this.currentTime);
- }
- }
-
- updateBackgroundShadow() {
- const bg = this.element?.querySelector('.timer-background');
- if (bg) {
- if (this.isActive) {
- bg.style.boxShadow =
- '0px 4px 32px rgba(0, 0, 0, 0.35), 0px 8px 64px rgba(0, 0, 0, 0.12), 0px 0px 20px rgba(52, 199, 89, 0.6), inset 0px 1px 0px rgba(255, 255, 255, 0.27)';
- } else {
- bg.style.boxShadow =
- '0px 4px 32px rgba(0, 0, 0, 0.35), 0px 8px 64px rgba(0, 0, 0, 0.12), inset 0px 1px 0px rgba(255, 255, 255, 0.27)';
- }
- }
- }
-
- // Timer control methods
- start() {
- if (this.isActive || this.currentTime <= 0) {
- return this;
- }
-
- this.isActive = true;
- this.updateBackgroundShadow();
-
- if (this.options.onStart) {
- this.options.onStart(this);
- }
-
- this.intervalId = setInterval(() => {
- this.currentTime--;
- this.updateDisplay();
-
- if (this.options.onTick) {
- this.options.onTick(this.currentTime, this);
- }
-
- if (this.currentTime <= 0) {
- this.complete();
- }
- }, 1000);
-
- return this;
- }
-
- stop() {
- if (!this.isActive) {
- return this;
- }
-
- this.isActive = false;
- this.updateBackgroundShadow();
-
- if (this.intervalId) {
- clearInterval(this.intervalId);
- this.intervalId = null;
- }
-
- if (this.options.onStop) {
- this.options.onStop(this);
- }
-
- return this;
- }
-
- toggle() {
- if (this.isActive) {
- this.stop();
- } else {
- this.start();
- }
- return this;
- }
-
- reset() {
- this.stop();
- this.currentTime = this.initialTime;
- this.updateDisplay();
- return this;
- }
-
- complete() {
- this.stop();
-
- if (this.options.onComplete) {
- this.options.onComplete(this);
- }
-
- return this;
- }
-
- // Getter/setter methods
- setTime(seconds) {
- const wasRunning = this.isActive;
- this.stop();
- this.currentTime = Math.max(0, Math.floor(seconds));
- this.updateDisplay();
-
- if (wasRunning && this.currentTime > 0) {
- this.start();
- }
-
- return this;
- }
-
- getTime() {
- return this.currentTime;
- }
-
- isRunning() {
- return this.isActive;
- }
-
- // Utility methods
- updateSize(sizeOptions) {
- // Update size options
- if (sizeOptions.width) {
- this.options.width = sizeOptions.width;
- }
- if (sizeOptions.height) {
- this.options.height = sizeOptions.height;
- }
- if (sizeOptions.fontSize) {
- this.options.fontSize = sizeOptions.fontSize;
- }
- if (sizeOptions.padding) {
- this.options.padding = sizeOptions.padding;
- }
- if (sizeOptions.borderRadius) {
- this.options.borderRadius = sizeOptions.borderRadius;
- }
-
- // Reapply styles
- this.element.style.cssText = this.getContainerStyles();
-
- const bg = this.element.querySelector('.timer-background');
- bg.style.cssText = this.getBackgroundStyles();
-
- this.timeDisplay.style.cssText = this.getTimeDisplayStyles();
-
- return this;
- }
-
- // Render method
- render(container) {
- if (typeof container === 'string') {
- container = document.querySelector(container);
- }
-
- if (!container) {
- throw new Error('Container not found');
- }
-
- container.appendChild(this.element);
- return this;
- }
-
- // Cleanup method
- destroy() {
- this.stop();
-
- if (this.element && this.element.parentNode) {
- this.element.parentNode.removeChild(this.element);
- }
-
- this.element = null;
- this.timeDisplay = null;
-
- return this;
- }
-
- // Static helper methods
- static createGroup(container, timers) {
- const group = document.createElement('div');
- group.className = 'timer-group';
- group.style.cssText = 'display: flex; gap: 8px; align-items: center;';
-
- timers.forEach((config) => {
- new GlassmorphismTimer(config.time, config.options).render(group);
- });
-
- if (typeof container === 'string') {
- container = document.querySelector(container);
- }
- container.appendChild(group);
-
- return group;
- }
-}
-
-// Export for use in modules
-if (typeof module !== 'undefined' && module.exports) {
- module.exports = GlassmorphismTimer;
-}
diff --git a/Shared (Extension)/Resources/content.js b/Shared (Extension)/Resources/content.js
index a650537..139c6d6 100644
--- a/Shared (Extension)/Resources/content.js
+++ b/Shared (Extension)/Resources/content.js
@@ -7,14 +7,13 @@ class ScrollStopCoordinator {
this.doomscrollAnimation = null;
this.transitionScreen = null;
this.blockingScreen = null;
- this.timerTracker = null;
this.choiceDialog = null;
this.periodicReminder = null;
this.grayscaleFilter = null;
this.isInitialized = false;
this.currentHostname = window.location.hostname;
- this.userChoice = null; // 'continue', 'timer-only', 'block'
+ this.userChoice = null; // 'continue', 'configure', 'block', 'bypass'
// Bind event handlers
this.handleDoomscrollDetected = this.handleDoomscrollDetected.bind(this);
@@ -51,25 +50,6 @@ class ScrollStopCoordinator {
}
}
- /**
- * Initialize timer tracker for all tracked sites (social media and news)
- */
- async initializeTimerTracker() {
- try {
- // Check if current site is tracked (either blocked or news site)
- const url = window.location.href;
- const hostname = window.location.hostname;
- const siteType = await StorageHelper.getCurrentSiteType(url, hostname);
-
- if (siteType.isBlocked || siteType.isNews || siteType.isAdult) {
- this.timerTracker = new TimerTracker();
- await this.timerTracker.initialize(siteType.isNews);
- }
- } catch (error) {
- console.error('Error initializing timer tracker:', error);
- }
- }
-
/**
* Set up event listeners for inter-module communication
*/
@@ -104,6 +84,21 @@ class ScrollStopCoordinator {
// Store site type for later use
this.currentSiteType = siteType;
+ // Check if bypass mode is active
+ if (this.checkBypassStatus()) {
+ console.log('ScrollStop: Bypass mode active, not showing dialog');
+ return;
+ }
+
+ // Check if user wants to configure activities (from popup)
+ const shouldShowQuestionnaire = localStorage.getItem('scrollstop_show_questionnaire');
+ if (shouldShowQuestionnaire === 'true') {
+ localStorage.removeItem('scrollstop_show_questionnaire');
+ console.log('ScrollStop: User requested questionnaire from popup, showing directly');
+ this.showQuestionnaireConfig();
+ return;
+ }
+
// Always show choice dialog on every page load (no session persistence)
console.log('ScrollStop: No session persistence - will show choice dialog');
@@ -265,22 +260,22 @@ class ScrollStopCoordinator {
async proceedWithChoice(choice) {
switch (choice) {
case 'continue':
- // Full ScrollStop functionality - initialize timer then start detection
- await this.initializeTimerTracker();
+ // Full ScrollStop functionality - start detection
// Only start doomscroll detection for blocked sites, not news sites
if (this.currentSiteType && this.currentSiteType.isBlocked) {
this.startDoomscrollDetection();
}
+ // Start grayscale filter tracking ONLY for 'continue' choice
+ this.startGrayscaleFilter();
break;
- case 'timer-only':
- // Only show timer, no blocking
- await this.initializeTimerOnly();
- break;
+ case 'configure':
+ // Show configuration interface
+ this.showQuestionnaireConfig();
+ return; // Don't proceed with normal initialization
case 'block':
- // Initialize timer first, then immediately block the site
- await this.initializeTimerTracker();
+ // Immediately block the site
if (this.currentSiteType && this.currentSiteType.isNews) {
// For news sites, create news time block
await TimeManager.createNewsTimeBlock();
@@ -291,45 +286,63 @@ class ScrollStopCoordinator {
this.showBlockingScreen();
break;
+ case 'bypass':
+ // Bypass ScrollStop completely - set a delay before showing choice dialog again
+ this.setBypassMode();
+ break;
+
default:
console.warn('Unknown choice:', choice);
// Default to continue
- await this.initializeTimerTracker();
if (this.currentSiteType && this.currentSiteType.isBlocked) {
this.startDoomscrollDetection();
}
+ // Start grayscale filter for default case too
+ this.startGrayscaleFilter();
break;
}
- // Start periodic reminder for all choices (except when immediately blocked)
- if (choice !== 'block') {
+ // Start periodic reminder for all choices (except when immediately blocked or bypassed)
+ if (choice !== 'block' && choice !== 'bypass') {
this.startPeriodicReminder();
}
-
- // Start grayscale filter tracking for all choices (except when immediately blocked)
- if (choice !== 'block') {
- this.startGrayscaleFilter();
- }
}
/**
- * Initialize timer-only mode
+ * Set bypass mode - extension acts inactive for 5 minutes (same as periodic reminder)
*/
- async initializeTimerOnly() {
- try {
- // Initialize timer but not doomscroll detection
- if (!this.timerTracker) {
- this.timerTracker = new TimerTracker();
- await this.timerTracker.initialize();
- }
+ setBypassMode() {
+ console.log('ScrollStop: Setting bypass mode for 5 minutes');
+
+ // Store bypass timestamp
+ const bypassUntil = Date.now() + 5 * 60 * 1000; // 5 minutes
+ localStorage.setItem('scrollstop_bypass_until', bypassUntil.toString());
+
+ // Clean up any active modules
+ this.cleanup();
+
+ // Set a timer to show choice dialog again after bypass period
+ setTimeout(
+ () => {
+ this.checkBypassStatus();
+ },
+ 5 * 60 * 1000
+ ); // 5 minutes
+ }
- // Set timer to timer-only mode to prevent hiding
- if (this.timerTracker.setTimerOnlyMode) {
- this.timerTracker.setTimerOnlyMode(true);
- }
- } catch (error) {
- console.error('Error initializing timer-only mode:', error);
+ /**
+ * Check if bypass mode is still active
+ */
+ checkBypassStatus() {
+ const bypassUntil = localStorage.getItem('scrollstop_bypass_until');
+ if (bypassUntil && Date.now() < parseInt(bypassUntil)) {
+ return true; // Still in bypass mode
}
+
+ // Bypass expired, remove flag and show choice dialog
+ localStorage.removeItem('scrollstop_bypass_until');
+ this.showChoiceDialog();
+ return false;
}
/**
@@ -360,7 +373,7 @@ class ScrollStopCoordinator {
if (!this.grayscaleFilter) {
this.grayscaleFilter = new window.GrayscaleFilter({
- timeLimit: 5 * 60 * 1000, // 5 minutes
+ timeLimit: 45 * 60 * 1000, // 45 minutes
filterDuration: 60 * 60 * 1000, // 1 hour
});
}
@@ -371,6 +384,32 @@ class ScrollStopCoordinator {
}
}
+ /**
+ * Show questionnaire configuration interface
+ */
+ showQuestionnaireConfig() {
+ try {
+ console.log('ScrollStop: Showing questionnaire configuration');
+
+ const config = new window.QuestionnaireConfig({
+ onComplete: () => {
+ console.log('ScrollStop: Configuration completed, proceeding with continue mode');
+ this.proceedWithChoice('continue');
+ },
+ onCancel: () => {
+ console.log('ScrollStop: Configuration cancelled, showing choice dialog again');
+ this.showChoiceDialog();
+ },
+ });
+
+ config.render();
+ } catch (error) {
+ console.error('ScrollStop: Error showing questionnaire config:', error);
+ // Fallback to continue mode if config fails
+ this.proceedWithChoice('continue');
+ }
+ }
+
/**
* Clean up all modules and event listeners
*/
@@ -396,11 +435,6 @@ class ScrollStopCoordinator {
this.blockingScreen = null;
}
- if (this.timerTracker) {
- this.timerTracker.cleanup();
- this.timerTracker = null;
- }
-
if (this.choiceDialog) {
this.choiceDialog.cleanup();
this.choiceDialog = null;
@@ -428,7 +462,7 @@ class ScrollStopCoordinator {
}
/**
- * Handle messages from background script
+ * Handle messages from background script and popup
*/
handleMessage(message, sender, sendResponse) {
if (message.action === 'checkBlockedSite') {
@@ -438,6 +472,13 @@ class ScrollStopCoordinator {
}
});
return true; // Indicates async response
+ } else if (message.action === 'showQuestionnaire') {
+ // Show questionnaire configuration directly
+ this.showQuestionnaireConfig();
+ if (sendResponse) {
+ sendResponse({ success: true });
+ }
+ return true;
}
}
}
diff --git a/Shared (Extension)/Resources/manifest.json b/Shared (Extension)/Resources/manifest.json
index 6f493a6..4aa139e 100644
--- a/Shared (Extension)/Resources/manifest.json
+++ b/Shared (Extension)/Resources/manifest.json
@@ -23,12 +23,14 @@
"content_scripts": [
{
"js": [
- "timer.js",
"storage-helper.js",
"time-manager.js",
- "timer-tracker.js",
"button.js",
+ "badge.js",
"dialog.js",
+ "divider.js",
+ "swipe-cards.js",
+ "questionnaire-config.js",
"choice-dialog.js",
"doomscroll-detector.js",
"doomscroll-animation.js",
@@ -48,7 +50,7 @@
"default_icon": "images/toolbar-icon.svg"
},
- "permissions": ["storage"],
+ "permissions": ["storage", "tabs"],
"host_permissions": [""],
"web_accessible_resources": [
@@ -57,7 +59,9 @@
"sites.json",
"doomscroll-animation.css",
"transition-screen.css",
- "blocking-screen.css"
+ "blocking-screen.css",
+ "popup.css",
+ "popup.js"
],
"matches": [""]
}
diff --git a/Shared (Extension)/Resources/modules/blocking-screen/blocking-screen.js b/Shared (Extension)/Resources/modules/blocking-screen/blocking-screen.js
index b31e6ef..f4dfd3d 100644
--- a/Shared (Extension)/Resources/modules/blocking-screen/blocking-screen.js
+++ b/Shared (Extension)/Resources/modules/blocking-screen/blocking-screen.js
@@ -1,5 +1,5 @@
// modules/blocking-screen/blocking-screen.js
-// Module for showing the full blocking screen with countdown
+// Modern blocking screen with activity suggestions and timer
if (typeof window.BlockingScreen === 'undefined') {
class BlockingScreen {
@@ -12,6 +12,10 @@ if (typeof window.BlockingScreen === 'undefined') {
this.blockingElement = null;
this.countdownInterval = null;
this.hostname = window.location.hostname;
+ this.swipeCards = null;
+ this.activityTimer = null;
+ this.currentMode = 'blocked'; // 'blocked', 'activities', 'timer'
+ this.selectedActivity = null;
}
/**
@@ -48,20 +52,24 @@ if (typeof window.BlockingScreen === 'undefined') {
this.blockingElement.id = 'time-block-screen';
this.blockingElement.className = 'blocking-screen';
- this.applyBlockingStyles();
- await this.updateBlockingContent();
+ this.applyModernStyles();
+ await this.createModernContent();
document.body.appendChild(this.blockingElement);
}
/**
- * Apply styles to blocking screen element
+ * Apply modern B&W styles to blocking screen element with dark mode support
*/
- applyBlockingStyles() {
+ applyModernStyles() {
if (!this.blockingElement) {
return;
}
+ // Detect dark mode
+ const isDarkMode =
+ window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
+
this.blockingElement.style.cssText = `
height: 100vh;
width: 100vw;
@@ -69,66 +77,533 @@ if (typeof window.BlockingScreen === 'undefined') {
top: 0;
left: 0;
z-index: 9999999;
- display: flex;
- justify-content: center;
- align-items: center;
- flex-direction: column;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Arial, sans-serif;
- text-align: center;
- padding: 2rem;
+ background: ${isDarkMode ? '#1f2937' : '#f8fafc'};
+ color: ${isDarkMode ? '#f9fafb' : '#1f2937'};
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ overflow-y: auto;
box-sizing: border-box;
`;
}
/**
- * Update blocking screen content
+ * Create modern blocking screen content
*/
- async updateBlockingContent() {
+ async createModernContent() {
if (!this.blockingElement) {
return;
}
- const suggestionsHTML = await this.getSuggestionsHTML();
-
// Check if this is a news site
const isNews = await StorageHelper.isCurrentSiteNews(window.location.href, this.hostname);
+ const siteName = this.hostname.replace('www.', '').split('.')[0];
- const emoji = isNews ? '📰' : '🌱';
- const title = isNews ? 'News Break Time!' : 'Time to Touch Grass!';
- const message = isNews
- ? "You've spent 20 minutes reading news today. Take a break for 60 minutes."
- : "You've been scrolling too much. This site is blocked for 60 minutes.";
+ // Detect dark mode for styling
+ const isDarkMode =
+ window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
this.blockingElement.innerHTML = `
-
-
${emoji}
-
- ${title}
-
-
- ${message}
-
-
-
+
-
- Loading...
+
+
+
+
+ ScrollStop
+
+
+
+ ${siteName} is blocked
+
+
+
+ ${
+ isNews
+ ? "You've reached your 20-minute daily news limit"
+ : 'Take a break from scrolling and try something productive'
+ }
+
+
+
+
+
+ Loading...
+
+
+ Time remaining
+
+
-
- until you can access ${isNews ? 'news sites' : 'this site'} again
+
+
+
+
+ Better things to do right now
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- ${suggestionsHTML}
`;
+
+ // Initialize activity cards for all platforms
+ await this.initializeActivityCards();
+
+ this.createActionButtons();
+ }
+
+ /**
+ * Initialize swipeable activity cards
+ */
+ async initializeActivityCards() {
+ const container = document.getElementById('activities-container');
+ if (!container) {
+ return;
+ }
+
+ try {
+ const personalData = await this.getPersonalData();
+ const activities = this.generateActivityCards(personalData);
+
+ if (activities.length === 0) {
+ container.innerHTML = `
+
+
🎯
+
No activities available
+
+ `;
+ return;
+ }
+
+ // Create swipe cards component
+ if (!window.SwipeCards) {
+ console.error('SwipeCards not available on window');
+ container.innerHTML = `
+
+ SwipeCards component not loaded
+
+ `;
+ return;
+ }
+
+ this.swipeCards = new window.SwipeCards(activities, {
+ onSwipeRight: (activity) => this.handleActivityAccepted(activity),
+ onSwipeLeft: (activity) => this.handleActivityRejected(activity),
+ onComplete: () => this.handleAllActivitiesCompleted(),
+ });
+
+ this.swipeCards.render(container);
+ } catch (error) {
+ console.error('Error initializing activity cards:', error);
+ container.innerHTML = `
+
+ Error loading activities
+
+ `;
+ }
+ }
+
+ /**
+ * Generate activity cards from personal data
+ */
+ generateActivityCards(personalData) {
+ const activities = [];
+
+ if (!personalData.hasData) {
+ return this.getDefaultActivityCards();
+ }
+
+ // Add current tasks (highest priority)
+ if (personalData.currentTasks && personalData.currentTasks.length > 0) {
+ personalData.currentTasks.forEach((task) => {
+ activities.push({
+ emoji: '✅',
+ text: task,
+ description: 'Current task to complete',
+ category: 'current',
+ });
+ });
+ }
+
+ // Add household tasks
+ if (personalData.householdTasks && personalData.householdTasks.length > 0) {
+ personalData.householdTasks.forEach((task) => {
+ activities.push({
+ emoji: '🏠',
+ text: task,
+ description: 'Household task',
+ category: 'household',
+ });
+ });
+ }
+
+ // Add friends to contact
+ if (personalData.friends && personalData.friends.length > 0) {
+ personalData.friends.forEach((friend) => {
+ activities.push({
+ emoji: '📞',
+ text: `Call ${friend}`,
+ description: 'Connect with someone you care about',
+ category: 'social',
+ });
+ });
+ }
+
+ // Add hobbies
+ if (personalData.hobbies && personalData.hobbies.length > 0) {
+ personalData.hobbies.forEach((hobby) => {
+ activities.push({
+ emoji: '🎯',
+ text: hobby,
+ description: 'Pursue your interests',
+ category: 'hobby',
+ });
+ });
+ }
+
+ // Add books
+ if (personalData.books && personalData.books.length > 0) {
+ personalData.books.forEach((book) => {
+ activities.push({
+ emoji: '📚',
+ text: `Read "${book}"`,
+ description: 'Expand your mind',
+ category: 'reading',
+ });
+ });
+ }
+
+ // Add goals
+ if (personalData.goals && personalData.goals.length > 0) {
+ personalData.goals.forEach((goal) => {
+ activities.push({
+ emoji: '🎯',
+ text: `Work on: ${goal}`,
+ description: 'Make progress on your goals',
+ category: 'goal',
+ });
+ });
+ }
+
+ // Shuffle activities for variety
+ return this.shuffleArray(activities).slice(0, 10); // Max 10 activities
+ }
+
+ /**
+ * Get default activity cards when no personal data is available
+ */
+ getDefaultActivityCards() {
+ return [
+ {
+ emoji: '🚶♂️',
+ text: 'Take a walk outside',
+ description: 'Get some fresh air and movement',
+ },
+ {
+ emoji: '📚',
+ text: 'Read a book',
+ description: 'Engage your mind with something meaningful',
+ },
+ {
+ emoji: '🧘♀️',
+ text: 'Meditate for 5 minutes',
+ description: 'Practice mindfulness and calm your thoughts',
+ },
+ {
+ emoji: '💪',
+ text: 'Do some exercise',
+ description: 'Get your body moving and energy flowing',
+ },
+ {
+ emoji: '👥',
+ text: 'Call a friend or family member',
+ description: 'Connect with someone you care about',
+ },
+ {
+ emoji: '🎨',
+ text: 'Be creative',
+ description: 'Draw, write, or make something with your hands',
+ },
+ {
+ emoji: '🧹',
+ text: 'Tidy up your space',
+ description: 'Organize your environment for mental clarity',
+ },
+ {
+ emoji: '💧',
+ text: 'Drink some water',
+ description: 'Hydrate and take care of your body',
+ },
+ {
+ emoji: '🌱',
+ text: 'Do some stretching',
+ description: 'Release tension and improve flexibility',
+ },
+ {
+ emoji: '📝',
+ text: 'Write in a journal',
+ description: 'Reflect on your thoughts and feelings',
+ },
+ ];
+ }
+
+ /**
+ * Shuffle array for randomness
+ */
+ shuffleArray(array) {
+ const shuffled = [...array];
+ for (let i = shuffled.length - 1; i > 0; i--) {
+ const j = Math.floor(Math.random() * (i + 1));
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
+ }
+ return shuffled;
+ }
+
+ /**
+ * Handle when user swipes right (accepts) an activity
+ */
+ handleActivityAccepted(activity) {
+ console.log('Activity accepted:', activity);
+ this.selectedActivity = activity;
+ this.showActivityTimer();
+ }
+
+ /**
+ * Handle when user swipes left (rejects) an activity
+ */
+ handleActivityRejected(activity) {
+ console.log('Activity rejected:', activity);
+ // Just continue to next card
+ }
+
+ /**
+ * Handle when all activities are completed
+ */
+ handleAllActivitiesCompleted() {
+ const container = document.getElementById('activities-container');
+ if (container) {
+ container.innerHTML = `
+
+
🎉
+
+ You've seen all available activities!
+
+
+ `;
+ }
+ }
+
+ /**
+ * Show the activity timer section
+ */
+ showActivityTimer() {
+ this.currentMode = 'timer';
+
+ // Hide activities section
+ const activitiesSection = document.getElementById('activities-section');
+ if (activitiesSection) {
+ activitiesSection.style.display = 'none';
+ }
+
+ // Show timer section
+ const timerSection = document.getElementById('timer-section');
+ if (timerSection) {
+ timerSection.style.display = 'block';
+ }
+
+ // Create activity timer
+ const timerContainer = document.getElementById('timer-container');
+ if (timerContainer && this.selectedActivity) {
+ this.activityTimer = new window.ActivityTimer({
+ duration: 5 * 60 * 1000, // 5 minutes
+ onComplete: () => this.handleTimerComplete(),
+ onTick: (remaining) => this.handleTimerTick(remaining),
+ });
+
+ this.activityTimer.render(timerContainer);
+ this.activityTimer.setActivity(this.selectedActivity);
+ }
+
+ // Update action buttons
+ this.createActionButtons();
+ }
+
+ /**
+ * Handle timer completion
+ */
+ handleTimerComplete() {
+ console.log('Activity timer completed');
+ // Timer component handles the completion dialog internally
+ }
+
+ /**
+ * Handle timer tick
+ */
+ handleTimerTick(_remaining) {
+ // Optional: Update any global state or UI
+ }
+
+ /**
+ * Create action buttons based on current mode (only on iOS)
+ */
+ createActionButtons() {
+ const container = document.getElementById('action-buttons');
+ if (!container) {
+ return;
+ }
+
+ container.innerHTML = '';
+
+ // Action buttons available on all platforms now
+
+ if (this.currentMode === 'blocked') {
+ // Show "Skip Activities" button with subtle green accent
+ const skipButton = new window.HeadlessButton('Skip activities', {
+ color: 'zinc',
+ outline: true,
+ onClick: () => this.skipActivities(),
+ });
+
+ container.appendChild(skipButton.element);
+ } else if (this.currentMode === 'timer') {
+ // Show "Back to Activities" button
+ const backButton = new window.HeadlessButton('← Back to activities', {
+ color: 'zinc',
+ outline: true,
+ onClick: () => this.backToActivities(),
+ });
+
+ container.appendChild(backButton.element);
+ }
+ }
+
+ /**
+ * Skip activities and just show countdown
+ */
+ skipActivities() {
+ const activitiesSection = document.getElementById('activities-section');
+ const actionButtons = document.getElementById('action-buttons');
+
+ if (activitiesSection) {
+ activitiesSection.style.display = 'none';
+ }
+ if (actionButtons) {
+ actionButtons.style.display = 'none';
+ }
+ }
+
+ /**
+ * Go back to activities from timer
+ */
+ backToActivities() {
+ this.currentMode = 'blocked';
+
+ // Clean up timer
+ if (this.activityTimer) {
+ this.activityTimer.destroy();
+ this.activityTimer = null;
+ }
+
+ // Hide timer section
+ const timerSection = document.getElementById('timer-section');
+ if (timerSection) {
+ timerSection.style.display = 'none';
+ }
+
+ // Show activities section
+ const activitiesSection = document.getElementById('activities-section');
+ if (activitiesSection) {
+ activitiesSection.style.display = 'block';
+ }
+
+ // Update action buttons
+ this.createActionButtons();
}
/**
@@ -414,10 +889,26 @@ if (typeof window.BlockingScreen === 'undefined') {
cleanup() {
this.stopCountdownUpdates();
+ // Clean up swipe cards
+ if (this.swipeCards) {
+ this.swipeCards.destroy();
+ this.swipeCards = null;
+ }
+
+ // Clean up activity timer
+ if (this.activityTimer) {
+ this.activityTimer.destroy();
+ this.activityTimer = null;
+ }
+
if (this.blockingElement && this.blockingElement.parentNode) {
this.blockingElement.parentNode.removeChild(this.blockingElement);
this.blockingElement = null;
}
+
+ // Reset state
+ this.currentMode = 'blocked';
+ this.selectedActivity = null;
}
/**
diff --git a/Shared (Extension)/Resources/modules/choice-dialog/choice-dialog.js b/Shared (Extension)/Resources/modules/choice-dialog/choice-dialog.js
index 4044d62..acf9496 100644
--- a/Shared (Extension)/Resources/modules/choice-dialog/choice-dialog.js
+++ b/Shared (Extension)/Resources/modules/choice-dialog/choice-dialog.js
@@ -77,34 +77,48 @@ class ChoiceDialog {
// Create dialog body (empty, no subtitle)
const body = new HeadlessDialogBody();
- // Create action buttons using HeadlessButton default styling
+ // Create action buttons using HeadlessButton with better visibility
const continueBtn = new HeadlessButton('Continue with ScrollStop', {
- color: 'blue',
+ color: 'green',
onClick: () => this.handleChoice('continue', resolve),
});
- const timerOnlyBtn = new HeadlessButton('Timer Only', {
+ const bypassBtn = new HeadlessButton('Continue without ScrollStop', {
color: 'zinc',
outline: true,
- onClick: () => this.handleChoice('timer-only', resolve),
+ onClick: () => this.handleChoice('bypass', resolve),
});
const blockBtn = new HeadlessButton('Block Now', {
color: 'red',
- outline: true,
onClick: () => this.handleChoice('block', resolve),
});
+ const configureBtn = new HeadlessButton('Configure Activities', {
+ color: 'dark',
+ onClick: () => this.handleChoice('configure', resolve),
+ });
+
console.log('ChoiceDialog: Buttons created, assembling content...');
- // Create simple button container without descriptions
+ // Create button container with divider structure
const buttonContainer = document.createElement('div');
buttonContainer.className = 'space-y-3';
+ // Main actions (above divider)
buttonContainer.appendChild(continueBtn.element);
- buttonContainer.appendChild(timerOnlyBtn.element);
+ buttonContainer.appendChild(bypassBtn.element);
buttonContainer.appendChild(blockBtn.element);
+ // Add divider
+ if (window.HeadlessDivider) {
+ const divider = new window.HeadlessDivider({ soft: true });
+ divider.render(buttonContainer);
+ }
+
+ // Configuration action (below divider)
+ buttonContainer.appendChild(configureBtn.element);
+
console.log('ChoiceDialog: Adding content to dialog panel...');
// Add all content to dialog panel manually to avoid render issues
@@ -231,7 +245,40 @@ class ChoiceDialog {
appearance: none !important;
-webkit-appearance: none !important;
">Continue with ScrollStop
-
+
+ ">Configure Activities
-
ScrollStop - Blocked Sites
+
ScrollStop
-
+
+
+
+
+