Browse Source

chore(init): added initial code

Leon Versteeg 1 month ago
commit
a13377711d
8 changed files with 1835 additions and 0 deletions
  1. 238 0
      README.md
  2. 7 0
      compose.yaml
  3. 490 0
      docs/bookmarks.html
  4. 310 0
      docs/config.html
  5. 34 0
      docs/data.json
  6. 22 0
      docs/index.html
  7. 631 0
      docs/script.js
  8. 103 0
      docs/style.css

+ 238 - 0
README.md

@@ -0,0 +1,238 @@
+# Startpage
+
+A minimal, keyboard-driven browser startpage with bookmark management and customizable themes.
+
+## Features
+
+- **Keyboard-First Navigation**: Navigate and search entirely with your keyboard
+- **Real-time Bookmark Search**: Instantly filter bookmarks by name, URL, or tags
+- **Command System**: Vim-style commands for quick actions
+- **Customizable Appearance**: Configure colors, background images, blur, and mask overlays
+- **Smart URL Detection**: Automatically opens domains or searches the web
+- **Import/Export**: Backup and restore your settings and bookmarks
+- **localStorage Support**: All settings persist locally with JSON fallback
+
+## Quick Start
+
+1. Clone this repository
+   ```bash
+   git clone https://github.com/aaaooai/startpage.git
+   cd startpage
+   ```
+2. Open `docs/index.html` in your browser
+3. Set it as your browser's homepage
+
+### Local Development with Docker/Podman
+
+You can run the startpage locally using Docker or Podman:
+
+**Using Docker Compose or Podman Compose:**
+```bash
+docker compose up -d
+# or
+podman compose up -d
+```
+
+**Using Docker or Podman directly:**
+```bash
+# Docker
+docker run --rm -it -v ./docs:/usr/share/nginx/html -p 8000:80 nginx:latest
+
+# Podman
+podman run --rm -it -v ./docs:/usr/share/nginx/html:Z -p 8000:80 docker.io/nginx:latest
+```
+
+Then access at `http://localhost:8000`
+
+### GitHub Pages Deployment
+
+The startpage is designed to work with GitHub Pages:
+
+1. Push the repository to GitHub
+2. Enable GitHub Pages in repository settings
+3. Select the `docs` folder as the source
+4. Access at `https://<username>.github.io/<repository>/`
+
+## Usage
+
+### Basic Search
+
+- **Type to search**: Start typing to filter bookmarks in real-time
+- **Direct URL**: Enter a domain (e.g., `github.com`) to open it directly
+- **Web search**: Enter any text to search with your configured search engine
+- **Arrow keys**: Navigate through results with `↑` and `↓`
+- **Enter**: Open selected bookmark or perform search
+- **Escape**: Clear input field
+
+### Commands
+
+All commands start with `:` and support autocomplete:
+
+- `:list` - Show all bookmarks
+- `:config` - Open settings page
+- `:bookmark` - Edit bookmarks
+- `:export` - Export settings and bookmarks as JSON
+- `:import` - Import settings and bookmarks from JSON
+- `:help` - Show keyboard shortcuts
+- `:reset` - Reset all settings to default (with confirmation)
+
+### Managing Bookmarks
+
+Access the bookmark editor with `:bookmark` or navigate to `bookmarks.html`.
+
+#### Adding Bookmarks
+
+- Click any `+` button between bookmarks to insert at that position
+- Fill in the name, URL, and optional tags
+- Changes are saved automatically
+
+#### Editing Bookmarks
+
+- **Name**: Required field (displays in search results)
+- **URL**: Required field, must be a valid URL format
+- **Tags**: Optional, comma-separated with space after comma (e.g., `dev, code, web`)
+- Invalid fields are highlighted with a red border
+- Tags are auto-formatted on blur
+
+#### Reordering Bookmarks
+
+- Drag bookmarks by the `≡` handle on the right
+- A blue line shows where the bookmark will be inserted
+- Order is saved automatically
+
+#### Deleting Bookmarks
+
+- Click the `×` button in the top-right corner of any bookmark
+- Confirmation dialog will appear
+- Invalid bookmarks (empty URL) are removed when navigating back to the startpage
+
+### Customizing Appearance
+
+Access settings with `:config` or navigate to `config.html`.
+
+#### Basic Colors
+
+- **Background Color**: Main background color
+- **Text Color**: Primary text color
+- **Accent Color**: Highlight color for selected items and borders
+
+#### Background Image
+
+- **Background Image URL**: Local file path or external URL
+- **Background Blur**: 0-20px blur effect (use slider)
+- **Mask Color**: Color overlay on background image
+- **Mask Opacity**: 0-100% opacity for mask (use slider, default: 60%)
+
+*Tip: Use blur and mask to improve text readability over background images*
+
+#### Search Engine
+
+Enter your preferred search engine URL with query parameter, e.g.:
+- `https://www.google.com/search?q=`
+- `https://duckduckgo.com/?q=`
+- `https://www.startpage.com/search?q=`
+
+All settings are previewed in real-time and saved automatically.
+
+## Sharing Configuration
+
+You can share your configuration and bookmarks across devices using Git:
+
+1. Export your current settings using `:export`
+2. Copy the exported content to `docs/data.json`
+3. Commit and push to Git:
+   ```bash
+   git add docs/data.json
+   git commit -m "Update shared configuration"
+   git push
+   ```
+4. On other devices, pull the changes and use `:reset` to clear localStorage, then reload to use the shared configuration
+
+## Data Management
+
+### Export Settings
+
+1. Use `:export` command
+2. A JSON file will download with timestamp: `startpage-export-YYYY-MM-DD.json`
+3. Contains all settings and bookmarks
+
+### Import Settings
+
+1. Use `:import` command
+2. Select a previously exported JSON file
+3. Page will reload with imported settings
+
+### Reset to Default
+
+1. Use `:reset` command
+2. Confirm the action
+3. All localStorage data is cleared and page reloads
+
+## File Structure
+
+```
+startpage/
+├── docs/
+│   ├── index.html          # Main startpage
+│   ├── config.html         # Settings page
+│   ├── bookmarks.html      # Bookmark editor
+│   ├── script.js           # Main application logic
+│   ├── style.css           # Shared styles
+│   └── data.json           # Default configuration and bookmarks (fallback)
+├── compose.yaml            # Docker Compose configuration
+└── README.md
+```
+
+## Default Data Structure
+
+The `data.json` file contains both configuration and bookmarks:
+
+```json
+{
+  "config": {
+    "backgroundColor": "#000000",
+    "textColor": "#ffffff",
+    "accentColor": "#4a9eff",
+    "searchEngine": "https://www.startpage.com/search?q=",
+    "backgroundImage": "",
+    "backgroundBlur": 0,
+    "maskColor": "#000000",
+    "maskOpacity": 60
+  },
+  "bookmarks": [
+    {
+      "name": "GitHub",
+      "url": "https://github.com",
+      "tags": ["dev", "code"]
+    }
+  ]
+}
+```
+
+This matches the export format, making it easy to share configurations via Git.
+
+## Keyboard Shortcuts
+
+| Key | Action |
+|-----|--------|
+| `Type` | Auto-focus search and filter bookmarks |
+| `:` | Show command suggestions |
+| `↓` / `↑` | Navigate results |
+| `Enter` | Open bookmark / Execute command / Search |
+| `Esc` | Clear input / Close help |
+
+## Browser Compatibility
+
+- Modern browsers with ES6+ support
+- localStorage API required
+- Tested on: Chrome, Firefox, Safari, Edge
+
+## Privacy
+
+- All data stored locally in browser's localStorage
+- No external requests except for user-configured background images
+- No analytics or tracking
+
+## License
+
+MIT License - Feel free to modify and use as you wish.

+ 7 - 0
compose.yaml

@@ -0,0 +1,7 @@
+services:
+  startpage:
+    image: docker.io/nginx:latest
+    volumes:
+      - ./docs:/usr/share/nginx/html
+    ports:
+      - 8000:80

+ 490 - 0
docs/bookmarks.html

@@ -0,0 +1,490 @@
+<!DOCTYPE html>
+<html lang="ja">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Bookmarks - Startpage</title>
+  <link rel="stylesheet" href="style.css">
+  <style>
+    .bookmarks-container {
+      max-width: 800px;
+      margin: 0 auto;
+      padding: 40px 20px;
+    }
+
+    h1 {
+      font-size: 2rem;
+      margin-bottom: 40px;
+      text-align: center;
+    }
+
+    .back-link {
+      display: inline-block;
+      margin-bottom: 20px;
+      color: #4a9eff;
+      text-decoration: none;
+    }
+
+    .back-link:hover {
+      text-decoration: underline;
+    }
+
+    .bookmark-list {
+      margin-bottom: 30px;
+    }
+
+    .bookmark-edit-item {
+      display: flex;
+      gap: 12px;
+      align-items: center;
+      margin-bottom: 12px;
+      position: relative;
+    }
+
+    .bookmark-edit-item.dragging .bookmark-card {
+      opacity: 0.5;
+    }
+
+    .bookmark-edit-item.drag-over .bookmark-card {
+      border-top: 2px solid #4a9eff;
+    }
+
+    .bookmark-card {
+      background-color: rgba(255, 255, 255, 0.05);
+      border: 1px solid #333;
+      border-radius: 4px;
+      padding: 16px;
+      flex: 1;
+      position: relative;
+    }
+
+    .bookmark-fields {
+      flex: 1;
+    }
+
+    .drag-handle {
+      flex-shrink: 0;
+      width: 24px;
+      cursor: grab;
+      color: #666;
+      font-size: 1.2rem;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      user-select: none;
+    }
+
+    .drag-handle:active {
+      cursor: grabbing;
+    }
+
+    .bookmark-edit-item input {
+      width: 100%;
+      padding: 8px;
+      margin-bottom: 8px;
+      background-color: rgba(255, 255, 255, 0.05);
+      border: 1px solid #444;
+      color: #fff;
+      font-family: 'Courier New', monospace;
+      font-size: 0.95rem;
+      border-radius: 4px;
+    }
+
+    .bookmark-edit-item input.error {
+      border-color: #d9534f;
+      background-color: rgba(217, 83, 79, 0.1);
+    }
+
+    .bookmark-edit-item label {
+      display: block;
+      margin-bottom: 4px;
+      font-size: 0.85rem;
+      color: #aaa;
+    }
+
+    .delete-btn {
+      position: absolute;
+      top: 8px;
+      right: 8px;
+      background: none;
+      border: none;
+      color: #666;
+      cursor: pointer;
+      font-size: 1.2rem;
+      padding: 4px 8px;
+      line-height: 1;
+      transition: color 0.2s;
+    }
+
+    .delete-btn:hover {
+      color: #d9534f;
+    }
+
+    .add-bookmark-separator {
+      display: flex;
+      align-items: center;
+      margin: 8px 0;
+    }
+
+    .add-bookmark-separator::before,
+    .add-bookmark-separator::after {
+      content: '';
+      flex: 1;
+      height: 1px;
+      background-color: #333;
+    }
+
+    .add-bookmark-btn {
+      background: none;
+      border: 1px solid #444;
+      color: #666;
+      font-family: 'Courier New', monospace;
+      font-size: 1rem;
+      cursor: pointer;
+      border-radius: 4px;
+      padding: 4px 12px;
+      margin: 0 12px;
+      transition: color 0.2s, border-color 0.2s;
+    }
+
+    .add-bookmark-btn:hover {
+      color: #aaa;
+      border-color: #666;
+    }
+
+    .button-group {
+      display: flex;
+      gap: 12px;
+    }
+
+    button {
+      flex: 1;
+      padding: 14px;
+      background-color: #4a9eff;
+      border: none;
+      color: #fff;
+      font-family: 'Courier New', monospace;
+      font-size: 1rem;
+      cursor: pointer;
+      border-radius: 4px;
+      transition: background-color 0.2s;
+    }
+
+    button:hover {
+      background-color: #3a8eef;
+    }
+
+    button.secondary {
+      background-color: #333;
+    }
+
+    button.secondary:hover {
+      background-color: #444;
+    }
+
+    .hint {
+      font-size: 0.85rem;
+      color: #666;
+      margin-top: 4px;
+    }
+  </style>
+</head>
+<body>
+  <div class="bookmarks-container">
+    <a href="index.html" class="back-link">← Back to Startpage</a>
+    <h1>Edit Bookmarks</h1>
+
+    <div class="bookmark-list" id="bookmarkList"></div>
+  </div>
+
+  <script>
+    const STORAGE_KEY = 'startpageBookmarks';
+    let bookmarks = [];
+
+    const defaultBookmarks = [
+      {
+        "name": "GitHub",
+        "url": "https://github.com",
+        "tags": ["dev", "code"]
+      },
+      {
+        "name": "Google",
+        "url": "https://google.com",
+        "tags": ["search"]
+      },
+      {
+        "name": "YouTube",
+        "url": "https://youtube.com",
+        "tags": ["video", "entertainment"]
+      },
+      {
+        "name": "Twitter",
+        "url": "https://twitter.com",
+        "tags": ["social"]
+      }
+    ];
+
+    async function loadBookmarks() {
+      const saved = localStorage.getItem(STORAGE_KEY);
+      if (saved) {
+        bookmarks = JSON.parse(saved);
+      } else {
+        try {
+          const response = await fetch('data.json');
+          const data = await response.json();
+          bookmarks = data.bookmarks || defaultBookmarks;
+        } catch (error) {
+          console.error('Failed to load data.json:', error);
+          bookmarks = defaultBookmarks;
+        }
+      }
+      renderBookmarks();
+    }
+
+    let draggedIndex = null;
+
+    function escapeHtml(text) {
+      const div = document.createElement('div');
+      div.textContent = text;
+      return div.innerHTML;
+    }
+
+    function clearDragOverClass() {
+      document.querySelectorAll('.bookmark-edit-item').forEach(item => {
+        item.classList.remove('drag-over');
+      });
+    }
+
+    function handleDragStart(e) {
+      draggedIndex = parseInt(e.currentTarget.dataset.index);
+      e.currentTarget.classList.add('dragging');
+      e.dataTransfer.effectAllowed = 'move';
+    }
+
+    function handleDragOver(e) {
+      e.preventDefault();
+      e.dataTransfer.dropEffect = 'move';
+      clearDragOverClass();
+      e.currentTarget.classList.add('drag-over');
+      return false;
+    }
+
+    function handleDrop(e) {
+      e.preventDefault();
+      e.stopPropagation();
+      clearDragOverClass();
+
+      const dropIndex = parseInt(e.currentTarget.dataset.index);
+      if (draggedIndex !== null && draggedIndex !== dropIndex) {
+        const [draggedItem] = bookmarks.splice(draggedIndex, 1);
+        bookmarks.splice(dropIndex, 0, draggedItem);
+        saveBookmarks();
+        renderBookmarks();
+      }
+      return false;
+    }
+
+    function handleDragEnd(e) {
+      e.currentTarget.classList.remove('dragging');
+      clearDragOverClass();
+      draggedIndex = null;
+    }
+
+    function createAddButton(position) {
+      const separator = document.createElement('div');
+      separator.className = 'add-bookmark-separator';
+      separator.dataset.position = position;
+      separator.innerHTML = `<button class="add-bookmark-btn" data-position="${position}">+</button>`;
+
+      // Allow dropping on separator to insert at position
+      separator.addEventListener('dragover', (e) => {
+        e.preventDefault();
+        e.dataTransfer.dropEffect = 'move';
+      });
+
+      separator.addEventListener('drop', (e) => {
+        e.preventDefault();
+        const dropPosition = parseInt(separator.dataset.position);
+        if (draggedIndex !== null) {
+          const [draggedItem] = bookmarks.splice(draggedIndex, 1);
+          const adjustedPosition = draggedIndex < dropPosition ? dropPosition - 1 : dropPosition;
+          bookmarks.splice(adjustedPosition, 0, draggedItem);
+          saveBookmarks();
+          renderBookmarks();
+        }
+      });
+
+      return separator;
+    }
+
+    function attachDragHandlers(item) {
+      item.addEventListener('dragstart', handleDragStart);
+      item.addEventListener('dragover', handleDragOver);
+      item.addEventListener('drop', handleDrop);
+      item.addEventListener('dragend', handleDragEnd);
+    }
+
+    function createBookmarkItem(bookmark, index) {
+      const item = document.createElement('div');
+      item.className = 'bookmark-edit-item';
+      item.draggable = true;
+      item.dataset.index = index;
+      item.innerHTML = `
+        <div class="bookmark-card">
+          <button class="delete-btn" data-index="${index}">×</button>
+          <div class="bookmark-fields">
+            <label>Name</label>
+            <input type="text" class="bookmark-name" value="${escapeHtml(bookmark.name)}" placeholder="Bookmark name" data-index="${index}">
+
+            <label>URL</label>
+            <input type="text" class="bookmark-url" value="${escapeHtml(bookmark.url)}" placeholder="https://example.com" data-index="${index}">
+
+            <label>Tags (comma-separated)</label>
+            <input type="text" class="bookmark-tags" value="${escapeHtml(bookmark.tags.join(', '))}" placeholder="tag1, tag2" data-index="${index}">
+            <div class="hint">Example: dev, code, web</div>
+          </div>
+        </div>
+        <div class="drag-handle">≡</div>
+      `;
+      attachDragHandlers(item);
+      return item;
+    }
+
+    function handleInputChange() {
+      validateBookmarks();
+      autoSaveBookmarks();
+    }
+
+    function renderBookmarks() {
+      const container = document.getElementById('bookmarkList');
+      container.innerHTML = '';
+
+      container.appendChild(createAddButton(0));
+
+      bookmarks.forEach((bookmark, index) => {
+        container.appendChild(createBookmarkItem(bookmark, index));
+        container.appendChild(createAddButton(index + 1));
+      });
+
+      document.querySelectorAll('.bookmark-name, .bookmark-url').forEach(input => {
+        input.addEventListener('input', handleInputChange);
+        input.addEventListener('focus', function() {
+          this.select();
+        });
+      });
+
+      // Tags: normalize format on blur
+      document.querySelectorAll('.bookmark-tags').forEach(input => {
+        input.addEventListener('input', handleInputChange);
+        input.addEventListener('focus', function() {
+          this.select();
+        });
+
+        input.addEventListener('blur', (e) => {
+          const normalized = normalizeTagsFormat(e.target.value);
+          if (e.target.value !== normalized) {
+            e.target.value = normalized;
+            handleInputChange();
+          }
+        });
+      });
+
+      document.querySelectorAll('.delete-btn').forEach(btn => {
+        btn.addEventListener('click', (e) => {
+          deleteBookmark(parseInt(e.target.dataset.index));
+        });
+      });
+
+      document.querySelectorAll('.add-bookmark-btn').forEach(btn => {
+        btn.addEventListener('click', (e) => {
+          addBookmarkAt(parseInt(e.target.dataset.position));
+        });
+      });
+
+      // Validate after rendering
+      validateBookmarks();
+    }
+
+    function saveBookmarks() {
+      localStorage.setItem(STORAGE_KEY, JSON.stringify(bookmarks));
+    }
+
+    function createNewBookmark() {
+      return {
+        name: '',
+        url: '',
+        tags: []
+      };
+    }
+
+    function addBookmarkAt(position) {
+      bookmarks.splice(position, 0, createNewBookmark());
+      saveBookmarks();
+      renderBookmarks();
+    }
+
+    function deleteBookmark(index) {
+      if (confirm('Delete this bookmark?')) {
+        bookmarks.splice(index, 1);
+        saveBookmarks();
+        renderBookmarks();
+      }
+    }
+
+    function parseTags(tagsString) {
+      return tagsString.split(',').map(tag => tag.trim()).filter(tag => tag);
+    }
+
+    function autoSaveBookmarks() {
+      const nameInputs = document.querySelectorAll('.bookmark-name');
+      const urlInputs = document.querySelectorAll('.bookmark-url');
+      const tagsInputs = document.querySelectorAll('.bookmark-tags');
+
+      bookmarks = Array.from(nameInputs).map((nameInput, index) => ({
+        name: nameInput.value,
+        url: urlInputs[index].value,
+        tags: parseTags(tagsInputs[index].value)
+      }));
+
+      saveBookmarks();
+    }
+
+    function isValidUrl(url) {
+      if (!url) return false;
+      try {
+        new URL(url);
+        return true;
+      } catch (e) {
+        return false;
+      }
+    }
+
+    function toggleError(input, hasError) {
+      input.classList.toggle('error', hasError);
+    }
+
+    function validateBookmarks() {
+      document.querySelectorAll('.bookmark-name').forEach(input => {
+        toggleError(input, input.value.trim() === '');
+      });
+
+      document.querySelectorAll('.bookmark-url').forEach(input => {
+        const url = input.value.trim();
+        toggleError(input, url === '' || !isValidUrl(url));
+      });
+
+      document.querySelectorAll('.bookmark-tags').forEach(input => {
+        const hasInvalidFormat = input.value && /,(?!\s|$)/.test(input.value);
+        toggleError(input, hasInvalidFormat);
+      });
+    }
+
+    function normalizeTagsFormat(tagsString) {
+      if (!tagsString) return '';
+      return tagsString.replace(/,\s*/g, ', ').replace(/,\s*$/, '');
+    }
+
+    loadBookmarks();
+  </script>
+</body>
+</html>

+ 310 - 0
docs/config.html

@@ -0,0 +1,310 @@
+<!DOCTYPE html>
+<html lang="ja">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Settings - Startpage</title>
+  <link rel="stylesheet" href="style.css">
+  <style>
+    .config-container {
+      max-width: 600px;
+      margin: 0 auto;
+      padding: 40px 20px;
+    }
+
+    h1 {
+      font-size: 2rem;
+      margin-bottom: 40px;
+      text-align: center;
+    }
+
+    .form-group {
+      margin-bottom: 30px;
+    }
+
+    label {
+      display: block;
+      margin-bottom: 8px;
+      font-size: 1rem;
+      color: #aaa;
+    }
+
+    input[type="text"],
+    input[type="color"] {
+      width: 100%;
+      padding: 12px;
+      background-color: rgba(255, 255, 255, 0.05);
+      border: 1px solid #333;
+      color: #fff;
+      font-family: 'Courier New', monospace;
+      font-size: 1rem;
+      border-radius: 4px;
+    }
+
+    input[type="color"] {
+      width: 100px;
+      height: 50px;
+      cursor: pointer;
+    }
+
+    .button-group {
+      display: flex;
+      gap: 12px;
+      margin-top: 40px;
+    }
+
+    button {
+      flex: 1;
+      padding: 14px;
+      background-color: #4a9eff;
+      border: none;
+      color: #fff;
+      font-family: 'Courier New', monospace;
+      font-size: 1rem;
+      cursor: pointer;
+      border-radius: 4px;
+      transition: background-color 0.2s;
+    }
+
+    button:hover {
+      background-color: #3a8eef;
+    }
+
+    button.secondary {
+      background-color: #333;
+    }
+
+    button.secondary:hover {
+      background-color: #444;
+    }
+
+    .hint {
+      font-size: 0.85rem;
+      color: #666;
+      margin-top: 4px;
+    }
+
+    .back-link {
+      display: inline-block;
+      margin-bottom: 20px;
+      color: #4a9eff;
+      text-decoration: none;
+    }
+
+    .back-link:hover {
+      text-decoration: underline;
+    }
+  </style>
+</head>
+<body>
+  <div class="config-container">
+    <a href="index.html" class="back-link">← Back to Startpage</a>
+    <h1>Settings</h1>
+
+    <div class="form-group">
+      <label for="backgroundColor">Background Color</label>
+      <input type="color" id="backgroundColor" value="#000000">
+    </div>
+
+    <div class="form-group">
+      <label for="textColor">Text Color</label>
+      <input type="color" id="textColor" value="#ffffff">
+    </div>
+
+    <div class="form-group">
+      <label for="accentColor">Accent Color</label>
+      <input type="color" id="accentColor" value="#4a9eff">
+    </div>
+
+    <div class="form-group">
+      <label for="searchEngine">Search Engine URL</label>
+      <input type="text" id="searchEngine" value="https://kagi.com/search?q=" placeholder="https://kagi.com/search?q=">
+      <div class="hint">Add search query parameter at the end (e.g., ?q=)</div>
+    </div>
+
+    <div class="form-group">
+      <label for="backgroundImage">Background Image URL</label>
+      <input type="text" id="backgroundImage" placeholder="background.jpg or https://..." value="">
+      <div class="hint">Local file path or external URL</div>
+    </div>
+
+    <div class="form-group">
+      <label for="backgroundBlur">Background Blur: <span id="blurValue">0</span>px</label>
+      <input type="range" id="backgroundBlur" value="0" min="0" max="20" step="1">
+      <div class="hint">Blur effect for background image (0-20px)</div>
+    </div>
+
+    <div class="form-group">
+      <label for="maskColor">Mask Color</label>
+      <input type="color" id="maskColor" value="#000000">
+      <div class="hint">Color overlay on background image</div>
+    </div>
+
+    <div class="form-group">
+      <label for="maskOpacity">Mask Opacity: <span id="opacityValue">60</span>%</label>
+      <input type="range" id="maskOpacity" value="60" min="0" max="100" step="1">
+      <div class="hint">Opacity of mask overlay (0-100%)</div>
+    </div>
+
+  </div>
+
+  <script>
+    const STORAGE_KEY = 'startpageConfig';
+
+    const DEFAULT_CONFIG = {
+      backgroundColor: '#000000',
+      textColor: '#ffffff',
+      accentColor: '#4a9eff',
+      searchEngine: 'https://kagi.com/search?q=',
+      backgroundImage: '',
+      backgroundBlur: 0,
+      maskColor: '#000000',
+      maskOpacity: 60
+    };
+
+    const CONFIG_FIELDS = [
+      'backgroundColor',
+      'textColor',
+      'accentColor',
+      'searchEngine',
+      'backgroundImage',
+      'backgroundBlur',
+      'maskColor',
+      'maskOpacity'
+    ];
+
+    function getConfigFromInputs() {
+      return CONFIG_FIELDS.reduce((config, field) => {
+        config[field] = document.getElementById(field).value;
+        return config;
+      }, {});
+    }
+
+    function updateSliderValue(sliderId, valueId) {
+      const slider = document.getElementById(sliderId);
+      const valueDisplay = document.getElementById(valueId);
+      valueDisplay.textContent = slider.value;
+    }
+
+    function setInputsFromConfig(config) {
+      CONFIG_FIELDS.forEach(field => {
+        document.getElementById(field).value = config[field];
+      });
+      updateSliderValue('backgroundBlur', 'blurValue');
+      updateSliderValue('maskOpacity', 'opacityValue');
+    }
+
+    async function loadSettings() {
+      const saved = localStorage.getItem(STORAGE_KEY);
+      if (saved) {
+        const config = JSON.parse(saved);
+        setInputsFromConfig(config);
+        applyTheme(config);
+      } else {
+        try {
+          const response = await fetch('data.json');
+          const data = await response.json();
+          const config = data.config || DEFAULT_CONFIG;
+          setInputsFromConfig(config);
+          applyTheme(config);
+        } catch (error) {
+          console.error('Failed to load data.json:', error);
+          setInputsFromConfig(DEFAULT_CONFIG);
+          applyTheme(DEFAULT_CONFIG);
+        }
+      }
+    }
+
+    function applyTheme(config) {
+      document.body.style.backgroundColor = config.backgroundColor;
+      document.body.style.color = config.textColor;
+
+      // Apply background image effects
+      removeElementById('background-style');
+      removeElementById('background-mask');
+
+      const backgroundImage = config.backgroundImage;
+      if (backgroundImage) {
+        applyBackgroundImage(backgroundImage, config.backgroundBlur);
+        applyMask(config.maskColor, config.maskOpacity);
+      }
+    }
+
+    function removeElementById(id) {
+      const element = document.getElementById(id);
+      if (element) {
+        element.remove();
+      }
+    }
+
+    function applyBackgroundImage(backgroundImage, backgroundBlur) {
+      const blur = backgroundBlur || 0;
+      const filterValue = blur > 0 ? `blur(${blur}px)` : 'none';
+
+      const bgStyle = document.createElement('style');
+      bgStyle.id = 'background-style';
+      bgStyle.textContent = `
+        body::before {
+          content: '';
+          position: fixed;
+          top: 0;
+          left: 0;
+          width: 100%;
+          height: 100%;
+          background-image: url('${backgroundImage}');
+          background-size: cover;
+          background-position: center;
+          background-repeat: no-repeat;
+          filter: ${filterValue};
+          z-index: -2;
+        }
+      `;
+      document.head.appendChild(bgStyle);
+      document.body.style.backgroundImage = 'none';
+    }
+
+    function applyMask(maskColor, maskOpacity) {
+      const opacity = (maskOpacity || 0) / 100;
+      if (opacity > 0) {
+        const mask = document.createElement('div');
+        mask.id = 'background-mask';
+        Object.assign(mask.style, {
+          position: 'fixed',
+          top: '0',
+          left: '0',
+          width: '100%',
+          height: '100%',
+          backgroundColor: maskColor || '#000000',
+          opacity: opacity.toString(),
+          zIndex: '-1',
+          pointerEvents: 'none'
+        });
+        document.body.appendChild(mask);
+      }
+    }
+
+    function autoSaveSettings() {
+      const config = getConfigFromInputs();
+      localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
+      applyTheme(config);
+    }
+
+    CONFIG_FIELDS.forEach(field => {
+      const element = document.getElementById(field);
+      element.addEventListener('input', autoSaveSettings);
+
+      // Select all text on focus for text inputs
+      if (element.type === 'text') {
+        element.addEventListener('focus', function() {
+          this.select();
+        });
+      }
+    });
+
+    document.getElementById('backgroundBlur').addEventListener('input', () => updateSliderValue('backgroundBlur', 'blurValue'));
+    document.getElementById('maskOpacity').addEventListener('input', () => updateSliderValue('maskOpacity', 'opacityValue'));
+
+    loadSettings();
+  </script>
+</body>
+</html>

+ 34 - 0
docs/data.json

@@ -0,0 +1,34 @@
+{
+  "config": {
+    "backgroundColor": "#000000",
+    "textColor": "#ffffff",
+    "accentColor": "#4a9eff",
+    "searchEngine": "https://kagi.com/search?q=",
+    "backgroundImage": "",
+    "backgroundBlur": 0,
+    "maskColor": "#000000",
+    "maskOpacity": 60
+  },
+  "bookmarks": [
+    {
+      "name": "GitHub",
+      "url": "https://github.com",
+      "tags": ["dev", "code"]
+    },
+    {
+      "name": "Google",
+      "url": "https://google.com",
+      "tags": ["search"]
+    },
+    {
+      "name": "YouTube",
+      "url": "https://youtube.com",
+      "tags": ["video", "entertainment"]
+    },
+    {
+      "name": "Twitter",
+      "url": "https://twitter.com",
+      "tags": ["social"]
+    }
+  ]
+}

+ 22 - 0
docs/index.html

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="ja">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Startpage</title>
+  <link rel="stylesheet" href="style.css">
+  <style>
+    body {
+      height: 100vh;
+      overflow: hidden;
+    }
+  </style>
+</head>
+<body>
+  <div class="container">
+    <input type="text" id="search-input" autocomplete="off" autofocus>
+    <div id="results"></div>
+  </div>
+  <script src="script.js"></script>
+</body>
+</html>

+ 631 - 0
docs/script.js

@@ -0,0 +1,631 @@
+let bookmarks = [];
+let config = {};
+let filteredBookmarks = [];
+let selectedIndex = 0;
+let currentCommandSuggestions = [];
+
+const searchInput = document.getElementById('search-input');
+const resultsContainer = document.getElementById('results');
+
+const STORAGE_KEYS = {
+  CONFIG: 'startpageConfig',
+  BOOKMARKS: 'startpageBookmarks'
+};
+
+const DEFAULT_CONFIG = {
+  backgroundColor: '#000000',
+  textColor: '#ffffff',
+  accentColor: '#4a9eff',
+  searchEngine: 'https://kagi.com/search?q=',
+  backgroundImage: '/Users/leonversteeg/Downloads/ChatGPT Image Feb 25, 2026, 02_14_59 PM.png',
+  backgroundBlur: 0,
+  maskColor: '#000000',
+  maskOpacity: 80
+};
+
+const STYLE_CONSTANTS = {
+  HOVER_OPACITY: 0.1,
+  SELECTED_OPACITY: 0.2
+};
+
+/**
+ * Loads data from localStorage or data.json file.
+ * Falls back to default values if both sources fail.
+ */
+async function loadData() {
+  // Try localStorage first
+  const savedConfig = localStorage.getItem(STORAGE_KEYS.CONFIG);
+  const savedBookmarks = localStorage.getItem(STORAGE_KEYS.BOOKMARKS);
+
+  /**
+  if (savedConfig && savedBookmarks) {
+    config = JSON.parse(savedConfig);
+    bookmarks = JSON.parse(savedBookmarks);
+    return;
+  }*/
+
+  // If localStorage is incomplete, try loading from data.json
+  try {
+    const response = await fetch('data.json');
+    const data = await response.json();
+    config = data.config || DEFAULT_CONFIG;
+    bookmarks = data.bookmarks || [];
+  } catch (error) {
+    console.error('Failed to load data.json:', error);
+    config = DEFAULT_CONFIG;
+    bookmarks = [];
+  }
+}
+
+function getOrCreateStyleElement(id) {
+  let element = document.getElementById(id);
+  if (!element) {
+    element = document.createElement('style');
+    element.id = id;
+    document.head.appendChild(element);
+  }
+  return element;
+}
+
+function removeElementById(id) {
+  const element = document.getElementById(id);
+  if (element) {
+    element.remove();
+  }
+}
+
+function applyBackgroundImage(backgroundImage, backgroundBlur) {
+  const blur = backgroundBlur || DEFAULT_CONFIG.backgroundBlur;
+  const filterValue = blur > 0 ? `blur(${blur}px)` : 'none';
+
+  const bgStyle = getOrCreateStyleElement('background-style');
+  bgStyle.textContent = `
+    body::before {
+      content: '';
+      position: fixed;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      background-image: url('${backgroundImage}');
+      background-size: cover;
+      background-position: center;
+      background-repeat: no-repeat;
+      filter: ${filterValue};
+      z-index: -2;
+    }
+  `;
+  document.body.style.backgroundImage = 'none';
+}
+
+function applyMask(maskColor, maskOpacity) {
+  removeElementById('background-mask');
+
+  const opacity = (maskOpacity || DEFAULT_CONFIG.maskOpacity) / 100;
+  if (opacity > 0) {
+    const mask = document.createElement('div');
+    mask.id = 'background-mask';
+    Object.assign(mask.style, {
+      position: 'fixed',
+      top: '0',
+      left: '0',
+      width: '100%',
+      height: '100%',
+      backgroundColor: maskColor || DEFAULT_CONFIG.maskColor,
+      opacity: opacity.toString(),
+      zIndex: '-1',
+      pointerEvents: 'none'
+    });
+    document.body.appendChild(mask);
+  }
+}
+
+function applyConfig() {
+  const {
+    backgroundColor,
+    textColor,
+    accentColor,
+    backgroundImage,
+    backgroundBlur,
+    maskColor,
+    maskOpacity
+  } = config;
+
+  document.body.style.backgroundColor = backgroundColor || DEFAULT_CONFIG.backgroundColor;
+  document.body.style.color = textColor || DEFAULT_CONFIG.textColor;
+  searchInput.style.color = textColor || DEFAULT_CONFIG.textColor;
+
+  if (backgroundImage) {
+    applyBackgroundImage(backgroundImage, backgroundBlur);
+    applyMask(maskColor, maskOpacity);
+  } else {
+    document.body.style.backgroundImage = 'none';
+    removeElementById('background-style');
+    removeElementById('background-mask');
+  }
+
+  const color = accentColor || DEFAULT_CONFIG.accentColor;
+  const style = getOrCreateStyleElement('dynamic-style');
+  style.textContent = `
+    .bookmark-item:hover,
+    .bookmark-item.selected {
+      border-left-color: ${color};
+    }
+    .bookmark-item:hover {
+      background-color: ${hexToRgba(color, STYLE_CONSTANTS.HOVER_OPACITY)};
+    }
+    .bookmark-item.selected {
+      background-color: ${hexToRgba(color, STYLE_CONSTANTS.SELECTED_OPACITY)};
+    }
+  `;
+}
+
+function hexToRgba(hex, alpha) {
+  const r = parseInt(hex.slice(1, 3), 16);
+  const g = parseInt(hex.slice(3, 5), 16);
+  const b = parseInt(hex.slice(5, 7), 16);
+  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
+}
+
+const commands = [
+  { name: ':list', description: 'Show all bookmarks' },
+  { name: ':config', description: 'Open settings' },
+  { name: ':bookmark', description: 'Edit bookmarks' },
+  { name: ':export', description: 'Export settings and bookmarks' },
+  { name: ':import', description: 'Import settings and bookmarks' },
+  { name: ':help', description: 'Show help' },
+  { name: ':reset', description: 'Reset all settings and bookmarks' }
+];
+
+let showingAllBookmarks = false;
+
+const commandHandlers = {
+  ':config': () => { window.location.href = 'config.html'; },
+  ':bookmark': () => { window.location.href = 'bookmarks.html'; },
+  ':help': () => { toggleHelp(); },
+  ':list': () => {
+    // Reload bookmarks from localStorage to get latest changes
+    const saved = localStorage.getItem(STORAGE_KEYS.BOOKMARKS);
+    if (saved) {
+      bookmarks = JSON.parse(saved);
+    }
+    showingAllBookmarks = true;
+    filteredBookmarks = bookmarks;
+    selectedIndex = 0;
+    renderResults();
+    searchInput.value = '';
+    searchInput.focus();
+  },
+  ':reset': () => {
+    if (confirm('Reset all settings and bookmarks to default? This cannot be undone.')) {
+      localStorage.removeItem(STORAGE_KEYS.CONFIG);
+      localStorage.removeItem(STORAGE_KEYS.BOOKMARKS);
+      window.location.reload();
+    }
+  },
+  ':export': () => { exportData(); },
+  ':import': () => { importData(); }
+};
+
+/**
+ * Executes a command if the query matches a known command.
+ * @param {string} query - The command string to execute
+ * @returns {boolean} - True if command was executed, false otherwise
+ */
+function executeCommand(query) {
+  const handler = commandHandlers[query];
+  if (handler) {
+    handler();
+    return true;
+  }
+  return false;
+}
+
+function getFromLocalStorage(key, fallback) {
+  const saved = localStorage.getItem(key);
+  return saved ? JSON.parse(saved) : fallback;
+}
+
+/**
+ * Exports settings and bookmarks as a JSON file
+ */
+function exportData() {
+  const data = {
+    config: getFromLocalStorage(STORAGE_KEYS.CONFIG, config),
+    bookmarks: getFromLocalStorage(STORAGE_KEYS.BOOKMARKS, bookmarks),
+    exportDate: new Date().toISOString()
+  };
+
+  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
+  const url = URL.createObjectURL(blob);
+  const a = document.createElement('a');
+  const today = new Date().toISOString().split('T')[0];
+  a.href = url;
+  a.download = `startpage-export-${today}.json`;
+  document.body.appendChild(a);
+  a.click();
+  document.body.removeChild(a);
+  URL.revokeObjectURL(url);
+}
+
+/**
+ * Imports settings and bookmarks from a JSON file
+ */
+function importData() {
+  const input = document.createElement('input');
+  input.type = 'file';
+  input.accept = 'application/json';
+  input.onchange = (e) => {
+    const file = e.target.files[0];
+    if (!file) return;
+
+    const reader = new FileReader();
+    reader.onload = (event) => {
+      try {
+        const data = JSON.parse(event.target.result);
+
+        if (data.config) {
+          localStorage.setItem(STORAGE_KEYS.CONFIG, JSON.stringify(data.config));
+        }
+        if (data.bookmarks) {
+          localStorage.setItem(STORAGE_KEYS.BOOKMARKS, JSON.stringify(data.bookmarks));
+        }
+
+        alert('Import successful! Page will reload.');
+        window.location.reload();
+      } catch (error) {
+        alert('Failed to import: Invalid file format');
+        console.error('Import error:', error);
+      }
+    };
+    reader.readAsText(file);
+  };
+  input.click();
+}
+
+/**
+ * Checks if a bookmark matches the search query.
+ * @param {Object} bookmark - The bookmark to check
+ * @param {string} lowerQuery - The lowercased search query
+ * @returns {boolean} - True if bookmark matches
+ */
+function bookmarkMatches(bookmark, lowerQuery) {
+  return bookmark.name.toLowerCase().includes(lowerQuery) ||
+         bookmark.url.toLowerCase().includes(lowerQuery) ||
+         bookmark.tags.some(tag => tag.toLowerCase().includes(lowerQuery));
+}
+
+/**
+ * Filters bookmarks or shows command suggestions based on query.
+ * @param {string} query - The search query
+ */
+function filterBookmarks(query) {
+  if (!query) {
+    filteredBookmarks = showingAllBookmarks ? bookmarks : [];
+    currentCommandSuggestions = [];
+    selectedIndex = 0;
+    renderResults();
+    return;
+  }
+
+  if (query.startsWith(':')) {
+    showingAllBookmarks = false;
+    const matchedCommands = commands.filter(cmd =>
+      cmd.name.startsWith(query.toLowerCase())
+    );
+    currentCommandSuggestions = matchedCommands;
+    selectedIndex = 0;
+    renderCommandSuggestions(matchedCommands);
+    return;
+  }
+
+  currentCommandSuggestions = [];
+  const lowerQuery = query.toLowerCase();
+  filteredBookmarks = bookmarks.filter(bookmark => bookmarkMatches(bookmark, lowerQuery));
+  selectedIndex = 0;
+  renderResults();
+}
+
+function createTextElement(className, text) {
+  const element = document.createElement('div');
+  element.className = className;
+  element.textContent = text;
+  return element;
+}
+
+function createResultItem(isSelected, onClickHandler) {
+  const item = document.createElement('div');
+  item.className = 'bookmark-item' + (isSelected ? ' selected' : '');
+  item.addEventListener('click', onClickHandler);
+  return item;
+}
+
+function renderCommandSuggestions(matchedCommands) {
+  resultsContainer.innerHTML = '';
+
+  matchedCommands.forEach((cmd, index) => {
+    const item = createResultItem(index === selectedIndex, () => {
+      searchInput.value = cmd.name;
+      executeCommand(cmd.name);
+    });
+
+    item.appendChild(createTextElement('bookmark-name', cmd.name));
+    item.appendChild(createTextElement('bookmark-url', cmd.description));
+    resultsContainer.appendChild(item);
+  });
+
+  updateScrollFade();
+}
+
+function createTagsElement(tags) {
+  const tagsContainer = document.createElement('div');
+  tagsContainer.className = 'bookmark-tags';
+  tags.forEach(tag => {
+    const tagSpan = document.createElement('span');
+    tagSpan.className = 'tag';
+    tagSpan.textContent = `#${tag}`;
+    tagsContainer.appendChild(tagSpan);
+  });
+  return tagsContainer;
+}
+
+function renderResults() {
+  resultsContainer.innerHTML = '';
+
+  filteredBookmarks.forEach((bookmark, index) => {
+    const item = createResultItem(index === selectedIndex, () => openBookmark(bookmark));
+
+    item.appendChild(createTextElement('bookmark-name', bookmark.name));
+    item.appendChild(createTextElement('bookmark-url', bookmark.url));
+    item.appendChild(createTagsElement(bookmark.tags));
+    resultsContainer.appendChild(item);
+  });
+
+  updateScrollFade();
+}
+
+function openBookmark(bookmark) {
+  window.location.href = bookmark.url;
+}
+
+function toggleHelp() {
+  const existingOverlay = document.getElementById('help-overlay');
+  if (existingOverlay) {
+    existingOverlay.remove();
+    return;
+  }
+
+  const accentColor = config.accentColor || DEFAULT_CONFIG.accentColor;
+  const textColor = config.textColor || DEFAULT_CONFIG.textColor;
+
+  const helpOverlay = document.createElement('div');
+  helpOverlay.id = 'help-overlay';
+  Object.assign(helpOverlay.style, {
+    position: 'fixed',
+    top: '0',
+    left: '0',
+    width: '100%',
+    height: '100%',
+    background: 'rgba(0, 0, 0, 0.9)',
+    display: 'flex',
+    alignItems: 'center',
+    justifyContent: 'center',
+    zIndex: '1000'
+  });
+
+  const helpContent = document.createElement('div');
+  Object.assign(helpContent.style, {
+    background: 'rgba(20, 20, 20, 0.95)',
+    border: `1px solid ${accentColor}`,
+    borderRadius: '8px',
+    padding: '40px',
+    maxWidth: '500px',
+    color: textColor
+  });
+
+  helpContent.innerHTML = `
+    <h2 style="margin-bottom: 30px; text-align: center; color: ${accentColor};">Keyboard Shortcuts</h2>
+    <div style="line-height: 2;">
+      <div><strong>:</strong> - Show command suggestions</div>
+      <div><strong>:list</strong> - Show all bookmarks</div>
+      <div><strong>:config</strong> - Open settings</div>
+      <div><strong>:bookmark</strong> - Edit bookmarks</div>
+      <div><strong>:export</strong> - Export settings and bookmarks</div>
+      <div><strong>:import</strong> - Import settings and bookmarks</div>
+      <div><strong>:reset</strong> - Reset all settings and bookmarks</div>
+      <div><strong>:help</strong> - Show/hide this help</div>
+      <div><strong>↓ / ↑</strong> - Move down / up</div>
+      <div><strong>Enter</strong> - Open bookmark / Search</div>
+      <div><strong>Esc</strong> - Clear input / Close help</div>
+    </div>
+    <div style="margin-top: 30px; text-align: center; color: #666; font-size: 0.9rem;">
+      Press Esc to close
+    </div>
+  `;
+
+  helpOverlay.appendChild(helpContent);
+  document.body.appendChild(helpOverlay);
+
+  helpOverlay.addEventListener('click', (e) => {
+    if (e.target === helpOverlay) {
+      helpOverlay.remove();
+    }
+  });
+}
+
+/**
+ * Updates the fade effect classes based on scroll position.
+ */
+function updateScrollFade() {
+  const scrollTop = resultsContainer.scrollTop;
+  const scrollHeight = resultsContainer.scrollHeight;
+  const clientHeight = resultsContainer.clientHeight;
+
+  // Check if content is scrollable
+  const isScrollable = scrollHeight > clientHeight;
+
+  if (!isScrollable) {
+    resultsContainer.classList.remove('has-scroll-top', 'has-scroll-bottom');
+    return;
+  }
+
+  // Check if scrolled from top (with small threshold)
+  const hasScrollTop = scrollTop > 10;
+
+  // Check if not at bottom (with small threshold)
+  const hasScrollBottom = scrollTop < scrollHeight - clientHeight - 10;
+
+  resultsContainer.classList.toggle('has-scroll-top', hasScrollTop);
+  resultsContainer.classList.toggle('has-scroll-bottom', hasScrollBottom);
+}
+
+/**
+ * Scrolls the selected item into view, keeping it centered when possible.
+ */
+function scrollSelectedIntoView() {
+  const selectedElement = resultsContainer.querySelector('.bookmark-item.selected');
+  if (!selectedElement) return;
+
+  const containerHeight = resultsContainer.clientHeight;
+  const elementHeight = selectedElement.offsetHeight;
+  const centerPosition = containerHeight / 2 - elementHeight / 2;
+  const desiredScroll = selectedElement.offsetTop - centerPosition;
+
+  resultsContainer.scrollTo({
+    top: desiredScroll,
+    behavior: 'smooth'
+  });
+
+  setTimeout(updateScrollFade, 100);
+}
+
+/**
+ * Moves the selection cursor up or down in the results list.
+ * @param {string} direction - Either 'up' or 'down'
+ */
+function moveSelection(direction) {
+  const items = currentCommandSuggestions.length > 0 ? currentCommandSuggestions : filteredBookmarks;
+  if (items.length === 0) return;
+
+  const delta = direction === 'down' ? 1 : -1;
+  const newIndex = selectedIndex + delta;
+
+  // Don't loop - stop at boundaries
+  if (newIndex < 0 || newIndex >= items.length) {
+    return;
+  }
+
+  selectedIndex = newIndex;
+
+  if (currentCommandSuggestions.length > 0) {
+    renderCommandSuggestions(currentCommandSuggestions);
+  } else {
+    renderResults();
+  }
+
+  scrollSelectedIntoView();
+}
+
+searchInput.addEventListener('input', (e) => {
+  filterBookmarks(e.target.value);
+});
+
+// Add scroll event listener to update fade effects
+resultsContainer.addEventListener('scroll', updateScrollFade);
+
+document.addEventListener('keydown', (e) => {
+  const isInputFocused = document.activeElement === searchInput;
+  const noModifiers = !e.ctrlKey && !e.metaKey;
+
+  // Auto-focus on typing
+  if (!isInputFocused && noModifiers && !e.altKey && e.key.length === 1 && e.key !== ' ') {
+    searchInput.focus();
+    return;
+  }
+
+  // Navigation and actions when input is focused
+  if (isInputFocused) {
+    if (e.key === 'ArrowDown' && noModifiers) {
+      e.preventDefault();
+      moveSelection('down');
+    } else if (e.key === 'ArrowUp' && noModifiers) {
+      e.preventDefault();
+      moveSelection('up');
+    } else if (e.key === 'Enter') {
+      e.preventDefault();
+      handleEnterKey();
+    } else if (e.key === 'Escape') {
+      e.preventDefault();
+      handleEscapeKey();
+    }
+  }
+});
+
+function isUrl(query) {
+  return query.startsWith('http://') ||
+         query.startsWith('https://') ||
+         /^[a-zA-Z0-9][-a-zA-Z0-9]*(\.[a-zA-Z0-9][-a-zA-Z0-9]*)+/.test(query);
+}
+
+function handleEnterKey() {
+  const query = searchInput.value.trim();
+
+  if (currentCommandSuggestions.length > 0) {
+    const selectedCommand = currentCommandSuggestions[selectedIndex];
+    searchInput.value = selectedCommand.name;
+    currentCommandSuggestions = [];
+    executeCommand(selectedCommand.name);
+    return;
+  }
+
+  if (executeCommand(query)) {
+    return;
+  }
+
+  if (filteredBookmarks.length > 0) {
+    openBookmark(filteredBookmarks[selectedIndex]);
+  } else if (query && !query.startsWith(':')) {
+    if (isUrl(query)) {
+      // Add https:// if no protocol specified
+      const url = query.startsWith('http') ? query : `https://${query}`;
+      window.location.href = url;
+    } else {
+      const searchEngine = config.searchEngine || DEFAULT_CONFIG.searchEngine;
+      window.location.href = searchEngine + encodeURIComponent(query);
+    }
+  }
+}
+
+function handleEscapeKey() {
+  const helpOverlay = document.getElementById('help-overlay');
+  if (helpOverlay) {
+    helpOverlay.remove();
+  } else {
+    showingAllBookmarks = false;
+    searchInput.value = '';
+    filterBookmarks('');
+    searchInput.focus();
+  }
+}
+
+// Reload bookmarks when page becomes visible (e.g., returning from bookmark editor)
+document.addEventListener('visibilitychange', () => {
+  if (!document.hidden) {
+    const saved = localStorage.getItem(STORAGE_KEYS.BOOKMARKS);
+    if (saved) {
+      bookmarks = JSON.parse(saved);
+      // Update current view if showing all bookmarks
+      if (showingAllBookmarks) {
+        filteredBookmarks = bookmarks;
+        renderResults();
+      }
+    }
+  }
+});
+
+async function init() {
+  await loadData();
+  applyConfig();
+}
+
+init();

+ 103 - 0
docs/style.css

@@ -0,0 +1,103 @@
+* {
+  margin: 0;
+  padding: 0;
+  box-sizing: border-box;
+}
+
+body {
+  font-family: 'Courier New', monospace;
+  background-color: #000000;
+  color: #ffffff;
+  min-height: 100vh;
+}
+
+.container {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  min-height: 100vh;
+  padding: 20px;
+}
+
+#search-input {
+  background: transparent;
+  border: none;
+  color: #ffffff;
+  font-size: 2rem;
+  text-align: center;
+  outline: none;
+  width: 80%;
+  max-width: 600px;
+  font-family: 'Courier New', monospace;
+}
+
+#results {
+  margin-top: 40px;
+  width: 80%;
+  max-width: 600px;
+  max-height: 35vh;
+  overflow-y: auto;
+  overflow-x: hidden;
+  position: relative;
+  -ms-overflow-style: none;
+  scrollbar-width: none;
+}
+
+#results::-webkit-scrollbar {
+  display: none;
+}
+
+#results.has-scroll-top {
+  mask-image: linear-gradient(to bottom, transparent 0, black 80px, black 100%);
+  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 80px, black 100%);
+}
+
+#results.has-scroll-bottom {
+  mask-image: linear-gradient(to bottom, black 0, black calc(100% - 80px), transparent 100%);
+  -webkit-mask-image: linear-gradient(to bottom, black 0, black calc(100% - 80px), transparent 100%);
+}
+
+#results.has-scroll-top.has-scroll-bottom {
+  mask-image: linear-gradient(to bottom, transparent 0, black 80px, black calc(100% - 80px), transparent 100%);
+  -webkit-mask-image: linear-gradient(to bottom, transparent 0, black 80px, black calc(100% - 80px), transparent 100%);
+}
+
+.bookmark-item {
+  padding: 12px 16px;
+  margin: 8px 0;
+  cursor: pointer;
+  border-left: 3px solid transparent;
+  transition: all 0.2s ease;
+}
+
+.bookmark-item:hover {
+  border-left-color: #4a9eff;
+  background-color: rgba(74, 158, 255, 0.1);
+}
+
+.bookmark-item.selected {
+  border-left-color: #4a9eff;
+  background-color: rgba(74, 158, 255, 0.2);
+}
+
+.bookmark-name {
+  font-size: 1.2rem;
+  margin-bottom: 4px;
+}
+
+.bookmark-url {
+  font-size: 0.9rem;
+  color: #888;
+}
+
+.bookmark-tags {
+  margin-top: 4px;
+  font-size: 0.8rem;
+  color: #666;
+}
+
+.tag {
+  display: inline-block;
+  margin-right: 8px;
+}